OpenResty 最佳實踐 (1)

網易雲社群發表於2018-10-31


此文已由作者湯曉靜授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。

OpenResty 發展起源

OpenResty(也稱為 ngx_openresty)是一個全功能的 Web 應用伺服器。它打包了標準的 nginx 核心,很多的常用的第三方模組,以及它們的大多數依賴項。 通過揉和眾多設計良好的 nginx 模組,OpenResty 有效地把 nginx 伺服器轉變為一個強大的 Web 應用伺服器,基於它開發人員可以使用 lua 程式語言對 nginx 核心以及現有的各種 nginx C 模組進行指令碼程式設計,構建出可以處理一萬以上併發請求的極端高效能的 Web 應用。

OpenResty 致力於將你的伺服器端應用完全執行於 nginx 伺服器中,充分利用 nginx 的事件模型來進行非阻塞 I/O 通訊。不僅僅是和 HTTP 客戶端間的網路通訊是非阻塞的,與 MySQL、PostgreSQL、Memcached 以及 Redis 等眾多後端之間的網路通訊也是非阻塞的。 因為 OpenResty 軟體包的維護者也是其中打包的許多 nginx 模組的作者,所以 OpenResty 可以確保所包含的所有元件可以可靠地協同工作。

OpenResty 最早是雅虎中國的一個公司專案,起步於 2007 年 10 月。當時興起了 OpenAPI 的熱潮,用於滿足各種 Web Service 的需求,基於 Perl 和 Haskell 實現; 2009 章亦春在加入淘寶資料部門的量子團隊,決定對 OpenResty 進行重新設計和徹底重寫,並把應用重點放在支援像量子統計這樣的 Web 產品上面,這是第二代的 OpenResty,基於 nginx 和 lua 進行開發。

為什麼要取 OpenResty 這個名字呢?OpenResty 最早是順應 OpenAPI 的潮流做的,所以 Open 取自“開放”之意,而 Resty 便是 REST 風格的意思。雖然後來也可以基於 ngx_openresty 實現任何形式的 Web service 或者傳統的 Web 應用。

也就是說 nginx 不再是一個簡單的靜態網頁伺服器,也不再是一個簡單的反向代理了,OpenResty 致力於通過一系列 nginx 模組,把 nginx 擴充套件為全功能的 Web 應用伺服器,目前有兩大應用目標:

  1. 通用目的的 Web 應用伺服器。在這個目標下,現有的 Web 應用技術都可以算是和 OpenResty 或多或少有些類似,比如 Nodejs,PHP 等等,但 OpenResty 的效能更加出色。

  2. nginx 的指令碼擴充套件程式設計,為構建靈活的 Web 應用閘道器和 Web 應用防火牆等功能提供了極大的便利性。

OpenResty 特性概括如下:

  • 基於 nginx 的 Web 伺服器

  • 打包 nginx 核心、常用的第三方模組及依賴項

  • 使用 lua 對 nginx 進行指令碼程式設計

  • 充分利用 nginx 的事件模型進行非阻塞 I/O 通訊

  • 使用 lua 以同步方式進行非同步程式設計

  • 擴充後端通訊方式

綜合 OpenResty 的特性,它不僅具備 nginx 的負載均衡、反向代理及傳統 http server 等功能,還可以利用 lua 指令碼程式設計實現路由閘道器,實現訪問認證、流量控制、路由控制及日誌處理等多種功能;同時利用 cosocket 擴充和後端(mysql、redis、kafaka)通訊後,更可開發通用的 restful api 程式。

OpenResty 之 lua 程式設計

lua 簡介

1993 年在巴西里約熱內盧天主教大學誕生了一門程式語言,他們給這門語言取了個浪漫的名字 — lua,在葡萄牙語裡代表美麗的月亮。事實證明他們沒有糟蹋這個優美的單詞,lua 語言正如它名字所預示的那樣成長為一門簡潔、優雅且富有樂趣的語言。

lua 從一開始就是作為一門方便嵌入(其它應用程式)並可擴充套件的輕量級指令碼語言來設計,因此她一直遵從著簡單、小巧、可移植、快速的原則,官方實現完全採用 ANSI C 編寫,能以 C 程式庫的形式嵌入到宿主程式中。luaJIT 2 和標準 lua 5.1 直譯器採用的是著名的 MIT 許可協議。正由於上述特點,所以 lua 在遊戲開發、機器人控制、分散式應用、影象處理、生物資訊學等各種各樣的領域中得到了越來越廣泛的應用。其中尤以遊戲開發為最,許多著名的遊戲,比如 World of Warcraft、大話西遊,都採用了 lua 來配合引擎完成資料描述、配置管理和邏輯控制等任務。即使像 Redis 這樣中性的記憶體鍵值資料庫也提供了內嵌使用者 lua 指令碼的官方支援。

