此文已由作者湯曉靜授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
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 應用伺服器,目前有兩大應用目標:
通用目的的 Web 應用伺服器。在這個目標下,現有的 Web 應用技術都可以算是和 OpenResty 或多或少有些類似,比如 Nodejs,PHP 等等,但 OpenResty 的效能更加出色。
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 有著如下的特性:
變數名沒有型別,值才有型別,變數名在執行時可與任何型別的值繫結;
語言只提供唯一一種資料結構,稱為表(table),它混合了陣列、雜湊,可以用任何型別的值作為 key 和 value。提供了一致且富有表達力的表構造語法,使得 lua 很適合描述複雜的資料;
函式是一等型別,支援匿名函式和正則尾遞迴(proper tail recursion);
支援詞法定界(lexical scoping)和閉包(closure);
提供 thread 型別和結構化的協程(coroutine)機制,在此基礎上可方便實現協作式多工;
執行期能編譯字串形式的程式文字並載入虛擬機器執行;
通過元表(metatable)和元方法(metamethod)提供動態元機制(dynamic meta-mechanism),從而允許程式執行時根據需要改變或擴充語法設施的內定語義;
能方便地利用表和動態元機制實現基於原型(prototype-based)的物件導向模型;
從 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 請求處理流程
每個 worker 程式都分階段處理 http 請求,簡單概括為初始化請求 -> 處理請求行 -> 後端互動 -> 響應頭處理 -> 響應包體處理 -> 列印日誌等幾個階段。其中處理響應體階段又可以掛載多個不同的 filter。具體的請求階段可以參見 nginx Phase, nginx 請求處理流程如下圖:
nginx 事件機制
nginx 的事件驅動機制是對 epoll 驅動的封裝,但其本質還是 epoll + callback 方式:
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 高效能日誌寫入方案