OpenResty一些重要特性的整理

OpenResty (簡稱OR) 是一個基於 Nginx 與 Lua 的高性能 Web 平臺,其內部集成了大量精良的Lua Api,第三方模塊以及經常使用的依賴項,基於這些能力,咱們能夠利用OR快速方便的搭建可以處理超高併發的,極具動態性和擴展的Web應用、Web服務和動態網關。html

這篇文章選擇OR中具備重要意義的一些模塊、命令、Api和框架介紹以下:mysql

基礎的Http處理

OR 做爲一款 Web 服務器,提供瞭如:nginx

  • ngx.print() | ngx.say()
  • ngx.req.get_headers()
  • ngx.resp.get_headers()
  • ngx.header.HEADER
  • ngx.exit()

    等函數和方法控制http請求的輸入和輸出,咱們能夠經過這些 api 靈活的控制 Web Server 中包括輸入輸出,轉發到上游或者發起子請求在內的各個環節。git

ngx.print

同ngx.say(), 經過該 api 能夠指定返回的請求的消息體,其中每一次函數的調用,都是在nginx的輸出鏈上增長了一個結點,因此咱們能夠屢次調用該函數而不用擔憂後者會覆蓋前者,如:github

location =/hello {
    content_by_lua_block {
        ngx.print("Hello")      
        ngx.print(" ")      
        ngx.print("World!")      
    }
}

GET /hello 將會獲得以下結果
======
Hello World!

請求頭和響應頭控制

ngx.exit() 和 ngx.status

ngx.exit,經過該 api能夠指定請求響應體的 http status, 如:redis

location =/error {
    content_by_lua_block {
        return ngx.exit(500)    
    }
}

GET /error
======
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>openresty</center>
</body>
</html>

當咱們訪問 /error 接口的時候,能夠獲得一個默認的 500 頁面,如上所示,固然咱們能夠經過ngx.status + ngx.print 的組合方式獲得自定義的錯誤頁面,如:sql

location =/error {
    content_by_lua_block {
        ngx.status = 500
        ngx.print("error here!")
        return ngx.exit(500)    
    }
}

GET /erro
======
error here!

以上方式一樣適用於其它錯誤,如 40四、50二、503 等頁面,經過這樣的方式咱們能夠更靈活的控制當這些錯誤,從而返回更友好的頁面或者其它輸出,固然你也可用使用 nginx 原生的 error_page 指令對錯誤頁面進行控制,只是靈活性相對差一些數據庫

ngx.timer.at

在OR內部經過 nginx 事件機制實現的定時器,咱們能夠經過它來實現延遲運行的任務邏輯,甚至於經過一些特殊的調用方法實現定時任務的功能,好比:json

local delay = 5
 local handler
 handler = function (premature)
     -- do some routine job in Lua just like a cron job
     if premature then
         return
     end
     local ok, err = ngx.timer.at(delay, handler)
     if not ok then
         ngx.log(ngx.ERR, "failed to create the timer: ", err)
         return
     end
 end

 local ok, err = ngx.timer.at(delay, handler)
 if not ok then
     ngx.log(ngx.ERR, "failed to create the timer: ", err)
     return
 end

能夠看到咱們給ngx.timer.at的第二個參數是這個函數體自己,經過這樣的寫法,咱們能夠實現每一個delay的間隔執行一次handler
固然這種「不友好」的技巧在v0.10.9這個版本以後被ngx.timer.every給「優化」了(上述技巧依舊有效)。api

固然除此以外,這個API還有其它「非凡」的意義。

OR的各個API其實都有其「生命週期」或者說做用域,好比其中很是重要的cosocket,它的做用域以下

rewrite_by_lua , access_by_lua, content_by_lua , ngx.timer., ssl_certificate_by_lua , ssl_session_fetch_by_lua

也就是說咱們只能在上述階段使用 cosocket,因爲不少第三方組件的實現都是基於它,好比

resty-redis,resty-memcached,resty-mysql

因此 cosocket 的做用域就等同於上述這些依賴它的模塊的做用域

咱們再來看看ngx.timer.at的做用域

init_worker_by_lua , set_by_lua, rewrite_by_lua , access_by_lua, content_by_lua*,
header_filter_by_lua , body_filter_by_lua, log_by_lua , ngx.timer., balancer_by_lua*,
ssl_certificate_by_lua , ssl_session_fetch_by_lua, ssl_session_store_by_lua*

這意味着,若是咱們但願在 cosocket 不支持的做用域內使用它,那麼咱們能夠經過 ngx.timer.at 的方式橋接,完成「跨域」, 好比:

init_worker_by_lua_block {
    local handler = function()
        local redis = require "resty.redis"
        redc = redis:new(anyaddre)
        redc:set('foo', 'bar')
    end
    
    ngx.timer.at(0, handler)
}