作為一門過程型動態語言,lua 有著如下的特性:

  1. 變數名沒有型別,值才有型別,變數名在執行時可與任何型別的值繫結;

  2. 語言只提供唯一一種資料結構,稱為表(table),它混合了陣列、雜湊,可以用任何型別的值作為 key 和 value。提供了一致且富有表達力的表構造語法,使得 lua 很適合描述複雜的資料;

  3. 函式是一等型別,支援匿名函式和正則尾遞迴(proper tail recursion);

  4. 支援詞法定界(lexical scoping)和閉包(closure);

  5. 提供 thread 型別和結構化的協程(coroutine)機制,在此基礎上可方便實現協作式多工;

  6. 執行期能編譯字串形式的程式文字並載入虛擬機器執行;

  7. 通過元表(metatable)和元方法(metamethod)提供動態元機制(dynamic meta-mechanism),從而允許程式執行時根據需要改變或擴充語法設施的內定語義;

  8. 能方便地利用表和動態元機制實現基於原型(prototype-based)的物件導向模型;

  9. 從 5.1 版開始提供了完善的模組機制,從而更好地支援開發大型的應用程式;

lua 基礎資料型別

print(type("hello world")) --> output:stringprint(type(print))         --> output:functionprint(type(true))          --> output:booleanprint(type(360.0))         --> output:numberprint(type(nil))           --> output:nil複製程式碼

nil

nil 是一種型別,lua 將 nil 用於表示“無效值”。一個變數在第一次賦值前的預設值是 nil,將 nil 賦予給一個全域性變數就等同於刪除它。

local numprint(num)        --> output:nil

num = 100print(num)        --> output:100複製程式碼

boolean (true/false)

布林型別,可選值 true/false;lua 中 nil 和 false 為“假”,其它所有值均為“真”,比如 0 和空字串就是“真”。

local a = truelocal b = 0local c = nilif a then
    print("a")        --> output:aelse
    print("not a")    -- 這個沒有執行
endif b then
    print("b")        --> output:belse
    print("not b")    -- 這個沒有執行
endif c then
    print("c")        -- 這個沒有執行else
    print("not c")    --> output:not c
end複製程式碼

number

Number 型別用於表示實數,和 C/C++ 裡面的 double 型別很類似。可以使用數學函式 math.floor(向下取整)和 math.ceil(向上取整)進行取整操作。

local order = 3.99local score = 98.01print(math.floor(order))   --> output:3print(math.ceil(score))    --> output:99複製程式碼

string

和其他語言 string 大同小異

local str1 = 'hello world'local str2 = "hello lua"local str3 = [["add\name",'hello']]
local str4 = [=[string have a [[]].]=]print(str1)    --> output:hello worldprint(str2)    --> output:hello luaprint(str3)    --> output:"add\name",'hello'print(str4)    --> output:string have a [[]].複製程式碼

table (陣列、字典)

Table 型別實現了一種抽象的“關聯陣列”。“關聯陣列”是一種具有特殊索引方式的陣列,索引通常是字串(string)或者 number 型別,但也可以是除 nil 以外的任意型別的值。

local corp = {
    web = "www.google.com",             -- 索引為字串,key = "web",
                                        --             value = "www.google.com"
    telephone = "12345678",             -- 索引為字串
    staff = {"Jack", "Scott", "Gary"},  -- 索引為字串,值也是一個表    100876,                             -- 相當於 [1] = 100876,此時索引為數字
                                        --       key = 1, value = 100876
    100191,                             -- 相當於 [2] = 100191,此時索引為數字
    [10] = 360,                         -- 直接把數字索引給出
    ["city"] = "Beijing"                -- 索引為字串
}print(corp.web)                         --> output:www.google.comprint(corp["telephone"])                --> output:12345678print(corp[2])                          --> output:100191print(corp["city"])                     --> output:"Beijing"print(corp.staff[1])                    --> output:Jackprint(corp[10])                         --> output:360複製程式碼

在內部實現上,table 通常實現為一個雜湊表、一個陣列、或者兩者的混合。具體的實現為何種形式,動態依賴於具體的 table 的鍵分佈特點。

function

在 lua 中,函式也是一種資料型別,函式可以儲存在變數中,可以通過引數傳遞給其他函式,還可以作為其他函式的返回值。

local function foo()
    print("in the function")
    -- dosomething()
    local x = 10
    local y = 20
    return x + y
