Openresty的開發閉環初探

爲什麼值得入手?

Nginx作爲現在使用最廣泛的高性能後端服務器,Openresty爲之提供了動態預言的靈活,當性能與靈活走在了一起,無疑對於被之前陷於臃腫架構,苦於提升性能的工程師來說是重大的利好消息,本文就是在這種背景下,將初入這一未知的領域之後的一些經驗與大家分享一下,若有失言之處,歡迎指教。

安裝

現在除了能在 Download裏面下載源碼來自己編譯安裝,現在連預編譯好的都有了, 安裝也就分分鐘的事了。

hello world

/path/to/nginx.confconftent_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的感覺呢:)

開發語言Lua的大致介紹

環境搭建完畢之後,接下來就是各種試錯了,關於Lua的介紹,網上的資料比如:Openresty最佳實踐(版本比較多,這裏就不放了)。 寫的都會比較詳細,本文就不在這裏過多解釋了,只展示部分基礎的Lua的模樣。下面對lua一些個性有趣的地方做一下分享,可能不會涉及到lua語言比較全面或者細節的一些部分,作爲補充,讀者可以翻閱官方的<<Programing in Lua>>。

-- 單行註釋以兩個連字符開頭 

--[[ 
     多行註釋
--]]

-- 變量賦值

num = 13  -- 所有的數字都是雙精度浮點型。

s = '單引號字符串'
t = "也可以用雙引號" 
u = [[ 多行的字符串
       ]] 

-- 控制流程,和python最明顯的差別可能就是冒號變成了do, 最後還得數end的對應
-- while
while n < 10 do 
  n = 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') 
  num = 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 ,做了適當的裁剪來做說明。

Lua語言個性的一面

第一道牆: 打印table

作爲lua裏面唯一標準的數據結構, 直接打印居然只有一個id狀的東西,這裏說這一點沒有抱怨的意思,只是讓讀者做好倒騰的心理準備,畢竟倒騰一個簡潔語言終歸是有代價的,瞭解決定背後的原因,有時候比現成的一步到位的現成方案這也是倒騰的另一面好處吧,這裏給出社區裏面的討論

舉個例子: lua裏面一般使用#table來獲取table的長度,究其原因,lua對於未定義的變量、table的鍵,總是返回nil,而不像python裏面肯定是拋出異常, 所以#來計算table長度的時候只會遍歷到第一個值爲nil的地方,畢竟他不能一直嘗試下去,這時候就需要使用table.maxn的方式來獲取了。

Good or Bad? 自動類型轉換

如果你在python裏面去把一個字符串和數字相加,python必定以異常回應。

>>> "a" + 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

但是Lua覺得他能搞定。

> = "20" + 10
30

如果你覺得Lua選擇轉換加號操作符的操作數成數字類型去進行求值顯得不可思議的,下面這種情況下,這種轉換又貌似是可以有點用的了,print("hello" .. 123),這時你不用手動去將所有參數手工轉換成字符串類型。尚沒有定論說這項特性就是一無是處,但是這種依賴語言本身不明顯的特性的代碼筆者是不希望在項目裏面去踩雷的。

多返回值

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 加一層括號,強制只返回一個值

真個性: 模式匹配

簡潔的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的模式匹配,但是如此單純爲了減少代碼而放棄正則表達式現成的庫,自己又玩了一套,這也是沒誰了。

與c的天然親密

一言不合,就拿c寫一個庫給lua用,由此可見兩門語言是多麼哥兩好了,如果舉個例子的話就是lua5.1裏面的位操作符,luajit就是這樣提供的解決方案, Lua Bit Operations Module, 有興趣的讀者可以下載源碼看一下,完全就是用lua的c api包裝了c裏面的位操作符出來用,除了加了些限制的話(ex.位移出來的必然是32位有符)。lua除了數據結構上面的過於簡潔外,其他的控制結構、操作符這些語言特性基本該有的都有了,唯獨缺了位操作符,5.1爲什麼當時選擇了不實現位操作符呢?有知道出處或者原因的讀者歡迎留言告知。(順帶一提,lua5.2裏面有官方的bit32庫可以用,lua5.3已經補上了位操作符,另外Openresty在lua版本的選擇上是選擇停留在5.1,這點在github的Issue裏面有回答過,且沒有升級的打算)

  • 只有nilfalse爲布爾假。

  • lua中的索引習慣以1開始。

  • 沒有整型,所有數字都是浮點數。

  • 當函數的參數是單引號或者雙引號的字符串或者table定義的時候,可以省略外面的(), 所以require "cookie"並不是代表require是個關鍵字。

  • table[index] 等價於 table [index]

