Openresty的開發閉環初探

圖片描述

爲何值得入手?

Nginx做爲如今使用最普遍的高性能後端服務器,Openresty爲之提供了動態預言的靈活,當性能與靈活走在了一塊兒,無疑對於被以前陷於臃腫架構,苦於提高性能的工程師來講是重大的利好消息,本文就是在這種背景下,將初入這一未知的領域以後的一些經驗與你們分享一下,如有失言之處,歡迎指教。php

安裝

如今除了能在 Download裏面下載源碼來本身編譯安裝,如今連預編譯好的都有了, 安裝也就分分鐘的事了。html

hello world

/path/to/nginx.conf, conftent_by_lua_file裏面的路徑請根據lua_package_path調整一下。python

location / {
    content_by_lua_file ../luablib/hello_world.lua;
}

/path/to/openresty/lualib/hello_world.lualinux

ngx.say("Hello World")

訪問一下, Hello World~.nginx

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的感受呢:)git

開發語言Lua的大體介紹

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

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

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

-- 變量賦值

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狀的東西,這裏說這一點沒有抱怨的意思,只是讓讀者作好倒騰的心理準備,畢竟倒騰一個簡潔語言終歸是有代價的,瞭解決定背後的緣由,有時候比現成的一步到位的現成方案這也是倒騰的另外一面好處吧,這裏給出社區裏面的討論apache

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

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 豈安科技搬運代碼負責人 主導各處的挖坑工做,擅長挖坑於悄然不息,負責生命不息,挖坑不止。

相關文章
相關標籤/搜索