Openresty的開發閉環初探
1. 為什么值得入手?
Nginx 作為現在使用最廣泛的高性能后端服務器,Openresty 為之提供了動態預言的靈活,當性能與靈活走在了一起,無疑對于被之前陷于臃腫架構,苦于提升性能的工程師來說是重大的利好消息,本文就是在這種背景下,將初入這一未知的領域之后的一些經驗與大家分享一下,若有失言之處,歡迎指教。
2. 安裝
現在除了能在 [Download](http://openresty.org/en/download.html )里面下載源碼來自己編譯安裝,現在連預編譯好的[包](http://openresty.org/en/linux-packages.html)都有了, 安裝也就分分鐘的事了。
3. hello world
/path/to/nginx.conf`, `conftent_by_lua_file 里面的路徑請根據 lua_package_path 調整一下。
- ```
- 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 相關的開發的路數也就大抵如此了, 將 lua 庫發布到 lualib 之下,將對應的 nginx 的配置文件發布到 nginx/conf 底下,然后 reload 已有的 Openresty 進程(少數需要清空 Openresty shared_dict 數據的情況需要重啟 )。
如果是測試環境的話,那更是簡單了,在 http 段將 lua_code_cache 設為 off , Openresty 不會緩存 lua 腳本,每次執行都會去磁盤上讀取 lua 腳本文件的內容,發布之后就可以直接看效果了(當然如果配置文件修改了,reload 是免不了了)。是不是找到一點當初 apache 寫 php 的感覺呢:)
4. 開發語言 Lua 的大致介紹
環境搭建完畢之后,接下來就是各種試錯了。關于 Lua 的介紹,網上的資料比如:Openresty ***實踐(版本比較多,這里就不放了)寫的都會比較詳細,本文就不在這里過多解釋了,只展示部分基礎的Lua的模樣。
下面對 lua 一些個性有趣的地方做一下分享,可能不會涉及到 lua 語言比較全面或者細節的一些部分,作為補充,讀者可以翻閱官方的<
- ```lua
- -- 單行注釋以兩個連字符開頭
- --[[
- 行注釋
- --]]
- -- 變量賦值
- num = 13 -- 所有的數字都是雙精度浮點型。
- s = '單引號字符串'
- t = "也可以用雙引號"
- u = [[ 多行的字符串
- ]]
- -- 控制流程,和python最明顯的差別可能就是冒號變成了do, ***還得數end的對應
- -- while
- while n < 10 do
- nn = n + 1 -- 不支持 ++ 或 += 運算符。
- 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
- -- 循環的另一種結構:
- repeat
- print('the way of the future')
- numnum = num - 1
- until num == 0
- -- 函數定義:
- 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用作列表、數組
- v = {'value1', 'value2', 1.21, 'gigawatts'}
- for i = 1, #v do -- #v 是列表的大小
- print(v[i])
- end
- -- 元表
- f1 = {a = 1, b = 2} -- 表示一個分數 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 -- 調用在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/ ) ,做了適當的裁剪來做說明。
4.1 Lua 語言個性的一面
4.1.1 ***道墻: 打印 table
作為 lua 里面唯一標準的數據結構, 直接打印居然只有一個 id 狀的東西,這里說這一點沒有抱怨的意思,只是讓讀者做好倒騰的心理準備,畢竟倒騰一個簡潔語言終歸是有代價的。
了解決定背后的原因,有時候比現成的一步到位的現成方案這也是倒騰的另一面好處吧,這里給出社區里面的[討論](http://luausers.org/wiki/TableSerialization)
舉個例子:
lua 里面一般使用 #table 來獲取 table 的長度,究其原因,lua 對于未定義的變量、table 的鍵,總是返回 nil,而不像 python 里面肯定是拋出異常, 所以 # 來計算 table 長度的時候只會遍歷到***個值為 nil 的地方,畢竟他不能一直嘗試下去,這時候就需要使用 table.maxn 的方式來獲取了。
4.1.2 Good or Bad ? 自動類型轉換
如果你在 python 里面去把一個字符串和數字相加,python 必定以異常回應。
- ```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 選擇轉換加號操作符的操作數成數字類型去進行求值顯得不可思議的,下面這種情況下,這種轉換又貌似是可以有點用的了 print("hello" .. 123) ,這時你不用手動去將所有參數手工轉換成字符串類型。
尚沒有定論說這項特性就是一無是處,但是這種依賴語言本身不明顯的特性的代碼筆者是不希望在項目里面去踩雷的。
4.1.3 多返回值
Lua 開始變得越來越與眾不同了:允許函數返回多個結果。
- ```
- function foo0() end --無返回值
- function foo1() return 'a' end -- 返回一個結果
- function foo2() return 'a','b' end -- 返回兩個結果
- -- 多重賦值時, 函數調用是***一個表達式時
- -- 保留盡可能多的返回值
- x, y = foo2() -- x='a', y='b'
- x = foo2() -- x='a', 'b'被丟棄
- x,y,z = 10,foo2() -- x=10, y='a', z='b'
- -- 如果多重賦值時,函數調用不是***一個表達式時
- -- 只產生一個值
- x, y = foo2(),20 -- x='a', y=20
- x,y = foo0(), 20, 30 -- x=nil, y= 20,30被丟棄,這種情況當函數沒有返回值時,會用nil來補充。
- x,y,z = foo2() -- x='a', y='b', z=nil, 這種情況函數沒有足夠的返回值時也會用nil來補充。
- -- 同樣在函數調用、table聲明中 函數調用作為***的表達式,都會竟可能多的填充返回值,如果不是***,則只返回一個
- print(foo2(), 1) --> a 1
- print(1, foo2()) --> 1 a b
- t = {foo2(), 1} --> {'a', 1}
- t = {1, foo2()} --> {1, 'a', 'b'}
- -- 阻止這種參數順序搞事:
- print(1, (foo2())) -- 1 a 加一層括號,強制只返回一個值
- ```
4.1.4 真個性: 模式匹配
簡潔的 Lua 容不下行數比自己實現語言行數還多的正則表達式實現(無論是 POSIX ,還是 Perl 正則表達式),于是乎有了獨樹一幟的模式與匹配,下面只用模式匹配來做 URL 解碼、編碼功能實現的演示。
- ```
- -- 解碼
- 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
- ```
模式匹配實現的功能是足夠強大,但是工程上是否值得投入還值得商榷,沒有通用性,只此 lua 一家用。
雖然正則表達式也是不好調試,但是至少知道了解的人多,可能到***筆者也不會多深入 lua 的模式匹配,但是如此單純為了減少代碼而放棄正則表達式現成的庫,自己又玩了一套,這也是沒誰了。
4.1.5 與 c 的天然親密
一言不合,就拿 c 寫一個庫給 lua 用,由此可見兩門語言是多么哥倆好了。如果舉個例子的話就是 lua5.1 里面的位操作符,luajit 就是這樣提供的解決方案 [Lua Bit Operations Module](http://bitop.luajit.org ),有興趣的讀者可以下載源碼看一下,完全就是用 lua 的 c api 包裝了 c 里面的位操作符出來用。
除了加了些限制的話(ex. 位移出來的必然是 32 位有符)。lua 除了數據結構上面的過于簡潔外,其他的控制結構、操作符這些語言特性基本該有的都有了,唯獨缺了位操作符。
5.1 為什么當時選擇了不實現位操作符呢?有知道出處或者原因的讀者歡迎留言告知。(順帶一提,lua5.2 里面有官方的 bit32 庫可以用,lua5.3 已經補上了位操作符,另外 Openresty 在 lua 版本的選擇上是選擇停留在 5.1,這點在 github 的 Issue 里面有回答過,且沒有升級的打算)。
4.1.6 雜
- 只有 nil 、false 為布爾假。
- lua 中的索引習慣以 1 開始。
- 沒有整型,所有數字都是浮點數。
- 當函數的參數是單引號或者雙引號的字符串或者 table 定義的時候,可以省略外面的 () , 所以 require "cookie" 并不是代表 require 是個關鍵字。
- table[index] 等價于 table [index] 。
5. 構建公司層面完整的 Openresty 生態
5.1 開發助手:成長中的 resty 命令
習慣了動態語言的解釋器的立即反饋,哪怕是熟悉 lua 的同學,初入 Openresty 的時候似乎又想起了編譯->執行->修改的***循環的記憶,因為每次都需要修改配置文件、reload 、測試再如此重復個幾次才能寫對一段函數,resty 命令無疑期待,筆者也希望 resty 命令能夠更加完善、易用。
另外提一個小遺憾,現在 resty 命令不能玩 Openresty 里面的 shared_dict 共享內存, 這可能跟目前 resty 使用的 nginx 配置的模板是固定有關吧。
5.2 環境:可能不再需要重新編譯 Nginx
有過 Nginx 維護開發經驗的同學可能都熟悉這么一個過程,因為多半會做業務的拆分,除了小公司外,基本都不會把一個 Nginx 的所有可選模塊都編譯進去,每次有新的 Nginx 相關的功能的增減,都免不了重新編譯,重新部署上線。
Openresty 是基于 Nginx 的,如果是新增 Nginx 本身的功能,重新編譯增加功能沒什么好說的,如何優雅的更新 Nginx 服務進程,Nginx 有提供方案、各家也有各家的服務可靠性要求,具體怎么辦這里就不贅述了。
5.3 發布部署
Openresty 本身的發布部署跟 Nginx 本身沒有太大的不同,Openresty 本身的發布部署官方也推出了 linux 平臺的預編譯好的包,在這樣的基礎上構建環境就更加便捷。
環境之上,首先是 lua 腳本和 nginx 配置文件的發布,在版本管理之下,加上自動構建的發布平臺,Openresty 的應用分分鐘就可以上線了:)
這個流程本身無關 Openresty ,但是簡而言之一句話,當重復性的東西自動化之后,我們才有精力去解決更有趣的問題,不是么?
5.4 第三方庫的安裝、管理
(1)以前:自己找個第三方庫編譯之后扔到 Openresty 的 lualib 目錄,luajit 是否兼容、是否 lua5.1 兼容都得自己來測試一遍。
(2)之前:對于解決前一個問題,Openresty 是通過給出 lua 里面 Luarocks 的安裝使用來解決的,但是這種方式不能解決上面所說的第二個問題,所以現在這種方式已經不推薦使用了,下面貼一下官網的說明,只做內容收集、展示用, ***的具體說明參見[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)現在:Openresty 提供了解決這兩個問題的完整方案,自己的包管理的規范和倉庫[opm](https://opm.openresty.org/)。
詳細的標準說明, 請移步:https://github.com/openresty/opm#readme, 這里就不多做介紹了。
關于第三方庫的質量,Openresty 官網上也有了專門的[QA頁面](http://qa.openresty.org/),至少保證了第三方庫一些實現的下限,不像 python 里面安裝某些第三方包,比如 aerospike 的,里面安裝 python 客戶端,每次要去網上拉個 c 的客戶端下來之類的稀奇古怪的玩法,期待 opm 未來更加完善。
5.5 關于單元測試
關于自動化測試的話,就筆者的試用經驗而言,感覺還不是特別的順手,官方提供的 Test:Nginx 工具已經提供簡潔強大的功能,但是如果作為 TDD 開發中的測試驅動的工具而言,筆者覺得報錯信息的有效性上面可能是唯一讓人有點覺得有點捉雞的地方,尚不清楚是否是筆者用的有誤——
一般 Test:Nginx 的報錯多半無法幫助調試代碼,還是要走調試、修改的老路子。但是 Test:Nginx 的真正價值筆者覺得是講實例代碼和測試***的結合,由此養成了看每個 Openresty 相關的項目代碼都必先閱讀里面的 Test:Nginx 的測試,當然最多最豐富的還是 Openresty 本身的測試。
舉個實際的例子:
在使用 Test:Nginx 之前,之前對于 Nginx 的日志輸出,一切的測試依據,對于外面的運行環境跑的測試只能通過 http 的請求和返回來做測試的判斷條件,這時候對于一些情況就束手無策了, 比如處理某種錯誤情況可能需要在 log 里面記錄一下,這種測試就無法保證。
另外也有類似[lua-resty-test](https://github.com/membphis/lua-resty-test)這樣通過提供組件的方式來進行,但是我們一旦接觸的了 Test:Nginx 的測試方法之后,這些就顯得相形見絀了,我們舉個實際的例子。
- ```
- # 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__以上的部分編排測試如何運行,
- __DATA__作為分隔符,
- __DATA__以下的是各個測試的說明部分。
測試部分如果具體細分的話,
- 一般由 ====TEST 1: name 開始到下一個測試的聲明;
- 然后是配置 nginx 配置的 http_config、config、... 的部分;
- 接著是模擬請求的部分,基本就是 http 請求報文的設定,功能不限于這里的 request 部分;
- ***是輸出部分,這時候不僅是 http 報文的 body 部分之類的 http 響應、還有 nginx 的日志的輸出這樣的測試條件。
對于這樣清晰可讀、還能順帶把使用例子寫的清楚的單元測試的框架, pythoner 真的難道不羨慕么?
5.6 關于調試、性能調優
這一塊筆者還沒有深入研究過,所以這里就不多說了,這里就做一下相關知識的鏈接歸納,方便大家整理資料吧。lua 語言本身提供的調試就比較簡潔、加上 Openresty 是嵌入 Nginx 內部的,這就更給排查工作帶來了困難。
(1)[官方的調試頁面]
(http://openresty.org/en/debugging.html )
(2)[官方的性能調優頁面]
(http://openresty.org/en/profiling.html )
(3)[通過systemtap探查在線的Nginx work進程]
(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專欄機構“豈安科技”的原創文章,轉載請通過微信公眾號(bigsec)聯系原作者】