OpenResty(nginx+lua) 入門

OpenResty 官網:http://openresty.org/php

OpenResty 是一個nginx和它的各類三方模塊的一個打包而成的軟件平臺。最重要的一點是它將lua/luajit打包了進來,使得咱們可使用lua腳原本進行web的開發。有了lua,咱們能夠藉助於nginx的異步非阻塞的功能,達到使用 lua 異步併發訪問後端的 MySQL, PostgreSQL, Memcached, Redis等等服務。特別是特有的 ngx.location.capture_multi 功能讓人印象深入,其能夠達到極大的減小瀏覽器的http鏈接數量,而且能夠異步併發的訪問後臺 Java/PHP/Python 等等接口。OpenResty 架構的web能夠輕鬆超越Node.js的性能,而且對後端語言沒有限制,你可使用Java/PHP/Python等等各類語言。OpenResty(nginx+lua)能夠替代node.js的前端渲染的功能。html

OpenResty (aka. ngx_openresty) is a full-fledged web application server by bundling the standard Nginx core, lots of 3rd-party Nginx modules, as well as most of their external dependencies.

By taking advantage of various well-designed Nginx modules, OpenResty effectively turns the nginx server into a powerful web app server, in which the web developers can use the Lua programming language to script various existing nginx C modules and Lua modules and construct extremely high-performance web applications that are capable to handle 10K+ connections.

OpenResty aims to run your server-side web app completely in the Nginx server, leveraging Nginx's event model to do non-blocking I/O not only with the HTTP clients, but also with remote backends like MySQL, PostgreSQL, Memcached, and Redis.前端

1. 安裝OpenRestyjava

先安裝依賴:yum install readline-devel pcre-devel openssl-devel gccnode

解壓: tar zxvf ngx_openresty-1.9.3.1.tar.gzmysql

創建一個軟鏈接:ln -s ngx_openresty-1.9.3.1 openrestynginx

進入目錄:cd openrestygit

編譯:github

./configure \
             --with-cc-opt="-I/usr/local/include" \
             --with-ld-opt="-L/usr/local/lib" \
             --prefix=/opt/openresty 

... ...
Configuration summary
  + using system PCRE library
  + using system OpenSSL library
  + md5: using OpenSSL library
  + sha1: using OpenSSL library
  + using system zlib library

  nginx path prefix: "/opt/openresty/nginx"
  nginx binary file: "/opt/openresty/nginx/sbin/nginx"
  nginx configuration prefix: "/opt/openresty/nginx/conf"
  nginx configuration file: "/opt/openresty/nginx/conf/nginx.conf"
  nginx pid file: "/opt/openresty/nginx/logs/nginx.pid"
  nginx error log file: "/opt/openresty/nginx/logs/error.log"
  nginx http access log file: "/opt/openresty/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"

其中 --prefix=/opt/openresty 指定了安裝目錄,不指定的話默認會安裝到 /usr/local/openresty 目錄下。web

編譯安裝: make && make install

[root@localhost src]# cd /opt/openresty/
[root@localhost openresty]# ls
bin  luajit  lualib  nginx

能夠看到 /opt/openresty 目錄下四個文件夾,其中包括了 luajit,nginx。

啓動openresty: /opt/openresty/nginx/sbin/nginx -c /opt/openresty/nginx/conf/nginx.conf -p /opt/openresty/nginx/

[root@localhost src]# ps -elf|grep nginx
1 S root      2076     1  0  80   0 - 34999 -      21:24 ?        00:00:00 nginx: master process /opt/openresty/nginx/sbin/nginx -c /opt/openresty/nginx/conf/nginx.conf -p /opt/openresty/nginx/
5 S nobody    2077  2076  0  80   0 - 35045 -      21:24 ?        00:00:00 nginx: worker process                                    
0 S root      2079  1678  0  80   0 -  1088 -      21:24 pts/1    00:00:00 grep nginx

驗證能夠訪問: curl 127.0.0.1

2. content_by_lua 和 content_by_lua_file

nginx 如何嵌入 lua 腳本。方法就是在nginx的配置文件nginx.conf 中使用 content_by_lua 或者 cotent_by_lua_file 指令:

1) content_by_lua 通常在很簡單的lua腳本時使用:

        location /lua {
                set $test "hello, world.";
                content_by_lua '
                        ngx.header.content_type = "text/plain";
                        ngx.say(ngx.var.test);
                ';
        }

訪問 http://localhost/lua 能夠看到輸出到頁面的  hello, world.

2)cotent_by_lua_file 適應於複雜的 lua 腳本,專門放入一個文件中:

        location /lua2 {
            #lua_code_cache off;
            content_by_lua_file lua/hello.lua;
        }

