OpenResty學習指南(一)

  • 2020 年 2 月 18 日
  • 筆記

我的部落格: https://www.luozhiyun.com/archives/217

想要學好 OpenResty,你必須理解下面 8 個重點:

  • 同步非阻塞的編程模式;
  • 不同階段的作用;
  • LuaJIT 和 Lua 的不同之處;
  • OpenResty API 和周邊庫;
  • 協程和 cosocket;
  • 單元測試框架和性能測試工具;
  • 火焰圖和周邊工具鏈;
  • 性能優化。

你不應該使用任何 Lua 世界的庫來解決上述問題,而是應該使用 cosocket 的 lua-resty-* 庫。Lua 世界的庫很可能會帶來阻塞,讓原本高性能的服務,直接下降幾個數量級。

OpenResty階段

和nginx一樣,都有階段的概念,並且每個階段都有自己不同的作用:

  • set_by_lua,用於設置變數;
  • rewrite_by_lua,用於轉發、重定向等;
  • access_by_lua,用於准入、許可權等;
  • content_by_lua,用於生成返回內容;
  • header_filter_by_lua,用於應答頭過濾處理;
  • body_filter_by_lua,用於應答體過濾處理;
  • log_by_lua,用於日誌記錄。

OpenResty 的 API 是有階段使用限制的。每一個 API 都有一個與之對應的使用階段列表,如果你超範圍使用就會報錯。

具體的API可以查閱文檔:https://github.com/openresty/lua-nginx-module

跨階段的變數

有些情況下,我們需要的是跨越階段的、可以讀寫的變數。

OpenResty 提供了 ngx.ctx,來解決這類問題。它是一個 Lua table,可以用來存儲基於請求的 Lua 數據,且生存周期與當前請求相同。我們來看下官方文檔中的這個示例:

location /test {        rewrite_by_lua_block {            ngx.ctx.foo = 76        }        access_by_lua_block {            ngx.ctx.foo = ngx.ctx.foo + 3        }        content_by_lua_block {            ngx.say(ngx.ctx.foo)        }    }

最終輸出79

包管理

OPM

OPM(OpenResty Package Manager)是 OpenResty 自帶的包管理器 opm search lua-resty-http

LUAROCKS

不同於 OPM 里只包含 OpenResty 相關的包,LuaRocks 裡面還包含 Lua 世界的庫。 luarocks search lua-resty-http 我們還可以去網站上看包的詳細資訊:https://luarocks.org/modules/pintsized/lua-resty-http,這裡面包含了作者、License、GitHub 地址、下載次數、功能簡介、歷史版本、依賴等。

AWESOME-RESTY

awesome-resty 這個項目,就維護了幾乎所有 OpenResty 可用的包,並且都分門別類地整理好了。

nginx

nginx命令行

  1. 格式:nginx -s reload
  2. 幫助: -? -h
  3. 使用指定的配置文件: -c
  4. 指定配置指令:-g
  5. 指定運行目錄:-p
  6. 發送訊號:-s (stop / quit / reload / reopen)
  7. 測試配置文件是否有語法錯誤:-t -T
  8. 列印nginx的版本資訊、編譯資訊等:-v -V

nginx訊號

因為nginx是多進程的程式:

所以訊號分為Master進程訊號和worker進程訊號。

Master進程:

  • 監控worker進程: CHILD ,如果worker進程出現了故障而掛掉了,那麼master可以通過這個訊號將worker進程迅速拉起
  • 管理worker進程:
    • TERM,INT:表示立刻停止nginx進程
    • QUIT:表示優雅停止nginx進程
    • HUP:重載配置文件
    • USR1:表示重新打開日誌文件
    • USR2、WINCH:專門針對熱部署使用

worker進程:與master進程命令一一對應

  • TERM,INT:表示立刻停止nginx進程
  • QUIT:表示優雅停止nginx進程
  • USR1:表示重新打開日誌文件
  • WINCH:專門針對熱部署使用

Nginx命令行,相當於直接向master進程發送命令

  • reload:HUP
  • reopen:USR1
  • stop:TERM
  • quit:QUIT

openresty入門

  1. 創建工作目錄
mkdir geektime  cd luoluo  mkdir logs/ conf/
  1. 在conf裡面添加nginx.conf文件
events {      worker_connections 1024;  }  http {      server {          listen 8080;          location / {              content_by_lua '                  ngx.say("hello, world")              ';          }      }  }
  1. 啟動openresty服務
openresty -p `pwd` -c conf/nginx.conf  指定運行目錄:-p  使用指定的配置文件: -c

openresty後面跟隨的命令和nginx是一樣的

獨立出Lua程式碼

  1. 我們先在luo的工作目錄下,創建一個名為lua的目錄
$ mkdir lua  $ cat lua/hello.lua  ngx.say("hello, world")
  1. 修改 nginx.conf 的配置
pid logs/nginx.pid;  events {      worker_connections 1024;  }  http {      server {          listen 8080;          location / {              content_by_lua_file lua/hello.lua;              }          }      }  }