這就是前文所說的,這個API的」非凡「意義,固然我更指望的是 cosocket 可以支持更多的做用域 ^_^!

跨進程共享 ngx.shared.DICT

一般來講,咱們會將一些系統的配置寫入lua代碼中,經過require的方式在不一樣worker之間共享,這種看起來像是跨worker的共享方式實際上是利用了」一個模塊只加載一次「的特性(lua_code_cache on), 可是這種方式只適合於一些只讀數據的共享,並不適合一些可寫的(嚴格來講, 能夠經過一些技術手段實現這個級別的緩存,因爲能實現table類型的緩存,它的效率並不差,這裏不作討論),或者動態變化的數據,好比從關係型數據庫、kv數據庫,上游、第三方服務器獲取的數據的緩存。

ngx.shared.DICT 方法集提供瞭解決跨 worker 且自帶互斥鎖的共享數據方案,配合 cjson 或者 resty-dkjson 這樣的 json 解析庫,極大豐富了這個API集合的應用空間,須要注意的是:

  1. 它須要在nginx的配置文件中被預先定義好大小且不可動態擴容, 如: lua_shared_dict cache 16m;
  2. 儘量的使用 cjson 而非 dkjson, 它們之間效率相差 50 倍以上;
  3. 它是一個內存數據集,並不具有持久化的能力;
  4. 寧願定義多個容量更小的存儲體,而非一個更大的;

ssl_certificate_by_lua_xxx

nginx 是支持SNI的,因此咱們能夠在一臺主機上爲同一個IP、不一樣域名的「租戶」綁定不一樣的證書,你只須要在nginx的配置中設置多個server block, 併爲每一個 block 配置server_name以及指定其證書和私鑰便可;

http
{
    server {
        listen       443 ssl;
        server_name  www.foo.com;
 
        ssl_certificate      cert/foo.pem;
        ssl_certificate_key  cert/foo.key;
 
 
        location / {
            proxy_pass http://foo;
        }
    }


    server {
        listen       443 ssl;
        server_name  www.bar.com;
 
        ssl_certificate      cert/bar.pem;
        ssl_certificate_key  cert/bar.key;
 
 
        location / {
            proxy_pass http://bar;
        }
    }
}

雖然上述方式可行,可是在更新證書的時候就會顯得很是麻煩,特別是當你擁有多個主機且每一個主機上不止一個證書的時候,證書的逐個替換,nginx 服務的重啓都是很是麻煩且容易出錯的步驟,即使是經過自動化的代碼完成上述步驟,可是多個主機的更換證書的協調性問題依然不容忽視。

ssl_certificate_by_lua_xxx 確實提供了一種更優雅的方式

前文提到的 cosocket 在 ssl_certificate_by_lua_xxx 階段是被支持的,因此咱們能夠在這個階段動態的爲終端加載其對應的證書,如:

server {
    listen 443 ssl;
    server_name   test.com;

    # 雖然咱們能夠動態的加載證書,可是爲了不nginx報錯,這裏須要配置用於佔位的證書和私鑰
    ssl_certificate placeholder.crt;
    ssl_certificate_key placeholder.key;
    
    ssl_certificate_by_lua_block {
         local ssl = require "ngx.ssl" 
         
         -- 清理掉當前的證書,爲後續加載證書作準備
         local ok, err = ssl.clear_certs() 
            
         if not ok then
             ngx.log(ngx.ERR, "clear current cert err:", err)
             return ngx.exit(500)
         end
         
         -- x509 格式的證書有兩種存儲格式 PEM 和 DER,這裏只描述PEM格式
         
         -- 獲取 Server Name Indication 簡稱(SNI)
         local sni = ssl.server_name()
         
         -- 這裏咱們假設已經經過cosocket實現了一個從數據庫獲取證書的函數
         -- 該函數以sni爲索引查詢對應的證書並返回
         local cert, err = get_pem_format_cert_by_server_name(sni)
         
         -- cert_of_x509 是一個cdata類型的數據
         local cert_of_x509, err = ssl.parse_pem_cert(cert)
         
         local ok, err = ssl.set_cert(cert_of_x509)
         
         if not ok then
             ngx.log(ngx.ERR, "set cert failed, err:", err)
             return ngx.exit(500)
         end
         
         --- 這裏還須要設置對應的私鑰,相關函數請參考以下
         
    }
}

ssl.parse_pem_cert
ssl. parse_pem_priv_key

經過上述方式,咱們能夠經過直接修改存儲介質中某個域名對應的證書,從而實現多節點的證書和私鑰替換,更進階一些,咱們甚至能夠經過一些技術手段讓「租戶或者用戶」本身保留私鑰,即實現所謂的key-less,這個咱們後面文章再講。

ssl_session_fetch_by_lua_xxx 和 ssl_session_store_by_lua_xxx

待續

Web 框架

待續

相關文章
相關標籤/搜索