路徑相對於 /opt/openresty/nginx

[root@localhost lua]# pwd
/opt/openresty/nginx/lua
[root@localhost lua]# cat hello.lua
ngx.say('hello ngx_lua!!!!');

本例子中 hello.lua 只包含一句: ngx.say('hello ngx_lua!!!!');

訪問 /lua2 :

[root@localhost lua]# curl localhost/lua
hello ngx_lua!!!!

能夠看到訪問成功。

在 nginx.conf 文件的 server {.. ...} 中加入 lua_code_cache off; 能夠方便調試lua腳本,修改lua腳本以後,不須要 reload nginx.

openresty 中的 nginx 嵌入 luajit 的原理:

每個nginx的進程中都嵌入了一個 luajit的虛擬機,來執行lua腳本。nginx將lua腳本的執行交給了luajit vm.

3. ngx_lua 的指令 和 API

上面咱們說到 nginx 嵌入 lua 腳本可使用 content_by_lua 和 content_by_lua_file,它們實際上是指令(Directives),相似的指令還有不少,

具體參見:https://www.nginx.com/resources/wiki/modules/lua/#directives

這些指令都是 nginx 訪問 lua 腳本的入口。

ngx_lua API:

指令是 nginx 訪問 lua 腳本的入口。那麼lua腳本如何調用nginx中的函數呢?就是經過 ngx_lua 的API 。

具體介紹參見:https://www.nginx.com/resources/wiki/modules/lua/#nginx-api-for-lua

The various *_by_lua and *_by_lua_file configuration directives serve as gateways to the Lua API within the nginx.conf file. The NGINX Lua API described below can only be called within the user Lua code run in the context of these configuration directives.

The API is exposed to Lua in the form of two standard packages ngx and ndk. These packages are in the default global scope within ngx_lua and are always available within ngx_lua directives.

其實nginx和Lua的交互開發主要就是指令和API,固然還有lua腳本的語法。指令是nginx訪問lua的入口,API是lua調用nginx的函數,lua是腳本編程語言。

指令其實很簡單,因此主要就是熟悉ngx_lua的 API 和Lua語法。

4. lua 訪問 redis

lua-resty-redis 模塊:https://github.com/openresty/lua-resty-redis (有文檔能夠參考)

在nginx.conf中加入:

        location /redis_test{
            content_by_lua_file lua/redis_test.lua;
        }

redis_test.lua 內容:

[root@localhost lua]# cat redis_test.lua
local redis = require "resty.redis"
local red = redis:new()

red:set_timeout(1000)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
        ngx.say("failed to connect: ", err)
        return
end

ngx.say("set result: ", ok)

local res, err = red:get("dog")
if not res then
        ngx.say("failed to get doy: ", err)
        return
end

if res == ngx.null then
        ngx.say("dog not found.")
        return
end

ngx.say("dog: ", res)

[root@localhost lua]#

訪問:

[root@localhost lua]# curl localhost/redis_test
set result: 1
dog: an animal
[root@localhost lua]#

咱們看到訪問成功。

5. lua 訪問mysql

openresty的mysql模塊:lua-resty-mysql :https://github.com/openresty/lua-resty-mysql(有文檔能夠參考)

在nginx.conf加入以下配置:

        location /mysql_test {
            content_by_lua_file lua/mysql_test.lua;
        }

mysql_test.lua腳本內容:

[root@localhost lua]# pwd
/opt/openresty/nginx/lua
[root@localhost lua]# cat mysql_test.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)

local ok, err, errno, sqlstate = db:connect{
        host = "127.0.0.1",
        port = 3306,
        database = "ngx_lua",
        user = "root",
        password="digdeep",
        max_packet_size = 1024 * 1024
}

if not ok then
        ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate)
        return
end

ngx.say("connected to mysql.")

local res, err, errno, sqlstate = db:query("drop table if exists cats")
if not res then
        ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
        return
end

res, err, errno, sqlstate = db:query("create table cats " .. "(id int not null primary key auto_increment, "
                                        .. "name varchar(30))")
if not res then
        ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
        return
end

ngx.say("table cats created.")

res, err, errno, sqlstate = db:query("insert into cats(name) " .. "values (\'Bob\'),(\'\'),(null)")
if not res then
        ngx.say("bad request: ", err, ": ", errno, ": ", sqlstate, ".")
        return
end

ngx.say(res.affected_rows, " rows inserted into table cats ", "(last insert id: ", res.insert_id, ")")

res, err, errno, sqlstate = db:query("select * from cats order by id asc", 10)
if not res then
        ngx.say("bad result ", err, ": ", errno, ": ", sqlstate, ".")
        return
