天天看點

OpenResty學習指南(一)

OpenResty學習指南(一)

我的部落格: 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是多程序的程式:

OpenResty學習指南(一)

是以信号分為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 庫的幾個函數:

OpenResty學習指南(一)

其中,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.1\r\nHost: www.baidu.com\r\n\r\n"
        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("\r\n")
 
 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 會傳回 \r\n 之前的資料,并通過疊代器每次讀取其中的 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 去通路一個網站