這裡把 content_by_lua_block 改為 content_by_lua_file

  1. 重啟OpenResty
$ sudo kill -HUP `cat logs/nginx.pid`

我這裡使用了發送訊號的方式 -HUP表示重載配置文件

NYI

NYI,全稱為 Not Yet Implemented。LuaJIT 中 JIT 編譯器的實現還不完善,有一些原語它還無法編譯,因為這些原語實現起來比較困難,再加上 LuaJIT 的作者目前處於半退休狀態。這些原語包括常見的 pairs() 函數、unpack() 函數、基於 Lua CFunction 實現的 Lua C 模組等。這樣一來,當 JIT 編譯器在當前程式碼路徑上遇到它不支援的操作時,便會退回到解釋器模式。這些不能編譯的函數稱為NYI。

NYI函數都在:http://wiki.luajit.org/NYI 在開發中,可以先去找OpenResty的API:https://github.com/openresty/lua-nginx-module

例如,NYI 列表中 string 庫的幾個函數:

其中,string.byte 對應的能否被編譯的狀態是 yes,表明可以被 JIT。

string.char 對應的編譯狀態是 2.1,表明從 LuaJIT 2.1 開始支援。我們知道,OpenResty 中的 LuaJIT 是基於 LuaJIT 2.1 的,所以你也可以放心使用。

string.dump 對應的編譯狀態是 never,即不會被 JIT,會退回到解釋器模式。

string.find 對應的編譯狀態是 2.1 partial,意思是從 LuaJIT 2.1 開始部分支援,後面的備註中寫的是 只支援搜索固定的字元串,不支援模式匹配。

如何檢測函數

LuaJIT 自帶的 jit.dump 和 jit.v 模組。它們都可以列印出 JIT 編譯器工作的過程。前者會輸出非常詳細的資訊,可以用來調試 LuaJIT 本身;後者的輸出比較簡單,每行對應一個 trace,通常用來檢測是否可以被 JIT。

使用resty:

$resty -j v -e 

其中,resty 的 -j 就是和 LuaJIT 相關的選項;後面的值為 dump 和 v,就對應著開啟 jit.dump 和 jit.v 模式。

如下例子:

$resty -j v -e 'local t = {}   for i=1,100 do       t[i] = i   end     for i=1, 1000 do       for j=1,1000 do           for k,v in pairs(t) do               --           end       end   end'