end

local cjson = require "cjson"
ngx.say("result: ", cjson.encode(res))

local ok, err = db:set_keepalive(1000, 100)
if not ok then
        ngx.say("failed to set keepalive: ", err)
        return
end

測試:

[root@localhost lua]# curl localhost/mysql_test
connected to mysql.
table cats created.
3 rows inserted into table cats (last insert id: 1)
result: [{"name":"Bob","id":1},{"name":"","id":2},{"name":null,"id":3}]

測試經過。

5. lua 的 capture 和 capture_multi(子查詢)

capture_multi 是 openresty 一個十分強大的功能。它能極大的減小前端瀏覽器發送的http請求的數量,突破了瀏覽器對於同一個服務器併發請求數量的限制,由於他能夠將前端的多個http請求減小爲只要一個http請求到nginx,而後nginx使用capture_multi特性,對後端發起多個異步併發請求,而後統一將結果返回給前端。下面看一個例子:

首先在nginx.conf中加入下面的 location 配置,而且配置好 nginx 訪問 php 的配置:

        location /capture {
            content_by_lua_file lua/capture.lua;
            #access_by_lua_file lua/capture.lua;
        }

        location ~ \.php$ {
            root           html;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }

capture.lua 的代碼以下:

[root@localhost lua]# pwd
/opt/openresty/nginx/lua
[root@localhost lua]# cat capture.lua
local res1,res2,res3,res4 = ngx.location.capture_multi{
        {"/mysql_test", {args="t=1&id=1"}},
        {"/redis_test", {args="t=2&id=2"}},
        {"/lua", {args="t=3&id=3"}},
        {"/index.php", {args="t=3&id=3"}},
}

ngx.header.content_type="text/plain"
ngx.say(res1.body)
ngx.say(res2.body)
ngx.say(res3.body)
ngx.say(res4.truncated)
ngx.say(res4.status)
ngx.say(res4.header["Set-Cookie"])

--ngx.say(res4.body)

index.php 代碼:

[root@localhost html]# pwd
/opt/openresty/nginx/html
[root@localhost html]# cat index.php
<?php
        echo phpinfo();
?>

訪問:

[root@localhost html]# curl localhost/capture
connected to mysql.
table cats created.
3 rows inserted into table cats (last insert id: 1)
result: [{"name":"Bob","id":1},{"name":"","id":2},{"name":null,"id":3}]

set result: 1
dog: an animal

hello ngx_lua!!!!

false
200
nil

能夠看到訪問成功了。/mysql_test/redis_test, /lua, /index.php 四個請求的結果都輸出了。

注意:

ngx.location.capture_multi{... ...} 中的多個異步併發請求能夠是 nginx.conf 中配置的 location(好比 /mysql_test, /redis_test, /lua),也能夠不是 location配置的路徑,好比 index.php 就不是。index.php 就是一個簡單的後臺php 腳本。固然也能夠是一個 java 實現的後臺接口。

6. openresty的緩存 lua_shared_dict

定義一個緩存:

在nginx的配置文件 nginx.conf 的 http 端下面加入指令:

lua_shared_dict ngx_cache 128m;

就定義了一個 名稱爲 ngx_cache 大小爲128m的內存用於緩存,注意該緩存是全部nginx work process所共享的。

在lua腳本中訪問緩存:

local ngx_cache = ngx.shared.ngx_cache
local value = ngx_cache:get(key)

local succ, err, forcible = ngx_cache:set(key, value, exptime)

下面測試一下,首先在 nginx.conf的server端中加入:

        location /cache {
            content_by_lua_file lua/cache.lua;
        }

而後編寫 cache.lua 腳本:

[root@localhost lua]# cat cache.lua
local redis = require "resty.redis"
local red = redis:new()

function set_to_cache(key, value, exptime)
        if not exptime then
                exptime = 0
        end
        local ngx_cache = ngx.shared.ngx_cache
        local succ, err, forcible = ngx_cache:set(key, value, exptime)
        return succ
end

function get_from_cache(key)
        local ngx_cache = ngx.shared.ngx_cache;
        local value = ngx_cache:get(key)
        if not value then
                value = get_from_redis(key)
                set_to_cache(key, value)
                return value
        end

        ngx.say("get from cache.")
        return value
end

function get_from_redis(key)
        red:set_timeout(1000)

        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
                ngx.say("failed to connect: ", err)
                return
        end

        local res, err = red:get(key)
        if not res then
                ngx.say("failed to get doy: ", err)
                return ngx.null
        end

        ngx.say("get from redis.")
        return res
end

