OpenResty 最佳實踐 (1)


此文已由做者湯曉靜受權網易雲社區發佈。
html

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

OpenResty 發展起源

OpenResty(也稱爲 ngx_openresty)是一個全功能的 Web 應用服務器。它打包了標準的 nginx 核心,不少的經常使用的第三方模塊,以及它們的大多數依賴項。 經過揉和衆多設計良好的 nginx 模塊,OpenResty 有效地把 nginx 服務器轉變爲一個強大的 Web 應用服務器,基於它開發人員可使用 lua 編程語言對 nginx 核心以及現有的各類 nginx C 模塊進行腳本編程,構建出能夠處理一萬以上併發請求的極端高性能的 Web 應用。mysql

OpenResty 致力於將你的服務器端應用徹底運行於 nginx 服務器中,充分利用 nginx 的事件模型來進行非阻塞 I/O 通訊。不只僅是和 HTTP 客戶端間的網絡通訊是非阻塞的,與 MySQL、PostgreSQL、Memcached 以及 Redis 等衆多後端之間的網絡通訊也是非阻塞的。 由於 OpenResty 軟件包的維護者也是其中打包的許多 nginx 模塊的做者,因此 OpenResty 能夠確保所包含的全部組件能夠可靠地協同工做。nginx

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

爲何要取 OpenResty 這個名字呢?OpenResty 最先是順應 OpenAPI 的潮流作的,因此 Open 取自「開放」之意,而 Resty 即是 REST 風格的意思。雖而後來也能夠基於 ngx_openresty 實現任何形式的 Web service 或者傳統的 Web 應用。redis

也就是說 nginx 再也不是一個簡單的靜態網頁服務器,也再也不是一個簡單的反向代理了,OpenResty 致力於經過一系列 nginx 模塊,把 nginx 擴展爲全功能的 Web 應用服務器,目前有兩大應用目標:sql

  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 高性能日誌寫入方案

相關文章
相關標籤/搜索