OpenResty學習筆記

2020-08-11 22:15:24

簡介

OpenResty是一個基於Nginx+Lua的Web執行環境,它打包了標準的 Nginx 核心,很多的常用的第三方模組,以及它們的大多數依賴項。OpenResty可以用來實現高併發的動態Web應用

Open 取自「開放」之意,而Resty便是 REST 風格的意思

OpenResty使用的Lua版本是5.1,不使用更新版本的原因是5.2+版本的Lua API和C API都不相容於5.1。

自從 OpenResty 1.5.8.1 版本之後,預設捆綁的 Lua 直譯器就被替換成了 LuaJIT,而不再是標準 Lua。

安裝

wget https://openresty.org/download/openresty-1.13.6.1.tar.gz

tar xzf openresty-1.13.6.1.tar.gzcd openresty-1.13.6.1/

./configure --prefix=/home/alex/Lua/openresty/1.13.6    \            
# 啓用LuaJIT,這是一個Lua的JIT編譯器,預設沒有啓用            
--with-luajit \            
# 使用Lua 5.1標準直譯器,不推薦,應該儘可能使用LuaJIT            
--with-lua51 \            
# Drizzle、Postgres、 Iconv這幾個模組預設沒有啓用           
--with-http_drizzle_module、--with-http_postgres_module  --with-http_iconv_module
# Nginx 路徑如下:# nginx path prefix: "/home/alex/Lua/openresty/1.13.6/nginx"
# nginx binary file: "/home/alex/Lua/openresty/1.13.6/nginx/sbin/nginx"
# nginx modules path: "/home/alex/Lua/openresty/1.13.6/nginx/modules"
# nginx configuration prefix: "/home/alex/Lua/openresty/1.13.6/nginx/conf"
# nginx configuration file: "/home/alex/Lua/openresty/1.13.6/nginx/conf/nginx.conf"
# nginx pid file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/nginx.pid"
# nginx error log file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/error.log"
# nginx http access log file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/access.log"
# nginx http client request body temporary files: "client_body_temp"
# nginx http proxy temporary files: "proxy_temp"
# nginx http fastcgi temporary files: "fastcgi_temp"
# nginx http uwsgi temporary files: "uwsgi_temp"
# nginx http scgi temporary files: "scgi_temp"

make -j8 && make install

起步


1. 建立工程

一個OpenRestry工程,實際上就是對應了Nginx執行環境的目錄結構。例如:

mkdir -p ~/Lua/projects/openrestry
cd ~/Lua/projects/openrestry
mkdir conf && mkdir logs

2. 組態檔

使用lua-nginx-module模組提供的指令,你可以嵌入Lua指令碼到Nginx組態檔中,以生成響應內容:

daemon off;
worker_processes  1;
error_log stderr debug;
events {
    worker_connections 1024;
}
http {
    access_log /dev/stdout;
    server {
        listen 8080;
        location / {
            default_type text/html;
            # lua-nginx-module模組,屬於OpenResty專案,支援根據Lua指令碼輸出響應
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

3. 啓動服務

# 將Nginx執行時的字首設定爲上面的工程目錄
~/Lua/openresty/1.13.6/nginx/sbin/nginx -p ~/Lua/projects/openrestry -c conf/nginx.conf

4. 測試服務

curl http://localhost:8080/
# <p>hello, world</p>

IDE


1. Intellij

安裝三個外掛:

  1. nginx support:支援Nginx組態檔的語法高亮、格式化、自動完成。自動基於Lua語言對lua-nginx-module模組的相關指令進行語法高亮、自動完成
  2. Lua:支援Lua語言的開發和偵錯
  3. OpenResty Lua Support:爲OpenResty提供自動完成

OpenResty執行週期


不同類型的指令,職責如下:

指令 說明
set_by_lua* 流程分支處理判斷變數初始化
rewrite_by_lua* 轉發、重定向、快取等功能
access_by_lua* IP 準入、身份驗證、介面許可權、解密
content_by_lua* 內容生成
header_filter_by_lua* 響應頭部過濾處理,可以新增響應頭
body_filter_by_lua* 響應體過濾處理,例如轉換響應體
log_by_lua* 非同步完成日誌記錄,日誌可以記錄在本地,還可以同步到其他機器

儘管僅使用單個階段的指令content_by_lua*就可以完成以上職責,但是把邏輯劃分在不同階段,更加容易維護。


API框架


1. Nginx.conf

daemon off;
worker_processes  1;
error_log stderr debug;
events {
    worker_connections 1024;
}
http {
    access_log /dev/stdout;
    # lua模組搜尋路徑
    # 如果使用相對路徑,則必須將Nginx所在目錄作爲工作目錄,然後啓動服務
    # ${prefix}爲Nginx的字首目錄,可以在啓動Nginx時使用-p來指定
    lua_package_path '$prefix/scripts/?.lua;;';
 
    # 在開發階段,可以設定爲off,這樣避免每次修改程式碼後都需要reload
    # 生產環境一定要設定爲on
    lua_code_cache off;
 
    server {
        listen 80;
 
        location ~ ^/api/([-_a-zA-Z0-9]+) {
            # 在access階段執行,進行合法性校驗
            access_by_lua_file  scripts/auth-and-check.lua;
            # 生成內容,API名稱即爲Lua指令碼名稱
            content_by_lua_file scripts/$1.lua;
        }
    }
}

2.auth-and-check.lua

-- 黑名單
local black_ips = {["127.0.0.1"]=true}
 
-- 當前用戶端IP
local ip = ngx.var.remote_addr
if true == black_ips[ip] then
    -- 返回相應的HTTP狀態碼
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

使用Nginx變數


變數 說明
arg_name 請求中的name參數
args 請求中的參數
binary_remote_addr 遠端地址的二進制表示
body_bytes_sent 已發送的訊息體位元組數
content_length HTTP請求資訊裡的"Content-Length"
content_type 請求資訊裡的"Content-Type"
document_root 針對當前請求的根路徑設定值
document_uri 與$uri相同; 比如 /test2/test.php
host 請求資訊中的"Host",如果請求中沒有Host行,則等於設定的伺服器名
hostname 機器名使用 gethostname系統呼叫的值
http_cookie Cookie資訊
http_referer 參照地址
http_user_agent 用戶端代理資訊
http_via 最後一個存取伺服器的Ip地址。
http_x_forwarded_for 相當於網路存取路徑
is_args 如果請求行帶有參數,返回「?」,否則返回空字串
limit_rate

對連線速率的限制。此變數支援寫入:

Lua

-- 設定當前請求的響應傳遞速率限制

ngx.var.limit_rate = 1000 

nginx_version 當前執行的nginx版本號
pid Worker進程的PID
query_string 與$args相同
realpath_root 按root指令或alias指令算出的當前請求的絕對路徑。其中的符號鏈接都會解析成真是檔案路徑
remote_addr 用戶端IP地址
remote_port 用戶端埠號
remote_user 用戶端使用者名稱,認證用
request 使用者請求
request_body 這個變數(0.7.58+)包含請求的主要資訊。在使用proxy_pass或fastcgi_pass指令的location中比較有意義
request_body_file 用戶端請求主體資訊的臨時檔名
request_completion 如果請求成功,設爲"OK";如果請求未完成或者不是一系列請求中最後一部分則設爲空
request_filename 當前請求的檔案路徑名,比如/opt/nginx/www/test.php
request_method 請求的方法,比如"GET"、"POST"等
request_uri 請求的URI,帶參數
scheme 所用的協定,比如http或者是https
server_addr 伺服器地址,如果沒有用listen指明伺服器地址,使用這個變數將發起一次系統呼叫以取得地址(造成資源浪費)
server_name 請求到達的伺服器名
server_port 請求到達的伺服器埠號
server_protocol 請求的協定版本,"HTTP/1.0"或"HTTP/1.1"
uri 請求的URI,可能和最初的值有不同,比如經過重定向之類的

數據共用


1.跨工作進程

可以使用共用記憶體方式實現。

2.跨請求

可以使用Lua模組方式實現。

3.跨階段

在單個請求中,跨越多個Ng處理階段(access、content)共用變數時,可以使用 ngx.ctx表:

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)
     }
 }

ngx.ctx表的生命週期和請求相同,類似於Nginx變數。需要注意,每個子請求都有自己的ngx.ctx表,它們相互獨立。

你可以爲ngx.ctx表註冊元表,任何數據都可以放到該表中。

注意:存取ngx.ctx需要相對昂貴的元方法呼叫,不要爲了避免傳參而大量使用,影響效能.

指定Lua包路徑


#  設定純 Lua 擴充套件庫的搜尋路徑
# ';;' 是預設路徑
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";
 
# 設定 C 編寫的 Lua 擴充套件模組的搜尋路徑
# ';;' 是預設路徑
lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;';

大響應體的處理

大靜態檔案的響應,讓Nginx自己完成。

