《用OpenResty搭建高性能服務端》是
OpenResty
系列課程中的入門課程,主講人:溫銘老師。課程分爲10個章節,側重於OpenResty
的基本概念和主要特色的介紹,包括它的指令、nginx_lua
API、緩存、如何鏈接數據庫、執行階段等,並經過幾個實際的操做和代碼片斷,告訴你們學習中如何搭建開發、測試環境,如何調試、查找和解決問題。php
視頻播放地址:https://study.163.com/course/introduction.htm?courseId=1520005html
課程目錄一覽:
前端
我的評價:評分滿分。內容由淺入深,思路清晰,內容組織有序,容易上手,爲初學者打開了一扇學習的大門。很是不錯的分享。學完後需再配合 《OpenResty最佳實踐》 + 官方文檔 進行系統學習。node
下面是學習筆記,內容主要是以老師的講解爲主,加上部分本身補充或理解的內容。mysql
本文環境:nginx
$ uname -a Linux ba2f3eedf7df 4.4.111-boot2docker #1 SMP Thu Jan 11 16:25:31 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/redhat-release CentOS release 6.8 (Final) $ /usr/local/openresty/bin/openresty -v nginx version: openresty/1.13.6.2 $ /usr/local/openresty/luajit/bin/luajit -v LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
OpenResty®
是一個基於 Nginx 與 Lua 的高性能 Web 平臺,其內部集成了大量精良的 Lua 庫、第三方模塊以及大多數的依賴項。用於方便地搭建可以處理超高併發、擴展性極高的動態 Web 應用、Web 服務和動態網關。git
OpenResty 基於Nginx
開發,能夠簡單認爲是 Nginx
+ lua-nginx-module
的組合版。github
官網:https://openresty.org/cn/
官方文檔:https://github.com/openresty/lua-nginx-module#versionweb
高性能服務端兩個重要要素:須要支持緩存,語言層面要支持異步非堵塞。redis
緩存速度上,內存 > SSD > 機械磁盤;本機 > 網絡 ; 進程內 > 進程間 。異步非阻塞指的是事件驅動方式(事件完成後再通知)。
OpenResty 包含的技術:
OpenResty
本質上是將 LuaJIT
的虛擬機嵌入到 Nginx的worker中,因此效率特別高,在性能上,OpenResty
接近或超過 Nginx c module:
OpenResty已經顛覆了高性能服務端的開發模式。
OpenResty
與市面上其餘語言對比:
SystemTap
提供了有限的支持)。以 CentOS 爲例:
mkdir /opt && cd /opt # download openresty wget https://openresty.org/download/openresty-1.13.6.2.tar.gz tar zxvf openresty-1.13.6.2.tar.gz cd openresty-1.13.6.2 # configure ./configure --prefix=/usr/local/openresty -j4 make -j4 && make install
其中 源碼包能夠到 https://openresty.org/cn/download.html 該頁面獲取。
-j4
表示使用4核。configure
那一步還能夠指定各類參數:
./configure --prefix=/usr/local/openresty \ --with-luajit \ --without-http_redis2_module \ --with-http_iconv_module \ --with-http_postgres_module
使用 ./configure --help
查看更多的選項。
其它系統環境上安裝能夠參考 https://openresty.org/cn/installation.html 。
其實安裝 OpenResty 和安裝 Nginx 是相似的,由於 OpenResty 是基於 Nginx 開發的。
若是已經安裝了 Nginx,又想使用 OpenResty 的功能,能夠參考 《Nginx編譯安裝Lua》:http://www.javashuo.com/article/p-uyselemm-dh.html 一文安裝lua-nginx-module
模塊便可。
修改 /usr/local/openresty/nginx/conf/nginx.conf
:
worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { server { listen 8080; location /hello { default_type text/html; content_by_lua ' ngx.say("<p>hello, world</p>") '; } } }
把默認的80
端口改成8080
,新增/hello
部分。
其中content_by_lua
即是 OpenResty 提供的指令,在官方文檔能夠搜索到:
如今咱們啓動OpenResty:
/usr/local/openresty/nginx/sbin/nginx
啓動成功後,查看效果:
curl http://127.0.0.1:8080/hello <p>hello, world</p>
說明成功運行了。
知識點:
一、content_by_lua
:返回的內容使用 lua 代碼。
二、content_by_lua_file
:讀取lua文件裏的 lua 代碼。
三、默認狀況下,修改Lua代碼,須要 reload OpenResty服務纔會生效。能夠修改lua_code_cache
爲off
,做用域: http, server, location, location if。請勿在生產環境裏開啓。
測試1:使用content_by_lua_file
cd /usr/local/openresty mkdir nginx/conf/lua vim nginx/conf/lua/hello.lua
內容爲:
ngx.say("<p>hello, lua world</p>")
而後修改 nginx.conf:
location /hello { default_type text/html; content_by_lua_file conf/lua/hello.lua; }
重啓 OpenResty:
$ ./nginx/sbin/nginx -s reload
啓動成功後,查看效果:
$ curl http://127.0.0.1:8080/hello <p>hello, lua world</p>
測試2:關閉lua_code_cache
:
根據lua_code_cache
做用域,咱們能夠在server塊加上:
lua_code_cache off; location /hello { default_type text/html; content_by_lua_file conf/lua/hello.lua; }
而後重啓:
$ ./nginx/sbin/nginx -s reload nginx: [alert] lua_code_cache is off; this will hurt performance in /usr/local/openresty/nginx/conf/nginx.conf:43
提示說lua_code_cache
關閉後影響性能。咱們再次修改 nginx/conf/lua/hello.lua
的代碼,保存後就會生效,無需 reload server。
這節使用 ngx_lua api完成一個小功能。
lua代碼:
nginx/conf/lua/get_random_string.lua
-- 實現隨機字符串 local args = ngx.req.get_uri_args() local salt = args.salt if not salt then ngx.exit(ngx.HTTP_BAD_REQUEST) end local str = ngx.md5(ngx.time() .. salt) ngx.say(str)
修改 nginx.conf ,新增:
location /get_random_string { content_by_lua_file conf/lua/get_random_string.lua; }
因爲修改了 nginx.conf ,須要reload OpenResty 服務。而後,咱們訪問服務:
$ curl http://127.0.0.1:8080/get_random_string?salt=2 2d8231ff301ab0ce8b95c7e4c2c59574 $ curl http://127.0.0.1:8080/get_random_string?salt=2 c145db4ec45a6bf792ac30ed4246c563
說明:
一、ngx.req.get_uri_args()
用於獲取URI請求參數。
二、ngx.HTTP_BAD_REQUEST
爲ngx常量,指的是400。代碼裏儘可能使用常量。
三、ngx.time()
用於獲取時間戳,是帶有緩存的。與Lua的日期庫不一樣,不涉及系統調用。儘可能使用Ngx給出的方法,以避免發生性能問題。
四、ngx.md5()
用於生成md5值。
五、若是代碼裏有語法錯誤,咱們能夠經過nginx 的 error.log裏看到,默認文件是 nginx/logs/error.log
。
再次提醒你們,作 OpenResty 開發,lua-nginx-module 的文檔是你的首選,Lua 語言的庫都是同步阻塞的,用的時候要三思。也就是說,儘可能使用 ngx_lua提供的api,而不是使用 Lua 自己的。例如
ngx.sleep()
與 lua提供的sleep,前者不會形成阻塞,後者是會阻塞的,詳見:sleep · OpenResty最佳實踐 。
本節主要是帶着你們簡單的過一下經常使用的ngx_lua API。
ngx_lua 有60多個指令(Directive),140多個 API(截止到2019-3-26)。
指令 是 ngx_lua 提供給Nginx調用的方法,與 Nginx自帶的 location
、rewrite
等是一個級別的。指令有本身的做用域,例如:content_by_lua_file
只能做用於location
和location if
裏面:
API 是指ngx_lua基於lua代碼實現的一系列方法或常量,遵循 lua的語法規則。只能在lua代碼塊或者lua文件裏使用。
例如:
content_by_lua ' ngx.say("<p>hello, world</p>") ';
其中content_by_lua
是指令,做用於location
塊;ngx.say()
是 ngx_lua 提供的API。
在官方文檔上能夠找到指令及API所在的位置:
下面,咱們使用 ngx_lua完成另一個小功能:實現base64的解碼並從新json編碼輸出。代碼裏會用到一些指令和API。
lua代碼:
nginx/conf/lua/decode_info.lua
-- 實現base64的解碼並從新json編碼輸出 local json = require "cjson" ngx.req.read_body() local args = ngx.req.get_post_args() if not args or not args.info then ngx.exit(ngx.HTTP_BAD_REQUEST) end local client_ip = ngx.var.remote_var or '127.0.0.1' local user_agnet = ngx.req.get_headers()['user_agent'] or '' local info = ngx.decode_base64(args.info) local res = {} res.client_ip = client_ip res.user_agnet = user_agnet res.info = info ngx.say(json.encode(res))
修改 nginx.conf ,新增:
location /decode_info { content_by_lua_file conf/lua/decode_info.lua; }
因爲修改了 nginx.conf ,須要 reload OpenResty 服務。而後,咱們訪問服務:
$ php -r "echo base64_encode('test');" dGVzdA== $ curl -XPOST -d "info=dGVzdA==" http://127.0.0.1:8080/decode_info {"user_agnet":"curl\/7.19.7","client_ip":"127.0.0.1","info":"test"}
說明:
一、require
是 lua 裏面引入其餘庫的關鍵字。這裏引入的 cjson。
二、當咱們要讀取 http裏的post數據的時候,就須要使用ngx.req.read_body()
。該API同步讀取客戶端請求主體而不阻塞Nginx事件循環。
三、ngx.req.get_post_args()
用於獲取post請求數據。
四、ngx.var.remote_var
實際是獲取的nginx裏的變量remote_var
。也就是說,ngx.var.xxx
實際是獲取的nginx裏的變量xxx
。例如:
nginx變量詳見:[Alphabetical index of variables}(http://nginx.org/en/docs/varindex.html)。 ngx_lua ngx.var
API詳見:ngx.var.VARIABLE。
五、ngx.req.get_headers()
用於讀取nginx的header參數。返回的是lua table。
六、ngx.decode_base64()
用於 base64字符串解碼。對應的編碼API是 ngx.encode_base64()
。
防盜版聲明:本文系原創文章,發佈於公衆號飛鴻影的博客
(fhyblog)及博客園,轉載需做者贊成。
鏈接數據庫咱們須要使用到ngx_lua的第三方庫:
這兩個庫都是基於cosocket實現的,特色是異步非阻塞。代碼風格是同步的寫法。更多第三方庫詳見:See Also 。
lua代碼:
nginx/conf/lua/test_mysql.lua
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) -- 1 sec local ok, err, errcode, sqlstate = db:connect{ host = "127.0.0.1", port = 3306, database = "ngx_test", user = "ngx_test", password = "ngx_test", charset = "utf8", max_packet_size = 1024 * 1024, } if not ok then ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate) return end -- insert res, err, errcode, sqlstate = db:query("insert into cats (name) " .. "values (\'Bob\'),(\'\'),(null)") if not res then ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") return end ngx.say(res.affected_rows, " rows inserted into table cats ", "(last insert id: ", res.insert_id, ")") -- run a select query, expected about 10 rows in the result set res, err, errcode, sqlstate = db:query("select * from cats order by id asc", 10) if not res then ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") return end local cjson = require "cjson" ngx.say("result: ", cjson.encode(res)) -- close connection local ok, err = db:close() if not ok then ngx.say("failed to close: ", err) return end
修改 nginx.conf ,新增:
location /test_mysql { content_by_lua_file conf/lua/test_mysql.lua; }
因爲修改了 nginx.conf ,須要 reload OpenResty 服務。而後,咱們訪問服務:
$ http://127.0.0.1:8080/test_mysql
lua代碼:
nginx/conf/lua/test_redis.lua
local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) -- 1 sec local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.say("failed to connect: ", err) return end ok, err = red:set("dog", "an animal") if not ok then ngx.say("failed to set dog: ", err) return end ngx.say("set result: ", ok) local res, err = red:get("dog") if not res then ngx.say("failed to get dog: ", err) return end if res == ngx.null then ngx.say("dog not found.") return end ngx.say("dog: ", res) -- close the connection right away local ok, err = red:close() if not ok then ngx.say("failed to close: ", err) return end
修改 nginx.conf ,新增:
location /test_redis { content_by_lua_file conf/lua/test_redis.lua; }
因爲修改了 nginx.conf ,須要 reload OpenResty 服務。而後,咱們訪問服務:
$ http://127.0.0.1:8080/test_redis
更多參考:
redis 接口的二次封裝(簡化建連、拆連等細節) · OpenResty最佳實踐
使用的話首先須要在 nginx.conf 加上一句:
lua_shared_dict my_cache 128m;
這個my_cache
就是申請 Lua shared dict 緩存, 下面示例裏會用到。
這個緩存是 Nginx 全部 worker 之間共享的,內部使用的 LRU 算法(最近最少使用)來判斷緩存是否在內存佔滿時被清除。
function get_from_cache(key) local cache_ngx = ngx.shared.my_cache local value = cache_ngx:get(key) return value end function set_to_cache(key, value, exptime) if not exptime then exptime = 0 end local cache_ngx = ngx.shared.my_cache local succ, err, forcible = cache_ngx:set(key, value, exptime) return succ end
更多支持的命令詳見:https://github.com/openresty/lua-nginx-module#ngxshareddict 。
這個 cache 是 worker 級別的,不會在 Nginx wokers 之間共享。而且,它是預先分配好 key 的數量,而 shared dict 須要本身用 key 和 value 的大小和數量,來估算須要把內存設置爲多少。
官方示例:
myapp.lua
local _M = {} -- alternatively: local lrucache = require "resty.lrucache.pureffi" local lrucache = require "resty.lrucache" -- we need to initialize the cache on the lua module level so that -- it can be shared by all the requests served by each nginx worker process: local c, err = lrucache.new(200) -- allow up to 200 items in the cache if not c then return error("failed to create the cache: " .. (err or "unknown")) end function _M.go() c:set("dog", 32) c:set("cat", 56) ngx.say("dog: ", c:get("dog")) ngx.say("cat: ", c:get("cat")) c:set("dog", { age = 10 }, 0.1) -- expire in 0.1 sec c:delete("dog") c:flush_all() -- flush all the cached data end return _M
nginx.conf
http { lua_package_path "/path/to/lua-resty-lrucache/lib/?.lua;;"; server { listen 8080; location = /t { content_by_lua ' require("myapp").go() '; } } }
更多支持的命令詳見:https://github.com/openresty/lua-resty-lrucache 。
那麼這兩個緩存 如何選擇 ?
shared.dict
使用的是共享內存,每次操做都是全局鎖,若是高併發環境,不一樣 worker 之間容易引發競爭。因此單個shared.dict
的體積不能過大。lrucache
是 worker 內使用的,因爲 Nginx 是單進程方式存在,因此永遠不會觸發鎖,效率上有優點,而且沒有shared.dict
的體積限制,內存上也更彈性,但不一樣 worker 之間數據不一樣享,同一緩存數據可能被冗餘存儲。
你須要考慮的,一個是
Lua lru cache
提供的 API 比較少,如今只有 get、set 和 delete,而ngx shared dict
還能夠add
、replace
、incr
、get_stale
(在 key 過時時也能夠返回以前的值)、get_keys
(獲取全部 key,雖然不推薦,但說不定你的業務須要呢);第二個是內存的佔用,因爲ngx shared dict
是 workers 之間共享的,因此在多 worker 的狀況下,內存佔用比較少。
本節內容參考來自:https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/cache.html
FFI是 LuaJIT
中的一個擴展庫,它容許咱們使用 Lua 代碼調用C語言的數據結構和函數。
FFI庫在很大程度上避免了在C中編寫繁瑣的手動 Lua/C
綁定的須要。無需學習單獨的綁定語言 - 它解析普通的C聲明!這些能夠從C頭文件或參考手冊中剪切粘貼。
如何調用外部C庫函數呢?
一、加載FFI庫。
二、爲函數添加C聲明。
三、調用命名的C函數。
看一個官方提供的簡單示例:
-- test_ffi.lua local ffi = require("ffi") ffi.cdef[[ int printf(const char *fmt, ...); ]] ffi.C.printf("Hello %s!", "world")
咱們運行:
$ luajit test_ffi.lua Hello world!
詳見:http://luajit.org/ext_ffi.html
默認的 resty 庫所在位置:
$ pwd /usr/local/openresty $ ls lualib/ cjson.so ngx/ rds/ redis/ resty/ $ ls lualib/resty/ aes.lua limit/ md5.lua redis.lua sha384.lua upload.lua core/ lock.lua memcached.lua sha1.lua sha512.lua upstream/ core.lua lrucache/ mysql.lua sha224.lua sha.lua websocket/ dns/ lrucache.lua random.lua sha256.lua string.lua
如今以安裝 lua-resty-http
爲例:
$ cd /opt # 下載並解壓 $ wget https://github.com/ledgetech/lua-resty-http/archive/v0.13.tar.gz && tar zxvf v0.13.tar.gz # 複製到resty目錄便可 $ cp -r lua-resty-http-0.13/lib/resty/* /usr/local/openresty/lualib/resty/ # 查看安裝的模塊 $ cd /usr/local/openresty/lualib/resty/ && ls http* http_headers.lua http.lua
使用示例:
local http = require "resty.http" local httpc = http.new() local res, err = httpc:request_uri("http://example.com/helloworld", { method = "POST", body = "a=1&b=2", headers = { ["Content-Type"] = "application/x-www-form-urlencoded", }, keepalive_timeout = 60, keepalive_pool = 10 }) if not res then ngx.say("failed to request: ", err) return end ngx.status = res.status for k,v in pairs(res.headers) do -- end ngx.say(res.body)
子查詢只是模擬 HTTP 接口的形式, 沒有 額外的 HTTP/TCP 流量,也 沒有 IPC (進程間通訊) 調用。全部工做在內部高效地在 C 語言級別完成。
子查詢只能在一個 location
裏調用其它 一個或多個 `location。
res = ngx.location.capture(uri, options?)
發起子查詢res.status
, res.header
, res.body
, 和 res.truncated
)。rewrite_by_lua *
,access_by_lua *
,content_by_lua *
。示例:
res = ngx.location.capture( '/foo/bar', { method = ngx.HTTP_POST, body = 'hello, world' } )
res1, res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ... })
發起多個併發子查詢rewrite_by_lua *
,access_by_lua *
,content_by_lua *
。示例:
res1, res2, res3 = ngx.location.capture_multi{ { "/foo", { args = "a=3&b=4" } }, { "/bar" }, { "/baz", { method = ngx.HTTP_POST, body = "hello" } }, } if res1.status == ngx.HTTP_OK then ... end if res2.body == "BLAH" then ... end
做用:
減小網絡請求。
方便配置降級服務。
子查詢文檔參考: https://github.com/openresty/lua-nginx-module#ngxlocationcapture
下面這個圖是 ngx_lua 各個指令的執行順序。
執行階段說明:
set_by_lua*
: 流程分支處理判斷變量初始化rewrite_by_lua*
: 轉發、重定向、緩存等功能(例如特定請求代理到外網)access_by_lua*
: IP 准入、接口權限等狀況集中處理(例如配合 iptable 完成簡單防火牆)content_by_lua*
: 內容生成header_filter_by_lua*
: 響應頭部過濾處理(例如添加頭部信息)body_filter_by_lua*
: 響應體過濾處理(例如完成應答內容統一成大寫)log_by_lua*
: 會話完成後本地異步完成日誌記錄(日誌能夠記錄在本地,還能夠同步到其餘機器)因爲 Nginx 把一個請求分紅了不少階段,這樣第三方模塊就能夠根據本身行爲,掛載到不一樣階段進行處理達到目的。不一樣的階段,有不一樣的處理行爲,理解了他,也能更好的理解 Nginx 的設計思惟。
一、如何自學
《OpenResty最佳實踐》
二、遇到問題怎麼辦
1) 看 nginx 的error.log
2) 疑難問題把可復現信息在官方郵件組裏反饋
3) 善用Google
4) QQ交流羣
一、OpenResty® - 中文官方站
https://openresty.org/cn/
二、openresty/lua-nginx-module: Embed the Power of Lua into NGINX HTTP servers
https://github.com/openresty/lua-nginx-module#version
三、FFI Library
http://luajit.org/ext_ffi.html
四、luajit FFI簡單使用(1) - Abel's Blog - CSDN博客
https://blog.csdn.net/erlang_hell/article/details/52836467
五、OpenResty最佳實踐
https://moonbingbing.gitbooks.io/openresty-best-practices/