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
一個OpenRestry工程,實際上就是對應了Nginx執行環境的目錄結構。例如:
mkdir -p ~/Lua/projects/openrestry
cd ~/Lua/projects/openrestry
mkdir conf && mkdir logs
使用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>")
}
}
}
}
# 將Nginx執行時的字首設定爲上面的工程目錄
~/Lua/openresty/1.13.6/nginx/sbin/nginx -p ~/Lua/projects/openrestry -c conf/nginx.conf
curl http://localhost:8080/
# <p>hello, world</p>
安裝三個外掛:
不同類型的指令,職責如下:
指令 | 說明 |
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*就可以完成以上職責,但是把邏輯劃分在不同階段,更加容易維護。
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;
}
}
}
-- 黑名單
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
變數 | 說明 | |
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
|
|
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,可能和最初的值有不同,比如經過重定向之類的 |
可以使用共用記憶體方式實現。
可以使用Lua模組方式實現。
在單個請求中,跨越多個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_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非阻塞的發起呼叫。目錄location可以是靜態檔案目錄,也可以由gx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle甚至其它ngx_lua模組提供內容生成。
需要注意:
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)
}
}
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協定層的信號。
使用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 -- 偵錯
模組lua-resty-logger-socket用於替代ngx_http_log_module,將Nginx日誌非同步的推播到遠端伺服器上。該模組的特性包括:
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)))
你可以用ngx.location.capture發起對另外一個location的子呼叫,並將後者設定爲上遊伺服器的代理。如果:
最好使用lua-resty-http模組。該模組提供了基於cosocket的HTTP用戶端。具有特性:
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