end

local a = foo    -- 把函式賦給變數

print(a())

-- output:in the function30複製程式碼

lua 表示式

算術運算子說明關係運算子說明邏輯運算子說明
+加法<小於and邏輯與
-減法>大於or邏輯或
*乘法<=小於等於not邏輯非
/除法>=大於等於--
^指數~=不等於--
%取模----

note: lua 中的不等於用 ~= 表示, 和其他語言的 != 不一致

lua 流程控制

lua 的流程控制結構和 python 類似,有幾個特例:

  • lua 中的 elseif 需要連寫,中間不能有空行;python 中寫法是 elif

  • lua 中沒有 continue 流控

if/else/elseif

if a = 1 then    print("1")elseif a == 2 then    print("2")else
    print("3")end複製程式碼

while

while a > 1 do    if a == 5 then
        break
    end
    a = a + 1end複製程式碼

repeat

local i = 0repeat    print(i)
    if i == 5 then        break
    end
until true複製程式碼

for/break

local t = { a = 1, b = 2}for k, v in pairs(t) do  -- 遍歷字典    print(k, v)end

local t = {1, 2}for k, v in ipairs(t) do -- 遍歷整型陣列    print(k, v)endfor i = 1, 10 do        -- range 迴圈
    print(i)
end複製程式碼

return

local function foo(arg)    if arg == "" then
        return nil
    end

    return "bar"end複製程式碼

OpenResty 模組編寫

編寫一個 access.lua 模組,原始碼如下:

local _M = {}

_M.check = function()    if ngx.var.http_host == "foo.bar.com" then
        ngx.exit(403)    endendreturn _M       -- 注意 return _M,返回 table 表示的模組複製程式碼

在 access_by_lua 的 nginx hook 中呼叫 access 模組:

access_by_lua_block {    local rule = require "access"   -- require 中不需要加 `.lua` 字尾
    rule.check()
}複製程式碼

OpenResty 核心原理

nginx 程式模型

nginx 是一個 master + 多個 worker 程式模型;master 程式負責管理和監控 worker 程式,如載入和解析配置檔案,重啟 worker 程式,更新二進位制檔案等。 worker 程式負責處理請求,每個 worker 地位和功能相同,內部按照 epoll + callback 方式實現併發連線處理;整體架構圖如下: nginx 架構模型

nginx 請求處理流程

每個 worker 程式都分階段處理 http 請求,簡單概括為初始化請求 -> 處理請求行 -> 後端互動 -> 響應頭處理 -> 響應包體處理 -> 列印日誌等幾個階段。其中處理響應體階段又可以掛載多個不同的 filter。具體的請求階段可以參見 nginx Phase, nginx 請求處理流程如下圖: nginx請求處理流程

nginx 事件機制

nginx 的事件驅動機制是對 epoll 驅動的封裝,但其本質還是 epoll + callback 方式: nginx事件機制

lua 協程

函式描述
coroutine.create()建立 coroutine,返回 coroutine,引數是一個函式,當和 resume 配合使用的時候就喚醒函式呼叫
coroutine.resume()重啟 coroutine,和 create 配合使用
coroutine.yield()掛起 coroutine,將 coroutine 設定為掛起狀態,這個和 resume 配合使用能有很多有用的效果
coroutine.status()檢視 coroutine 的狀態。注:coroutine 的狀態有四種:dead,suspend,running,normal

coroutine.create(f)

建立一個主體函式為 f 的新協程。f 必須是一個 lua 的函式。返回這個新協程,它是一個型別為 "thread" 的物件,建立後並不會啟動該協程。

coroutine.resume(co, [, val1, ...])

開始或繼續協程 co 的執行。當第一次執行一個協程時,他會從主函式處開始執行。val1, ... 這些值會以引數形式傳入主體函式。如果該協程被掛起,resume 會重新啟動它;val1, ... 這些引數會作為掛起點的返回值。如果協程執行起來沒有錯誤,resume 返回 true 加上傳給 yield 的所有值 (當協程掛起),或是主體函式的所有返回值(當協程中止)。

coroutine.yield(...)

掛起正在呼叫的協程的執行。 傳遞給 yield 的引數都會轉為 resume 的額外返回值。

coroutine.status(co)

以字串形式返回協程 co 的狀態:

  • 當協程正在執行(它就是呼叫 status 的那個) ,返回 "running";

  • 如果協程呼叫 yield 掛起或是還沒有開始執行,返回 "suspended";

  • 如果協程是活動的,都並不在執行(即它正在延續其它協程),返回 "normal";

  • 如果協程執行完主體函式或因錯誤停止,返回 "dead"。