function set_to_redis(key, value)
        red:set_timeout(1000)
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
                ngx.say("failed to connect: ", err)
                return
        end

        local ok, err = red:set(key, value)
        if not ok then
                ngx.say("failed to set to redis: ", err)
                return
        end
        return ok
end

set_to_redis('dog', "Bob")
local rs = get_from_cache('dog')
ngx.say(rs)

測試:

[root@localhost ~]# curl localhost/cache
get from redis.
Bob
[root@localhost ~]# curl localhost/cache
get from cache.
Bob
[root@localhost ~]# curl localhost/cache
get from cache.
Bob

第一次從 redis中獲取,之後每次都從cache中獲取。

可使用 ab 測試一下rps(Requests per second):

 ab -n 1000 -c 100 -k http://127.0.0.1/cache

7. 解決緩存失效風暴 lua-resty-lock

緩存失效風暴是指緩存由於時間過時而失效時,會致使全部的請求都去訪問 後臺的redis或者mysql,而致使CPU性能即刻增加的現象。因此關鍵是當緩存失效時,用lock保證只有一個線程去訪問後臺的redis或者mysql,而後更新緩存。須要使用到 lua-resty-lock 模塊的加鎖、解鎖功能。

lua-resty-lock 文檔:https://github.com/openresty/lua-resty-lock

首先在nginx.conf 的 http 端下面加入指令:

lua_shared_dict ngx_cache 128m;     # cache
lua_shared_dict cache_lock 100k;    # lock for cache

而後在nginx.conf的server端中加入:

        location /cache_lock {
            content_by_lua_file lua/cache_lock.lua;
        }

cache_lock.lua代碼:

[root@localhost lua]# cat cache_lock.lua
local redis = require "resty.redis"
local red = redis:new()
local resty_lock = require "resty.lock"
local ngx_cache = ngx.shared.ngx_cache

function set_to_cache(key, value, exptime)
        if not exptime then
                exptime = 0
        end
        local succ, err, forcible = ngx_cache:set(key, value, exptime)
        return succ
end

function get_from_cache(key)
        local ngx_cache = ngx.shared.ngx_cache;
        local value = ngx_cache:get(key)
        if not value then       -- cache miss
                local lock = resty_lock:new("cache_lock")
                local elapsed, err = lock:lock(key)
                if not elapsed then
                        return fail("failed to acquire the lock: ", err)
                end

                value = get_from_redis(key)
                if not value then
                        local ok, err = lock:unlock()
                        if not ok then
                                return fail("failed to unlock: ", err)
                        end
                        ngx.say("no value found")
                        return
                end

                local ok, err = ngx_cache:set(key, value, 1)
                if not ok then
                        local ok, err = lock:unlock()
                        if not ok then
                                return fail("failed to unlock: ", err)
                        end
                        return faile("failed to update ngx_cache: ", err)
                end

                local ok, err = lock:unlock()
                if not ok then
                        return faile("failed to unlock: ", err)
                end

                return value
        end

        ngx.say("get from cache.")
        return value
end

function get_from_redis(key)
        red:set_timeout(1000)

        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
                ngx.say("failed to connect: ", err)
                return
        end

        local res, err = red:get(key)
        if not res then
                ngx.say("failed to get doy: ", err)
                return ngx.null
        end

        ngx.say("get from redis.")
        return res
end

function set_to_redis(key, value)
        red:set_timeout(1000)
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
                ngx.say("failed to connect: ", err)
                return
        end

        local ok, err = red:set(key, value)
        if not ok then
                ngx.say("failed to set to redis: ", err)
                return
        end
        return ok
end

set_to_redis('dog', "Bob")
local rs = get_from_cache('dog')
ngx.say(rs)
View Code

測試:

[root@localhost lua]# curl localhost/cache_lock
get from cache.
Bob
[root@localhost lua]# curl localhost/cache_lock
get from cache.
Bob

7. openresty 執行階段

nginx的執行階段分紅了不少個階段,因此第三方模塊就能夠在某個適當的階段加入一些處理。openresty進行了簡化成了7個階段:

7個階段的執行順序以下:

set_by_lua: 流程分支判斷,判斷變量初始哈

rewrite_by_lua: 用lua腳本實現nginx rewrite

access_by_lua: ip准入,是否能合法性訪問,防火牆

content_by_lua: 內存生成

header_filter_by_lua:過濾http頭信息,增長頭信息

body_filter_by_lua: 內容大小寫,內容加密

log_by_lua: 本地/遠程記錄日誌

可是其實咱們能夠只用 content_by_lua,全部功能都在該階段完成,也是能夠的。

相關文章
相關標籤/搜索