Openresty的開發(fā)閉環(huán)初探
1. 為什么值得入手?
Nginx 作為現(xiàn)在使用最廣泛的高性能后端服務(wù)器,Openresty 為之提供了動態(tài)預(yù)言的靈活,當(dāng)性能與靈活走在了一起,無疑對于被之前陷于臃腫架構(gòu),苦于提升性能的工程師來說是重大的利好消息,本文就是在這種背景下,將初入這一未知的領(lǐng)域之后的一些經(jīng)驗(yàn)與大家分享一下,若有失言之處,歡迎指教。
2. 安裝
現(xiàn)在除了能在 [Download](http://openresty.org/en/download.html )里面下載源碼來自己編譯安裝,現(xiàn)在連預(yù)編譯好的[包](http://openresty.org/en/linux-packages.html)都有了, 安裝也就分分鐘的事了。
3. hello world
/path/to/nginx.conf`, `conftent_by_lua_file 里面的路徑請根據(jù) lua_package_path 調(diào)整一下。
- ```
- location / {
- content_by_lua_file ../luablib/hello_world.lua;
- }
- ```
- /path/to/openresty/lualib/hello_world.lua`
- ```
- ngx.say("Hello World")
- ```
訪問一下, Hello World~.
- ```
- HTTP/1.1 200 OK
- Connection: keep-alive
- Content-Type: application/octet-stream
- Date: Wed, 11 Jan 2017 07:52:15 GMT
- Server: openresty/1.11.2.2
- Transfer-Encoding: chunked
- Hello World
- ```
基本上早期的 Openresty 相關(guān)的開發(fā)的路數(shù)也就大抵如此了, 將 lua 庫發(fā)布到 lualib 之下,將對應(yīng)的 nginx 的配置文件發(fā)布到 nginx/conf 底下,然后 reload 已有的 Openresty 進(jìn)程(少數(shù)需要清空 Openresty shared_dict 數(shù)據(jù)的情況需要重啟 )。
如果是測試環(huán)境的話,那更是簡單了,在 http 段將 lua_code_cache 設(shè)為 off , Openresty 不會緩存 lua 腳本,每次執(zhí)行都會去磁盤上讀取 lua 腳本文件的內(nèi)容,發(fā)布之后就可以直接看效果了(當(dāng)然如果配置文件修改了,reload 是免不了了)。是不是找到一點(diǎn)當(dāng)初 apache 寫 php 的感覺呢:)
4. 開發(fā)語言 Lua 的大致介紹
環(huán)境搭建完畢之后,接下來就是各種試錯了。關(guān)于 Lua 的介紹,網(wǎng)上的資料比如:Openresty ***實(shí)踐(版本比較多,這里就不放了)寫的都會比較詳細(xì),本文就不在這里過多解釋了,只展示部分基礎(chǔ)的Lua的模樣。
下面對 lua 一些個性有趣的地方做一下分享,可能不會涉及到 lua 語言比較全面或者細(xì)節(jié)的一些部分,作為補(bǔ)充,讀者可以翻閱官方的<
- ```lua
- -- 單行注釋以兩個連字符開頭
- --[[
- 行注釋
- --]]
- -- 變量賦值
- num = 13 -- 所有的數(shù)字都是雙精度浮點(diǎn)型。
- s = '單引號字符串'
- t = "也可以用雙引號"
- u = [[ 多行的字符串
- ]]
- -- 控制流程,和python最明顯的差別可能就是冒號變成了do, ***還得數(shù)end的對應(yīng)
- -- while
- while n < 10 do
- nn = n + 1 -- 不支持 ++ 或 += 運(yùn)算符。
- end
- -- for
- for i = 0, 9 do
- print(i)
- end
- -- if語句:
- f n == 0 then
- print("no hits")
- elseif n == 1 then
- print("one hit")
- else
- print(n .. " hits")
- end
- --只有nil和false為假; 0和 ''均為真!
- if not aBoolValue then print('false') end
- -- 循環(huán)的另一種結(jié)構(gòu):
- repeat
- print('the way of the future')
- numnum = num - 1
- until num == 0
- -- 函數(shù)定義:
- function add(x, y)
- return x + y
- end
- -- table 用作鍵值對
- t = {key1 = 'value1', key2 = false}
- print(t.key1) -- 打印 'value1'.
- -- 使用任何非nil的值作為key:
- u = {['@!#'] = 'qbert', [{}] = 1729, [6.28] = 'tau'}
- print(u[6.28]) -- 打印 "tau"
- -- table用作列表、數(shù)組
- v = {'value1', 'value2', 1.21, 'gigawatts'}
- for i = 1, #v do -- #v 是列表的大小
- print(v[i])
- end
- -- 元表
- f1 = {a = 1, b = 2} -- 表示一個分?jǐn)?shù) a/b.
- f2 = {a = 2, b = 3}
- -- 這會失敗:
- -- s = f1 + f2
- metafraction = {}
- function metafraction.__add(f1, f2)
- local sum = {}
- sum.b = f1.b * f2.b
- sum.a = f1.a * f2.b + f2.a * f1.b
- return sum
- end
- setmetatable(f1, metafraction)
- setmetatable(f2, metafraction)
- s = f1 + f2 -- 調(diào)用在f1的元表上的__add(f1, f2) 方法
- -- __index、__add等的值,被稱為元方法。
- -- 這里是一個table元方法的清單:
- -- __add(a, b) for a + b
- -- __sub(a, b) for a - b
- -- __mul(a, b) for a * b
- -- __div(a, b) for a / b
- -- __mod(a, b) for a % b
- -- __pow(a, b) for a ^ b
- -- __unm(a) for -a
- -- __concat(a, b) for a .. b
- -- __len(a) for #a
- -- __eq(a, b) for a == b
- -- __lt(a, b) for a < b
- -- __le(a, b) for a <= b
- -- __index(a, b) <fn or a table> for a.b
- -- __newindex(a, b, c) for a.b = c
- -- __call(a, ...) for a(...)
- ```
以上參考了
[learn lua in y minute](https://learnxinyminutes.com/docs/zh-cn/lua-cn/ ) ,做了適當(dāng)?shù)牟眉魜碜稣f明。
4.1 Lua 語言個性的一面
4.1.1 ***道墻: 打印 table
作為 lua 里面唯一標(biāo)準(zhǔn)的數(shù)據(jù)結(jié)構(gòu), 直接打印居然只有一個 id 狀的東西,這里說這一點(diǎn)沒有抱怨的意思,只是讓讀者做好倒騰的心理準(zhǔn)備,畢竟倒騰一個簡潔語言終歸是有代價的。
了解決定背后的原因,有時候比現(xiàn)成的一步到位的現(xiàn)成方案這也是倒騰的另一面好處吧,這里給出社區(qū)里面的[討論](http://luausers.org/wiki/TableSerialization)
舉個例子:
lua 里面一般使用 #table 來獲取 table 的長度,究其原因,lua 對于未定義的變量、table 的鍵,總是返回 nil,而不像 python 里面肯定是拋出異常, 所以 # 來計算 table 長度的時候只會遍歷到***個值為 nil 的地方,畢竟他不能一直嘗試下去,這時候就需要使用 table.maxn 的方式來獲取了。
4.1.2 Good or Bad ? 自動類型轉(zhuǎn)換
如果你在 python 里面去把一個字符串和數(shù)字相加,python 必定以異?;貞?yīng)。
- ```python
- >>> "a" + 1
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: cannot concatenate 'str' and 'int' objects
- ```
但是 Lua 覺得他能搞定。
- ```lua
- > = "20" + 10
- 30
- ```
如果你覺得 Lua 選擇轉(zhuǎn)換加號操作符的操作數(shù)成數(shù)字類型去進(jìn)行求值顯得不可思議的,下面這種情況下,這種轉(zhuǎn)換又貌似是可以有點(diǎn)用的了 print("hello" .. 123) ,這時你不用手動去將所有參數(shù)手工轉(zhuǎn)換成字符串類型。
尚沒有定論說這項(xiàng)特性就是一無是處,但是這種依賴語言本身不明顯的特性的代碼筆者是不希望在項(xiàng)目里面去踩雷的。
4.1.3 多返回值
Lua 開始變得越來越與眾不同了:允許函數(shù)返回多個結(jié)果。
- ```
- function foo0() end --無返回值
- function foo1() return 'a' end -- 返回一個結(jié)果
- function foo2() return 'a','b' end -- 返回兩個結(jié)果
- -- 多重賦值時, 函數(shù)調(diào)用是***一個表達(dá)式時
- -- 保留盡可能多的返回值
- x, y = foo2() -- x='a', y='b'
- x = foo2() -- x='a', 'b'被丟棄
- x,y,z = 10,foo2() -- x=10, y='a', z='b'
- -- 如果多重賦值時,函數(shù)調(diào)用不是***一個表達(dá)式時
- -- 只產(chǎn)生一個值
- x, y = foo2(),20 -- x='a', y=20
- x,y = foo0(), 20, 30 -- x=nil, y= 20,30被丟棄,這種情況當(dāng)函數(shù)沒有返回值時,會用nil來補(bǔ)充。
- x,y,z = foo2() -- x='a', y='b', z=nil, 這種情況函數(shù)沒有足夠的返回值時也會用nil來補(bǔ)充。
- -- 同樣在函數(shù)調(diào)用、table聲明中 函數(shù)調(diào)用作為***的表達(dá)式,都會竟可能多的填充返回值,如果不是***,則只返回一個
- print(foo2(), 1) --> a 1
- print(1, foo2()) --> 1 a b
- t = {foo2(), 1} --> {'a', 1}
- t = {1, foo2()} --> {1, 'a', 'b'}
- -- 阻止這種參數(shù)順序搞事:
- print(1, (foo2())) -- 1 a 加一層括號,強(qiáng)制只返回一個值
- ```
4.1.4 真?zhèn)€性: 模式匹配
簡潔的 Lua 容不下行數(shù)比自己實(shí)現(xiàn)語言行數(shù)還多的正則表達(dá)式實(shí)現(xiàn)(無論是 POSIX ,還是 Perl 正則表達(dá)式),于是乎有了獨(dú)樹一幟的模式與匹配,下面只用模式匹配來做 URL 解碼、編碼功能實(shí)現(xiàn)的演示。
- ```
- -- 解碼
- function unescape(s)
- s = string.gsub(s, "+", " ")
- s = string.gsub(s, "%%(%x%x)", function (h)
- return string.char(tonumber(h, 16))
- end)
- return s
- end
- print(unescape("a%2Bb+%3D+c")) ---> a+b =c
- cgi = {}
- function decode(s)
- for name,value in string.gmatch(s, "([^&=]+)=([^&=]+)") do
- name = unescape(name)
- value = unescape(value)
- cgi[name] = value
- end
- end
- -- 編碼
- function escape(s)
- s = string.gsub(s, "[&=+%%%c]", function(c)
- return string.format("%%%02X", string.byte(c))
- end)
- s = string.gsub(s, " ", "+")
- return s
- end
- function encode(t)
- local b = {}
- for k,v in pairs(t) do
- b[#b+1] = (escape(k) .. "=" .. escape(v))
- end
- return table.concat(b,'&')
- end
- ```
模式匹配實(shí)現(xiàn)的功能是足夠強(qiáng)大,但是工程上是否值得投入還值得商榷,沒有通用性,只此 lua 一家用。
雖然正則表達(dá)式也是不好調(diào)試,但是至少知道了解的人多,可能到***筆者也不會多深入 lua 的模式匹配,但是如此單純?yōu)榱藴p少代碼而放棄正則表達(dá)式現(xiàn)成的庫,自己又玩了一套,這也是沒誰了。
4.1.5 與 c 的天然親密
一言不合,就拿 c 寫一個庫給 lua 用,由此可見兩門語言是多么哥倆好了。如果舉個例子的話就是 lua5.1 里面的位操作符,luajit 就是這樣提供的解決方案 [Lua Bit Operations Module](http://bitop.luajit.org ),有興趣的讀者可以下載源碼看一下,完全就是用 lua 的 c api 包裝了 c 里面的位操作符出來用。
除了加了些限制的話(ex. 位移出來的必然是 32 位有符)。lua 除了數(shù)據(jù)結(jié)構(gòu)上面的過于簡潔外,其他的控制結(jié)構(gòu)、操作符這些語言特性基本該有的都有了,唯獨(dú)缺了位操作符。
5.1 為什么當(dāng)時選擇了不實(shí)現(xiàn)位操作符呢?有知道出處或者原因的讀者歡迎留言告知。(順帶一提,lua5.2 里面有官方的 bit32 庫可以用,lua5.3 已經(jīng)補(bǔ)上了位操作符,另外 Openresty 在 lua 版本的選擇上是選擇停留在 5.1,這點(diǎn)在 github 的 Issue 里面有回答過,且沒有升級的打算)。
4.1.6 雜
- 只有 nil 、false 為布爾假。
- lua 中的索引習(xí)慣以 1 開始。
- 沒有整型,所有數(shù)字都是浮點(diǎn)數(shù)。
- 當(dāng)函數(shù)的參數(shù)是單引號或者雙引號的字符串或者 table 定義的時候,可以省略外面的 () , 所以 require "cookie" 并不是代表 require 是個關(guān)鍵字。
- table[index] 等價于 table [index] 。
5. 構(gòu)建公司層面完整的 Openresty 生態(tài)
5.1 開發(fā)助手:成長中的 resty 命令
習(xí)慣了動態(tài)語言的解釋器的立即反饋,哪怕是熟悉 lua 的同學(xué),初入 Openresty 的時候似乎又想起了編譯->執(zhí)行->修改的***循環(huán)的記憶,因?yàn)槊看味夹枰薷呐渲梦募eload 、測試再如此重復(fù)個幾次才能寫對一段函數(shù),resty 命令無疑期待,筆者也希望 resty 命令能夠更加完善、易用。
另外提一個小遺憾,現(xiàn)在 resty 命令不能玩 Openresty 里面的 shared_dict 共享內(nèi)存, 這可能跟目前 resty 使用的 nginx 配置的模板是固定有關(guān)吧。
5.2 環(huán)境:可能不再需要重新編譯 Nginx
有過 Nginx 維護(hù)開發(fā)經(jīng)驗(yàn)的同學(xué)可能都熟悉這么一個過程,因?yàn)槎喟霑鰳I(yè)務(wù)的拆分,除了小公司外,基本都不會把一個 Nginx 的所有可選模塊都編譯進(jìn)去,每次有新的 Nginx 相關(guān)的功能的增減,都免不了重新編譯,重新部署上線。
Openresty 是基于 Nginx 的,如果是新增 Nginx 本身的功能,重新編譯增加功能沒什么好說的,如何優(yōu)雅的更新 Nginx 服務(wù)進(jìn)程,Nginx 有提供方案、各家也有各家的服務(wù)可靠性要求,具體怎么辦這里就不贅述了。
5.3 發(fā)布部署
Openresty 本身的發(fā)布部署跟 Nginx 本身沒有太大的不同,Openresty 本身的發(fā)布部署官方也推出了 linux 平臺的預(yù)編譯好的包,在這樣的基礎(chǔ)上構(gòu)建環(huán)境就更加便捷。
環(huán)境之上,首先是 lua 腳本和 nginx 配置文件的發(fā)布,在版本管理之下,加上自動構(gòu)建的發(fā)布平臺,Openresty 的應(yīng)用分分鐘就可以上線了:)
這個流程本身無關(guān) Openresty ,但是簡而言之一句話,當(dāng)重復(fù)性的東西自動化之后,我們才有精力去解決更有趣的問題,不是么?
5.4 第三方庫的安裝、管理
(1)以前:自己找個第三方庫編譯之后扔到 Openresty 的 lualib 目錄,luajit 是否兼容、是否 lua5.1 兼容都得自己來測試一遍。
(2)之前:對于解決前一個問題,Openresty 是通過給出 lua 里面 Luarocks 的安裝使用來解決的,但是這種方式不能解決上面所說的第二個問題,所以現(xiàn)在這種方式已經(jīng)不推薦使用了,下面貼一下官網(wǎng)的說明,只做內(nèi)容收集、展示用, ***的具體說明參見[using luarocks](http://openresty.org/en/using-luarocks.html)。
- ```
- wget http://luarocks.org/releases/luarocks-2.0.13.tar.gz
- tar -xzvf luarocks-2.0.13.tar.gz
- cd luarocks-2.0.13/
- ./configure --prefix=/usr/local/openresty/luajit \
- --with-lua=/usr/local/openresty/luajit/ \
- --lua-suffix=jit \
- --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1
- make
- sudo make install
- ```
安裝第三方庫示例:
- sudo /usr/local/openresty/luajit/luarocks install md5。
(3)現(xiàn)在:Openresty 提供了解決這兩個問題的完整方案,自己的包管理的規(guī)范和倉庫[opm](https://opm.openresty.org/)。
詳細(xì)的標(biāo)準(zhǔn)說明, 請移步:https://github.com/openresty/opm#readme, 這里就不多做介紹了。
關(guān)于第三方庫的質(zhì)量,Openresty 官網(wǎng)上也有了專門的[QA頁面](http://qa.openresty.org/),至少保證了第三方庫一些實(shí)現(xiàn)的下限,不像 python 里面安裝某些第三方包,比如 aerospike 的,里面安裝 python 客戶端,每次要去網(wǎng)上拉個 c 的客戶端下來之類的稀奇古怪的玩法,期待 opm 未來更加完善。
5.5 關(guān)于單元測試
關(guān)于自動化測試的話,就筆者的試用經(jīng)驗(yàn)而言,感覺還不是特別的順手,官方提供的 Test:Nginx 工具已經(jīng)提供簡潔強(qiáng)大的功能,但是如果作為 TDD 開發(fā)中的測試驅(qū)動的工具而言,筆者覺得報錯信息的有效性上面可能是唯一讓人有點(diǎn)覺得有點(diǎn)捉雞的地方,尚不清楚是否是筆者用的有誤——
一般 Test:Nginx 的報錯多半無法幫助調(diào)試代碼,還是要走調(diào)試、修改的老路子。但是 Test:Nginx 的真正價值筆者覺得是講實(shí)例代碼和測試***的結(jié)合,由此養(yǎng)成了看每個 Openresty 相關(guān)的項(xiàng)目代碼都必先閱讀里面的 Test:Nginx 的測試,當(dāng)然最多最豐富的還是 Openresty 本身的測試。
舉個實(shí)際的例子:
在使用 Test:Nginx 之前,之前對于 Nginx 的日志輸出,一切的測試依據(jù),對于外面的運(yùn)行環(huán)境跑的測試只能通過 http 的請求和返回來做測試的判斷條件,這時候?qū)τ谝恍┣闆r就束手無策了, 比如處理某種錯誤情況可能需要在 log 里面記錄一下,這種測試就無法保證。
另外也有類似[lua-resty-test](https://github.com/membphis/lua-resty-test)這樣通過提供組件的方式來進(jìn)行,但是我們一旦接觸的了 Test:Nginx 的測試方法之后,這些就顯得相形見絀了,我們舉個實(shí)際的例子。
- ```
- # vim:set ft= ts=4 sw=4 et fdm=marker:
- use Test::Nginx::Socket::Lua;
- #worker_connections(1014);
- #master_process_enabled(1);
- #log_level('warn');
- #repeat_each(2);
- plan tests => repeat_each() * (blocks() * 3 + 0);
- #no_diff();
- no_long_string();
- #master_on();
- #workers(2);
- run_tests();
- __DATA__
- === TEST 1: lpush & lpop
- --- http_config
- lua_shared_dict dogs 1m;
- --- config
- location = /test {
- content_by_lua_block {
- local dogs = ngx.shared.dogs
- local len, err = dogs:lpush("foo", "bar")
- if len then
- ngx.say("push success")
- else
- ngx.say("push err: ", err)
- end
- local val, err = dogs:llen("foo")
- ngx.say(val, " ", err)
- local val, err = dogs:lpop("foo")
- ngx.say(val, " ", err)
- local val, err = dogs:llen("foo")
- ngx.say(val, " ", err)
- local val, err = dogs:lpop("foo")
- ngx.say(val, " ", err)
- }
- }
- --- request
- GET /test
- --- response_body
- push success
- 1 nil
- bar nil
- 0 nil
- nil nil
- --- no_error_log
- [error]
- ```
以上是隨便選取的lua-nginx-module的測試文件145-shdict-list.t中的一段做說明,測試文件分為3個部分:
- __DATA__以上的部分編排測試如何運(yùn)行,
- __DATA__作為分隔符,
- __DATA__以下的是各個測試的說明部分。
測試部分如果具體細(xì)分的話,
- 一般由 ====TEST 1: name 開始到下一個測試的聲明;
- 然后是配置 nginx 配置的 http_config、config、... 的部分;
- 接著是模擬請求的部分,基本就是 http 請求報文的設(shè)定,功能不限于這里的 request 部分;
- ***是輸出部分,這時候不僅是 http 報文的 body 部分之類的 http 響應(yīng)、還有 nginx 的日志的輸出這樣的測試條件。
對于這樣清晰可讀、還能順帶把使用例子寫的清楚的單元測試的框架, pythoner 真的難道不羨慕么?
5.6 關(guān)于調(diào)試、性能調(diào)優(yōu)
這一塊筆者還沒有深入研究過,所以這里就不多說了,這里就做一下相關(guān)知識的鏈接歸納,方便大家整理資料吧。lua 語言本身提供的調(diào)試就比較簡潔、加上 Openresty 是嵌入 Nginx 內(nèi)部的,這就更給排查工作帶來了困難。
(1)[官方的調(diào)試頁面]
(http://openresty.org/en/debugging.html )
(2)[官方的性能調(diào)優(yōu)頁面]
(http://openresty.org/en/profiling.html )
(3)[通過systemtap探查在線的Nginx work進(jìn)程]
(https://github.com/agentzh/nginx-systemtap-toolkit)
(4)[額外的工具庫stap++]
(https://github.com/agentzh/stapxx )
(5)[工具火焰圖Flame Graphs的介紹]
(http://dtrace.org/blogs/brendan/2011/12/16/flame-graphs/ )
(6)[Linux Kernel Performance: Flame Graphs]
(http://dtrace.org/blogs/brendan/2012/03/17/linux-kernel-performance-flame-graphs/ )
【本文是51CTO專欄機(jī)構(gòu)“豈安科技”的原創(chuàng)文章,轉(zhuǎn)載請通過微信公眾號(bigsec)聯(lián)系原作者】