上面的pairs是NYI的語句,不能被JIT,所以結果裡面就會顯示:

 [TRACE   1 (command line -e):2 loop]   [TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

shdict get API

shared dict(共享字典)是基於 NGINX 共享記憶體區的 Lua 字典對象,它可以跨多個 worker 來存取數據,一般用來存放限流、限速、快取等數據。

例子:

http {        lua_shared_dict dogs 10m;        server {            location /demo {                content_by_lua_block {                    local dogs = ngx.shared.dogs           dogs:set("Jim", 8)           local v = dogs:get("Jim")                    ngx.say(v)                }            }        }    }

簡單說明一下,在 Lua 程式碼中使用 shared dict 之前,我們需要在 nginx.conf 中用 lua_shared_dict 指令增加一塊記憶體空間,它的名字是 dogs,大小為 10M。

也可以使用resty CLI:

$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs   dogs:set("Jim", 8)   local v = dogs:get("Jim")   ngx.say(v)'

共享記憶體使用階段

context: set_by_lua*,  rewrite_by_lua*,  access_by_lua*,  content_by_lua*,  header_filter_by_lua*,  body_filter_by_lua*,  log_by_lua*,  ngx.timer.*,  balancer_by_lua*,  ssl_certificate_by_lua*,  ssl_session_fetch_by_lua*,  ssl_session_store_by_lua*

可以看出, init 和 init_worker 兩個階段不在其中,也就是說,共享記憶體的 get API 不能在這兩個階段使用。

get函數返回多個值

value, flags = ngx.shared.DICT:get(key)

正常情況下: 第一個參數value 返回的是字典中 key 對應的值;但當 key 不存在或者過期時,value 的值為 nil。 第二個參數 flags 就稍微複雜一些了,如果 set 介面設置了 flags,就返回,否則不返回。

一旦 API 調用出錯,value 返回 nil,flags 返回具體的錯誤資訊。

cosocket

cosocket 是把協程和網路套接字的英文拼在一起形成的,即 cosocket = coroutine + socket。

遇到網路 I/O 時,它會交出控制權(yield),把網路事件註冊到 Nginx 監聽列表中,並把許可權交給 Nginx;當有 Nginx 事件達到觸發條件時,便喚醒對應的協程繼續處理(resume),最終實現了非阻塞網路 I/O。

API

  • 創建對象:ngx.socket.tcp。
  • 設置超時:tcpsock:settimeout 和 tcpsock:settimeouts。
  • 建立連接:tcpsock:connect。
  • 發送數據:tcpsock:send。
  • 接受數據:tcpsock:receive、tcpsock:receiveany 和 tcpsock:receiveuntil。
  • 連接池:tcpsock:setkeepalive。
  • 關閉連接:tcpsock:close。

上下文:

rewrite_by_lua*,  access_by_lua*,  content_by_lua*,  ngx.timer.*,  ssl_certificate_by_lua*,  ssl_session_fetch_by_lua*_

cosocket API 在 set_by_lua, log_by_lua, header_filter_by_lua* 和 body_filter_by_lua* 中是無法使用的。init_by_lua* 和 init_worker_by_lua* 中暫時也不能用。

與這些API相應的Nginx指令:

  • lua_socket_connect_timeout:連接超時,默認 60 秒。
  • lua_socket_send_timeout:發送超時,默認 60 秒。
  • lua_socket_send_lowat:發送閾值(low water),默認為 0。
  • lua_socket_read_timeout: 讀取超時,默認 60 秒。
  • lua_socket_buffer_size:讀取數據的快取區大小,默認 4k/8k。
  • lua_socket_pool_size:連接池大小,默認 30。
  • lua_socket_keepalive_timeout:連接池 cosocket 對象的空閑時間,默認 60 秒。
  • lua_socket_log_errors:cosocket 發生錯誤時,是否記錄日誌,默認為 on。

例子

$ resty -e 'local sock = ngx.socket.tcp()          sock:settimeout(1000)  -- one second timeout          local ok, err = sock:connect("www.baidu.com", 80)          if not ok then              ngx.say("failed to connect: ", err)              return          end            local req_data = "GET / HTTP/1.1rnHost: www.baidu.comrnrn"          local bytes, err = sock:send(req_data)          if err then              ngx.say("failed to send: ", err)              return          end            local data, err, partial = sock:receive()          if err then              ngx.say("failed to receive: ", err)              return          end            sock:close()          ngx.say("response is: ", data)'
  • 首先,通過 ngx.socket.tcp() ,創建 TCP 的 cosocket 對象,名字是 sock。
  • 然後,使用 settimeout() ,把超時時間設置為 1 秒。注意這裡的超時沒有區分 connect、receive,是統一的設置。
  • 接著,使用 connect() 去連接指定網站的 80 埠,如果失敗就直接退出。
  • 連接成功的話,就使用 send() 來發送構造好的數據,如果發送失敗就退出。
  • 發送數據成功的話,就使用 receive() 來接收網站返回的數據。這裡 receive() 的默認參數值是 l,也就是只返回第一行的數據;如果參數設置為了a,就是持續接收數據,直到連接關閉;
  • 最後,調用 close() ,主動關閉 socket 連接。

超時時間

在上面settimeout() ,作用是把連接、發送和讀取超時時間統一設置為一個值。如果要想分開設置,就需要使用 settimeouts() 函數:

sock:settimeouts(1000, 2000, 3000) 

接收數據

receive 接收指定大小:

local data, err, partial = sock:receiveany(10240)

這段程式碼就表示,最多只接收 10K 的數據。

關於 receive,還有另一個很常見的用戶需求,那就是一直獲取數據,直到遇到指定字元串才停止。

ocal reader = sock:receiveuntil("rn")     while true do       local data, err, partial = reader(4)       if not data then           if err then               ngx.say("failed to read the data stream: ", err)               break           end             ngx.say("read done")           break       end       ngx.say("read chunk: [", data, "]")   end

這段程式碼中的 receiveuntil 會返回 rn 之前的數據,並通過迭代器每次讀取其中的 4 個位元組。

連接池

沒有連接池的話,每次請求進來都要新建一個連接,就會導致 cosocket 對象被頻繁地創建和銷毀,造成不必要的性能損耗。

在你使用完一個 cosocket 後,可以調用 setkeepalive() 放到連接池中:

local ok, err = sock:setkeepalive(2 * 1000, 100)  if not ok then      ngx.say("failed to set reusable: ", err)  end

這段程式碼設置了連接的空閑時間為 2 秒,連接池的大小為 100。在調用 connect() 函數時,就會優先從連接池中獲取 cosocket 對象。

需注意:

  1. 不能把發生錯誤的連接放入連接池
  2. 第二,要搞清楚連接的數量。連接池是 worker 級別的,每個 worker 都有自己的連接池。所以,如果你有 10 個 worker,連接池大小設置為 30,那麼對於後端的服務來講,就等於有 300 個連接。

定時任務

OpenResty 的定時任務可以分為下面兩種:

  • ngx.timer.at,用來執行一次性的定時任務;
  • ngx.time.every,用來執行固定周期的定時任務。但是在啟動了一個 timer 之後,你就再也沒有機會來取消這個定時任務了

如下:

init_worker_by_lua_block {          local function handler()              local sock = ngx.socket.tcp()              local ok, err = sock:connect(「www.baidu.com", 80)          end          local ok, err = ngx.timer.at(0, handler)      }

啟動了一個延時為 0 的定時任務。它啟動了回調函數 handler,並在這個函數中,用 cosocket 去訪問一個網站