構建公司層面完整的Openresty生態

開發助手:成長中的resty命令

習慣了動態語言的解釋器的立即反饋,哪怕是熟悉lua的同學,初入Openresty的時候似乎又想起了編譯->執行->修改的無限循環的記憶,因爲每次都需要修改配置文件、reload、測試再如此重複個幾次才能寫對一段函數,resty命令無疑期待,筆者也希望resty命令能夠更加完善、易用。

另外提一個小遺憾,現在resty命令不能玩Openresty裏面的shared_dict共享內存, 這可能跟目前resty使用的nginx配置的模板是固定有關吧。

環境:可能不再需要重新編譯Nginx

有過Nginx維護開發經驗的同學可能都熟悉這麼一個過程,因爲多半會做業務的拆分,除了小公司外,基本都不會把一個Nginx的所有可選模塊都編譯進去,每次有新的Nginx相關的功能的增減,都免不了重新編譯,重新部署上線,Openresty是基於Nginx的,如果是新增Nginx本身的功能,重新編譯增加功能沒什麼好說的,如何優雅的更新Nginx服務進程,Nginx有提供方案、各家也有各家的服務可靠性要求,具體怎麼辦這裏就不贅述了。

發佈部署

Openresty本身的發佈部署跟Nginx本身沒有太大的不同,Openresty本身的發佈部署官方也推出了linux平臺的預編譯好的包,在這樣的基礎上構建環境就更加便捷,環境之上,首先是lua腳本和nginx配置文件的發佈,在版本管理之下,加上自動構建的發佈平臺,Openresty的應用分分鐘就可以上線了:),這個流程本身無關Openresty,但是簡而言之一句話,當重複性的東西自動化之後,我們纔有精力去解決更有趣的問題,不是麼?

第三方庫的安裝、管理

  • 以前: 自己找個第三方庫編譯之後扔到Openresty的lualib目錄,luajit是否兼容、是否lua5.1兼容都得自己來測試一遍。

  • 之前: 對於解決前一個問題,Openresty是通過給出lua裏面Luarocks的安裝使用來解決的,但是這種方式不能解決上面所說的第二個問題,所以現在這種方式已經不推薦使用了,下面貼一下官網的說明,只做內容收集、展示用, 最新的具體說明參見using luarocks

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

  • 現在: Openresty提供瞭解決這兩個問題的完整方案,自己的包管理的規範和倉庫opm

詳細的標準說明, 請移步: https://github.com/openresty/... 這裏就不多做介紹了,關於第三方庫的質量,Openresty官網上也有了專門的QA頁面,至少保證了第三方庫一些實現的下限,不像python裏面安裝某些第三方包,比如aerospike的, 裏面安裝python客戶端,每次要去網上拉個c的客戶端下來之類的稀奇古怪的玩法,期待opm未來更加完善。

關於單元測試

關於自動化測試的話,就筆者的試用經驗而言,感覺還不是特別的順手,官方提供的Test:Nginx工具已經提供簡潔強大的功能,但是如果作爲TDD開發中的測試驅動的工具而言,筆者覺得報錯信息的有效性上面可能是唯一讓人有點覺得有點捉雞的地方,尚不清楚是否是筆者用的有誤,一般Test:Nginx的報錯多半無法幫助調試代碼,還是要走調試、修改的老路子。但是Test:Nginx的真正價值筆者覺得是講實例代碼和測試完美的結合,由此養成了看每個Openresty相關的項目代碼都必先閱讀裏面的Test:Nginx的測試,當然最多最豐富的還是Openresty本身的測試。

舉個實際的例子,在使用Test:Nginx之前,之前對於Nginx的日誌輸出,一切的測試依據,對於外面的運行環境跑的測試只能通過http的請求和返回來做測試的判斷條件,這時候對於一些情況就束手無策了, 比如處理某種錯誤情況可能需要在log裏面記錄一下,這種測試就無法保證,另外也有類似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真的難道不羨慕麼?

關於調試、性能調優

這一塊筆者還沒有深入研究過,所以,這裏就不多說了,這裏就做一下相關知識的鏈接歸納,方便大家整理資料吧。

lua語言本身提供的調試就比較簡潔、加上Openresty是嵌入Nginx內部的,這就更給排查工作帶來了困難。

官方的調試頁面

官方的性能調優頁面

通過systemtap探查在線的Nginx work進程

額外的工具庫stap++

工具火焰圖Flame Graphs的介紹

Linux Kernel Performance: Flame Graphs

作者 toyld 豈安科技搬運代碼負責人 
主導各處的挖坑工作,擅長挖坑於悄然不息,負責生命不息,挖坑不止。


反爬蟲
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章