如果是應用程式動態生成的大響應體,可以使用HTTP 1.1的CHUNKED編碼。對應響應頭: Transfer-Encoding: chunked。這樣響應就可以逐塊的發送到用戶端,不至於佔用伺服器記憶體。

-- 可以進行限速,單位位元組
ngx.var.limit_rate = 64
--                        獲取設定目錄
local file, err = io.open(ngx.config.prefix() .. "nginx.conf", "r")
if not file then
    -- 列印Nginx日誌
    ngx.log(ngx.ERR, "open file error:", err)
    -- 以指定的HTTP狀態碼退出處理
    ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
-- 如果沒有ngx.exit,則:
local data
while true do
    data = file:read(64)
    if nil == data then
        break
    end
    ngx.print(data)
    --        true表示等待IO操作完成
    ngx.flush(true)
    ngx.sleep(1)
end
file:close()
 
-- http://localhost:8080/put-res-body-chunked 會一行行的輸出

配合其它location


1.內部呼叫

使用內部呼叫(子查詢),可以向某個location非阻塞的發起呼叫。目錄location可以是靜態檔案目錄,也可以由gx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle甚至其它ngx_lua模組提供內容生成。

需要注意:

  1. 內部呼叫僅僅是模擬HTTP的介面形式,不會產生額外的HTTP/TCP流量
  2. 內部呼叫完全不同於HTTP 301/302重定向指令,和內部重定向(ngx.exec)也完全不同
  3. 、在發起內部呼叫之前,必須先讀取完整的請求體。你可以設定lua_need_request_body指令或者呼叫ngx.req.read_body。如果請求體太大,可以考慮使用cosockets模組進行流式處理
  4. 子請求預設繼承當前請求的所有請求頭資訊。設定 proxy_pass_request_headers=off;可以忽略父請求的頭
  5. ngx.location.capture/capture_multi無法請求包含以下指令的location:add_before_body, add_after_body, auth_request, echo_location, echo_location_async, echo_subrequest, echo_subrequest_async
location /sum {
    -- 僅允許內部跳轉呼叫
    internal;
    content_by_lua_block {
        -- 解析請求參數
        local args = ngx.req.get_uri_args()
        -- 輸出
        ngx.say(tonumber(args.a)+tonumber(args.b))
    }
}
location /sub {
    internal;
    content_by_lua_block{
        -- 休眠
        ngx.sleep(0.1)
        local args = ngx.req.get_uri_args()
        ngx.print(tonumber(args.a) - tonumber(args.b))
    }
 
}
location /test{
    content_by_lua_block{
        -- 發起一個子查詢
        -- res.status 子請求的響應狀態碼
        -- res.header 子請求的響應頭,如果某個頭是多值的,則存放在table中
        -- res.body 子請求的響應體
        -- res.truncated 標記響應體是否被截斷。截斷意味着子請求處理過程中出現不可恢復的錯誤,例如超時、早斷
        local res = ngx.location.capture( "/sum", {
            args={a=3, b=8},  -- 爲子請求附加URI參數
            method = ngx.HTTP_POST, -- 指定請求方法,預設GET
            body = 'hello, world'   -- 指定請求體
        } )
        -- 並行的發起多個子查詢
        local res1, res2 = ngx.location.capture_multi( {
            {"/sum", {args={a=3, b=8}}},
            {"/sub", {args={a=3, b=8}}}
        })
        ngx.say(res1.status," ",res1.body)
        ngx.say(res2.status," ",res2.body)
    }
}

2.內部跳轉

location ~ ^/static/([-_a-zA-Z0-9/]+).jpg {
    -- 這裏將URI中捕獲的第一個分組,賦值給變數
    set $image_name $1;
    content_by_lua_block {
        -- ng.var可以讀取Nginx變數
        -- ngx.exec執行跳轉
        ngx.exec("/download_internal/images/" .. ngx.var.image_name .. ".jpg");
    };
}
 
location /download_internal {
    internal;
    -- 可以在這裏進行各種宣告,例如限速
    alias ../download;
}

注意,ngx.exec引發的跳轉完全在Ng內部完成,不會產生HTTP協定層的信號。

3.外部跳轉

使用ngx.redirect可以進行外部跳轉,也就是重定向。

location = / {
    rewrite_by_lua_block {
        return ngx.redirect('/blog');
    }
}

日誌記錄


OpenResty提供的日誌API爲 ngx.log(log_level, ...) 日誌輸出到Nginx的errorlog中。 

支援的日誌級別如下:

ngx.STDERR     -- 標準輸出
ngx.EMERG      -- 緊急報錯
ngx.ALERT      -- 報警
ngx.CRIT       -- 嚴重,系統故障,觸發運維告警系統
ngx.ERR        -- 錯誤,業務不可恢復性錯誤
ngx.WARN       -- 告警,業務中可忽略錯誤
ngx.NOTICE     -- 提醒,業務比較重要資訊
ngx.INFO       -- 資訊,業務瑣碎日誌資訊,包含不同情況判斷等
ngx.DEBUG      -- 偵錯

1.日誌歸集

模組lua-resty-logger-socket用於替代ngx_http_log_module,將Nginx日誌非同步的推播到遠端伺服器上。該模組的特性包括:

  1. 基於 cosocket 非阻塞 IO 實現
  2. 日誌累計到一定量,集體提交,增加網路傳輸利用率
  3. 短時間的網路抖動,自動容錯
  4. 日誌累計到一定量,如果沒有傳輸完畢,直接丟棄
  5. 日誌傳輸過程完全不落地,沒有任何磁碟 IO 消耗
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";
log_by_lua_file log.lua;
local logger = require "resty.logger.socket"
if not logger.initted() then
    local ok, err = logger.init {
        host = 'ops.gmem.cc',
        port = 8087,
        flush_limit = 1234,
        drop_limit = 5678,
    }
    if not ok then
        ngx.log(ngx.ERR, "failed to initialize the logger: ", err)
        return
    end
end
 
-- 通過變數msg來存取 accesslog
 
local bytes, err = logger.log(msg)
if err then
    ngx.log(ngx.ERR, "failed to log message: ", err)
    return
end

呼叫數據庫


-- 引入操控MySQL需要的模組
local mysql = require "resty.mysql"
-- 初始化數據庫物件
local db, err = mysql:new()
if not db then
    ngx.say("failed to instantiate mysql: ", err)
    return
end
-- 設定連線超時
db:set_timeout(1000)
-- 設定連線最大空閒時間,連線池容量
db:set_keepalive(10000, 100)
 
-- 發起數據庫連線
local ok, err, errno, sqlstate = db:connect {
    host = "127.0.0.1",
    port = 3306,
    database = "test",
    user = "root",
    password = "root",
    max_packet_size = 1024 * 1024
}
 
if not ok then
    ngx.say("Failed to connect: ", err, ": ", errno, " ", sqlstate)
    return
end
 
 
local res, err, _, _ = db:query([[
    DROP TABLE IF EXISTS USERS;
]])
if not res then ngx.say(err); return end
 
res, err, errno, sqlstate = db:query([[
    CREATE TABLE USERS
    (
        ID INT ,
        NAME VARCHAR(64)
    );
]])
if not res then ngx.say(err); return end
 
 
res, err, errno, sqlstate = db:query([[
    INSERT INTO USERS (ID,NAME) VALUES ('10000','Alex');
    INSERT INTO USERS (ID,NAME) VALUES ('10001','Meng');
]])
if not res then ngx.say(err); return end
 
local cjson = require "cjson"
ngx.say(cjson.encode(res))

要防止SQL隱碼攻擊,可以預處理一下使用者提供的參數:

req_id = ndk.set_var.set_quote_sql_str(req_id)))

呼叫HTTP


你可以用ngx.location.capture發起對另外一個location的子呼叫,並將後者設定爲上遊伺服器的代理。如果:

  1. 內部請求數量較多
  2. 需要頻繁修改上遊伺服器的地址

最好使用lua-resty-http模組。該模組提供了基於cosocket的HTTP用戶端。具有特性:

  1. 支援HTTP 1.0/1.1
  2. 支援SSL
  3. 支援響應體的流式介面,記憶體用量可控
  4. 對於簡單的應用場景,提供更簡單的介面
  5. 支援Keepalive
ngx.req.read_body()
-- 獲取當前請求的參數
local args, err = ngx.req.get_uri_args()
 
local http = require "resty.http"
-- 建立HTTP用戶端
local httpc = http.new()
-- request_uri函數在內部自動處理連線池
local res, err = httpc:request_uri("http://media-api.dev.svc.k8s.gmem.cc:8800/media/newpub/2017-01-01", {
    method = "POST",
    body = args.data,  -- 轉發請求參數給上遊伺服器
})
 
if 200 ~= res.status then
    ngx.exit(res.status)
end
 
if args.key == res.body then
    ngx.say("valid request")
else
    ngx.say("invalid request")
end