OpenResty學習指南(一)

個人博客: https://www.luozhiyun.com/archives/217nginx

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

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

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

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-moduleapi

跨階段的變量

有些狀況下,咱們須要的是跨越階段的、能夠讀寫的變量。緩存

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-httpsession

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:表示從新打開日誌文件
    • USR二、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.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 去訪問一個網站

相關文章
相關標籤/搜索