《用OpenResty搭建高性能服務端》筆記

概要

《用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 簡介

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 包含的技術:

  • Nginx:不只僅是負載均衡+反向代理等功能,Nginx c module開發成本高。
  • LuaJIT:OpenResty用的是 LuaJIT,LuaJIT 是主打性能的Lua。

OpenResty 本質上是將 LuaJIT 的虛擬機嵌入到 Nginx的worker中,因此效率特別高,在性能上,OpenResty 接近或超過 Nginx c module:

OpenResty已經顛覆了高性能服務端的開發模式。

OpenResty與市面上其餘語言對比:

  • node.js:第一門將異步非阻塞特性放入本身語言中的,前端同窗能夠快速切入。可是 node.js 用回調(callback)實現異步非阻塞,代碼寫起來比較麻煩。
  • Python:3.4以後加入了異步的支持,好比異步io和aiohttp;3.5引入了協程。缺點是版本跨度大,由於不少人仍是使用2.7。
  • Golang:最近幾年很是火。缺點:代碼寫法上須要使用go關鍵字;線上熱調試不方便(SystemTap 提供了有限的支持)。

Hello World

OpenResty安裝

以 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_cacheoff,做用域: 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。

OpenResty 入門

這節使用 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 API。

ngx_lua 有60多個指令(Directive),140多個 API(截止到2019-3-26)。

指令 是 ngx_lua 提供給Nginx調用的方法,與 Nginx自帶的 locationrewrite等是一個級別的。指令有本身的做用域,例如:content_by_lua_file只能做用於locationlocation 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

鏈接 MySQL

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

鏈接 Redis

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最佳實踐

OpenResty 緩存

使用 Lua shared dict

使用的話首先須要在 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

使用 Lua LRU cache

這個 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 還能夠 addreplaceincrget_stale(在 key 過時時也能夠返回以前的值)、get_keys(獲取全部 key,雖然不推薦,但說不定你的業務須要呢);第二個是內存的佔用,因爲 ngx shared dict 是 workers 之間共享的,因此在多 worker 的狀況下,內存佔用比較少。

本節內容參考來自:https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/cache.html

FFI和第三方模塊

FFI

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?) 發起子查詢
    返回一個包含四個元素的 Lua 表 (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/

相關文章
相關標籤/搜索