協程例項(生產者消費者)

使用協程實現生產者消費者:

local function produce()
    while true do
        local x = io.read()
        coroutine.yield(x)       -- 掛起協程
    endendlocal producer = coroutine.create(produce)  -- 建立協程local function receive()    local status, value = coroutine.resume(producer)  -- 執行協程
    return valueendlocal function consumer()
    while true do
        local x = receive()
        io.write(x, "\n")    endendconsumer() -- loop複製程式碼

lua 與 c 堆疊互動

lua 虛擬機器常嵌入 C 程式中執行,對於 C 程式來說,lua 虛擬機器就是一個子程式。lua 將所有狀態都儲存在 lua_State 型別中,所有的 C API 都要求傳入一個指向該結構的指標。我們根據這個指標來獲取 lua 虛擬機器(也就是子程式)的狀態。

虛擬機器內部與外部的 C 程式發生資料交換主要是通過一個公用棧實現的,也就是說 lua 虛擬機器和 C 程式公用一個棧,雙方都可以壓棧或讀取資料。一方壓入,另一方彈出就能實現資料的交換。

在 c 中,lua 堆疊就是一個 struct,堆疊索引方式可能是正數也可能是負數,區別是:正數索引 1 永遠表示棧底,負數索引 -1 永遠表示棧頂。 堆疊的預設大小是 20,可以用 lua_checkstack 修改,用 lua_gettop 則可以獲得棧裡的元素數目。

C 呼叫 lua

  • 在 C 中建立 lua 虛擬機器

    lua_State *luaL_newstate (void)複製程式碼
  • 載入 lua 的庫函式

    void luaL_openlibs (lua_State *L);複製程式碼
  • 載入 lua 檔案,使用介面

    int luaL_dofile (lua_State *L, const char *filename);複製程式碼
  • 開始互動,lua 定義一個函式

    function test_func_add(a, b) return a + b end複製程式碼
  • 如果你的 lua_State 是全域性變數,那麼每次對堆疊有新操作時務必使用lua_settop(lua_State, -1)將偏移重新置到棧頂

  • 去lua檔案中取得test_func_add方法

    void lua_getglobal (lua_State *L, const char *name);複製程式碼
  • 引數壓棧

    lua_pushnumber複製程式碼
  • 通過 pcall 呼叫

    int lua_pcall (lua_State *L, int nargs, int nresults, int msg);複製程式碼

完整示例,先編寫一個 foo.lua 檔案,在檔案中實現 test_func_add 方法

function test_func_add(a, b)
    return a + b
end複製程式碼

接下來在 C 程式碼中呼叫 foo.lua:

lua_State* init_lua()
{
    lua_State* s_lua = luaL_newstate();    if (!s_lua) {
        printf("luaL_newstate failed!\n");
        exit(-1);
    }
    luaL_openlibs(s_lua);    return s_lua;
}bool load_lua_file(lua_State* s_lua, const char* lua_file){    if (luaL_dofile(s_lua, lua_file) != 0) {
        printf("LOAD LUA %s %s\n", lua_file, BOOT_FAIL);        return false;
    }
    printf("LOAD LUA %s %s\n", lua_file, BOOT_OK);    return true;
}int proc_add_operation(lua_State* s_lua, int a, int b){
    lua_settop(s_lua, -1);
    lua_getglobal(s_lua, "test_func_add");
    lua_pushnumber(s_lua, a);
    lua_pushnumber(s_lua, b);    int val = lua_pcall(s_lua, 2, 1, 0);    if (val) {
        printf("lua_pcall_error %d\n", val);
    }    return (int)lua_tonumber(s_lua, -1);
}int main() {
    lua_State* s_lua =init_lua();    if (!load_lua_file(s_lua, "foo")) {        return -1;
    }

    proc_add_operation(s_lua, 1, 2);
}複製程式碼

lua 呼叫 c

  • 定義誰先實現 C 介面

    #define target 300static int l_test_check_value(lua_State * l){  int num = lua_tointeger(l, -1);  bool check = (num == target);
      lua_pushboolean(l, check);  return 1;
    }複製程式碼
  • lua 虛擬啟動時候,註冊載入 C 介面

    lua_register(s_lua, "test_check_value", l_test_check_value);複製程式碼
  • 在 lua 程式碼中呼叫註冊的 C 介面

    function test_func_check(a)
      local val = test_check_value(a)  return val
    end複製程式碼


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選




相關文章:
【推薦】 Wireshark對HTTPS資料的解密
【推薦】 網易七魚 Android 高效能日誌寫入方案


相關文章