1. 程式人生 > >OpenResty 不完全指南

OpenResty 不完全指南

作者 | 黃超    

640?wx_fmt=jpeg

杏仁運維工程師,關注容器技術和自動化運維。

OpenResty 簡介

OpenResty® 是一個基於 Nginx 與 Lua 的高效能 Web 平臺。我們知道開發 Nginx 的模組需要用 C 語言,同時還要熟悉它的原始碼,成本和門檻比較高。國人章亦春把 LuaJIT VM 嵌入到了 Nginx 中,使得可以直接通過 Lua 指令碼在 Nginx 上進行程式設計,同時還提供了大量的類庫(如:lua-resty-mysql lua-resty-redis 等),直接把一個 Nginx 這個 Web Server 擴充套件成了一個 Web 框架,藉助於 Nginx 的高效能,能夠快速地構造出一個足以勝任 10K 乃至 1000K 以上單機併發連線的高效能 Web 應用系統。

Nginx 採用的是 master-worker 模型,一個 master 程序管理多個 worker 程序,worker 真正負責對客戶端的請求處理,master 僅負責一些全域性初始化,以及對 worker 進行管理。在 OpenResty 中,每個 worker 中有一個 Lua VM,當一個請求被分配到 worker 時,worker 中的 Lua VM 裡建立一個 coroutine(協程) 來負責處理。協程之間的資料隔離,每個協程具有獨立的全域性變數 _G

640?wx_fmt=png

OpenResty 處理請求流程

由於 Nginx 把一個請求分成了很多階段,第三方模組就可以根據自己的行為,掛載到不同階段處理達到目的。OpenResty 也應用了同樣的特性。不同的階段,有不同的處理行為,這是 OpenResty 的一大特色。OpenResty 處理一個請求的流程參考下圖(從 Request start 開始):

640?wx_fmt=png

指令使用範圍解釋
int_by_lua*init_worker_by_lua*http初始化全域性配置/預載入Lua模組
set_by_lua*server,server if,location,location if設定nginx變數,此處是阻塞的,Lua程式碼要做到非常快
rewrite_by_lua*http,server,location,location ifrewrite階段處理,可以實現複雜的轉發/重定向邏輯
access_by_lua*http,server,location,location if請求訪問階段處理,用於訪問控制
content_by_lua*location, location if內容處理器,接收請求處理並輸出響應
header_filter_by_lua*http,server,location,location if設定 heade 和 cookie
body_filter_by_lua*http,server,location,location if對響應資料進行過濾,比如截斷、替換
log_by_luahttp,server,location,location iflog階段處理,比如記錄訪問量/統計平均響應時間


更多詳情請參考官方文件

配置 OpenResty

OpenResty 的 Lua 程式碼是提現在 nginx.conf的配置檔案之中的,可以與配置檔案寫在一起,也可以把 Lua 指令碼放在一個檔案中進行載入:

內聯在 nginx.conf中:

server {
    ...
    location /lua_content {
         # MIME type determined by default_type:
         default_type 'text/plain';

         content_by_lua_block {
             ngx.say('Hello,world!')
         }
    }
    ....
}    

通過載入 lua 指令碼的方式:

server {
    ...
    location = /mixed {
         rewrite_by_lua_file /path/to/rewrite.lua;
         access_by_lua_file /path/to/access.lua;
         content_by_lua_file /path/to/content.lua;
     }
    ....
} 

OpenResty 變數的共享範圍

全域性變數

在 OpenResty 中,只有在 init_by_lua*init_worker_by_lua*階段才能定義真正的全域性變數。因為在其他階段,OpenResty 會設定一個隔離的全域性變量表,以免在處理過程中汙染了其他請求。即使在上述兩個階段可以定義全域性變數,也儘量避免這麼做。全域性變數能解決的問題,用模組變數也能解決,而且會更清晰,乾淨。

模組變數

這裡將定義在 Lua 模組中的變數稱為模組變數。Lua VM 會將 require進來的模組換成到package.loadedtable 裡,模組裡的變數都會被快取起來,在同一個 Lua VM下,模組中的變數在每個請求中是共享的,這樣就可以避免使用全域性變數來實現共享了,看下面一個例子:

nginx.conf

worker_processes  1;

...
location {
    ...
    lua_code_cache on;
    default_type "text/html";
    content_by_lua_file 'lua/test_module_1.lua'
}

lua/test_module_1.lua

local module1 = require("module1")

module1.hello()

lua/module1.lua

local count = 0
local function hello() 
    count = count + 1
    ngx.say("count: ", count)
end

local _M  = {
    hello = hello
}   

return _M

當通過瀏覽器訪問時,可以看到 count 輸出是一個遞增的,這也說明了在lua/module1.lua 的模組變數在每個請求中時共享的:

count: 1
count: 2
.....

另外,如果 worker_processes的數量大於 1 時呢,得到的結果可能就不一樣了。因為每個 worker 中都有一個 Lua VM 了,模組變數僅在同一個 VM 下,所有的請求共享。如果要在多個 Worker 程序間共享請考慮使用 ngx.shared.DICT 或如 Redis 儲存了。

本地變數

跟全域性變數,模組變數相對,我們這裡姑且把 *_by_lua*裡定義的變數稱為本地變數。本地變數僅在當前階段有效,如果需要跨階段使用,需要藉助ngx.ctx或者附加到模組變數裡。

這裡我們使用了 ngx.ctx表在三個不同的階段來傳遞使用變數foo

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 表。例如:

 location /sub {
     content_by_lua_block {
         ngx.say("sub pre: ", ngx.ctx.blah)
         ngx.ctx.blah = 32
         ngx.say("sub post: ", ngx.ctx.blah)
     }
 }

 location /main {
     content_by_lua_block {
         ngx.ctx.blah = 73
         ngx.say("main pre: ", ngx.ctx.blah)
         local res = ngx.location.capture("/sub")
         ngx.print(res.body)
         ngx.say("main post: ", ngx.ctx.blah)
     }
 }

訪問 GET /main 輸出:

main pre: 73
sub pre: nil  # 子請求中並沒有獲取到父請求的變數 $pre
sub post: 32
main post: 73

效能開關 lua_code_cache

開啟或關閉在 *_by_lua_file(如:set_by_lua_file,content_by_lua_file) 指令中以及 Lua 模組中 Lua 程式碼的快取。

若關閉,ngx_lua 會為每個請求建立一個獨立的 Lua VM,所有 *_by_lua_file指令中的程式碼將不會被快取到記憶體中,並且所有的 Lua 模組每次都會從頭重新載入。在開發模式下,這給我們帶來了不需要reloadnginx 就能除錯的便利性,但是在生成環境下,強烈建議開啟。若關閉,即使是一個簡單的 Hello World 都會慢上一個數量級(每次 IO 讀取和編譯消耗很大)。

但是,那些直接寫在 nginx.conf配置檔案中的*_by_lua_block指令下的程式碼不會在你編輯下實時更新,只有傳送HUP訊號給 Nginx 才能能夠重新。

小案例

通過 OpenResty + Redis 實現動態路由

Nginx 經常用來作為反向代理伺服器。通常情況下,我們將後端的服務配置在 Nginx 的 upstream中,當後端服務有變更時就去修改upstream中的配置再通過reload的方式使其生效。這個操作如果在後端服務經常發生變更的情況下,操作起來就會顯得有些繁瑣了。現在利用 Lua + Redis 的方式將upstream中的配置放在 Redis 中,以實現動態配置的效果。

架構圖

640?wx_fmt=png

原理:

在求請求訪問階段處理(access_by_lua*)通過指定的規則(這個規則根據自己的需求去設計)從 Redis 中去獲取相對應的後端服務地址去替換 Nginx 配置中的proxy_pass 的地址。

流程:

  1. 在 Nginx 配置中建立後端服務地址的變數 $backend_server

    server {
listen 80; server_name app1.example.com; location / { ... set $backend_server ''; } }

同時在 Redis 中存入後端服務的地址。

set app1 10.10.10.10:8080
  1. 使用 ngx_redis2模組來實現一個讀取 Redis 的介面。

    # GET /get?key=some_key
location = /get { internal; # 保護這個介面只執行內部呼叫 set_unescape_uri $key $arg_key; # this requires ngx_set_misc redis2_query get $key; redis2_pass foo.com:6379; # redis_server and port }

   2. 在求請求訪問階段處理利用 ngx.location.capture模組請求去上個階段定義的 Redis 介面,並將結果替換$backend_server

    location / {
        ...
        access_by_lua_block {
            local rds_key = "app1"
            # 從 redis 中獲取 key 為 app1 對應的 server_ip
            local res = ngx.location.capture('/get', { args = {key = rds_key}})
            # 解析 redis 結果
            local parser = require("redis.parser")
            local server, typ = parser.parse_reply(res.body)
            if typ ~= parser.BULK_REPLY or not server then
                ngx.log(ngx.ERR, "bad redis response: ", res.body)
                ngx.exit(500)
            end

            ngx.var.backend_server = server
        }
    }

    3.Nginx 轉發階段將請求轉發至後端服務。

    location / {
        ...
        access_by_lua_block {...};
        proxy_pass http://$backend_server;
    }

最後,推薦兩個基於 OpenResty 的比較實用的兩個開源專案:

  • 基於動態策略的灰度釋出系統 ABTestingGateway

  • 基於ngx_lua的web應用防火牆 ngx_lua_waf

參考

  • OpenResty Best Practices( https://legacy.gitbook.com/book/moonbingbing/openresty-best-practices/details )

  • lua-nginx-module( https://www.nginx.com/resources/wiki/modules/lua/ )

  • Nginx Lua Directives( https://github.com/openresty/lua-nginx-module#directives )

  • Nginx API for Lua( https://github.com/openresty/lua-nginx-module#nginx-api-for-lua )

  • Lua 簡明教程( https://coolshell.cn/articles/10739.html )

全文完

以下文章您可能也會感興趣:

我們正在招聘 Java 工程師,歡迎有興趣的同學投遞簡歷到 [email protected]

640?wx_fmt=png

杏仁技術站

長按左側二維碼關注我們,這裡有一群熱血青年期待著與您相會。

相關推薦

OpenResty 完全指南

作者 | 黃超    杏仁運維工程師,關注容器技術和自動化運維。OpenResty 簡介Open

Laravel(PHP)使用Swagger生成API文件完全指南 - 基本概念和環境搭建 - 簡書

在PHPer中,很多人聽說過Swagger,部分人知道Swagger是用來做API文件的,然而只有少數人真正知道怎麼正確使用Swagger,因為PHP界和Swagger相關的資料實在是太少了。所以鄙人斗膽一試,希望能以本文幫助到大家瞭解Swagger,從此告別成天用Word、Markdown折騰API文件的日

Laravel(PHP)使用Swagger生成API文檔完全指南 - 基本概念和環境搭建 - 簡書

function 閱讀 編程語言 文字 formdata 自動 tom dev 開始 在PHPer中,很多人聽說過Swagger,部分人知道Swagger是用來做API文檔的,然而只有少數人真正知道怎麽正確使用Swagger,因為PHP界和Swagger相關的資料實在是太少

Bullet物理引擎完全指南 Bullet Physics Engine not complete Guide

                前言    Bullet據稱為遊戲世界佔有率為第三的物理引擎,也是前幾大引擎目前唯一能夠找到的支援iPhone,開源,免費(Zlib協議,非常自由,且商業免費)的物理引擎,但是文件資料並不是很好,Demo雖然多,但是主要出於特性測試/展示的目的,會讓初學者無從看起,一頭霧水。我

高效使用Pycharm完全指南

定位 Search EverywhereCtrl+游標最近開啟的檔案目錄樹的“雷達”查詢補全 Tab萬能的Alt-EnterSurroud withEmmet編輯 BasicExtend Select

ctf工具完全指南

最近接連趕上了ssctf和hctf,對我來說算是一償夙願,總算從在網站上做做以前的題,到參加比賽,正式邁入圈內了。雖然學校內的氛圍不濃厚,但總算也能拉起一支能參賽的隊伍,第一次正式參賽的結果算是讓我挺滿意的。戰後是時候來個總結了。 經驗上的差距讓一些本來可以做的題目也

Chrome開發者工具完全指南(四、效能進階篇)

前言   Profiles面板功能的作用主要是監控網頁中各種方法執行時間和記憶體的變化,簡單來說它就是Timeline的數字化版本。它的功能選項卡不是很多(只有三個),操作起來比較前面的幾塊功能版本來說簡單,但是裡面的資料確很多,很雜,要弄懂它們需要花費一些時間。尤其是在記憶體快照中的各種龐雜的資料。在這篇

Chrome開發者工具完全指南(五、移動篇)

  前面介紹了Chrome開發者工具的大部分內容工具,現在介紹最後兩塊功能Audits和Console面板。一、Audits  Audits面板會針對目前網頁提出若干條優化的建議,這些建議分為兩大類,一類是網路載入效能,另一類是介面效能。首先開下它的主介面。  Audits面板的網路優化建議參照的是雅虎前端工

Chrome開發者工具完全指南(一、基礎功能篇)

  就算你不是一名前端開發工程師,相信你也不會對Chrome瀏覽器感到陌生。根據最新的一份(2015/06)的瀏覽器市場佔有率報告,Chrome近乎佔有瀏覽器天下的半壁江山。簡單、快捷使它成為了新時代人們的新寵。如果你是一名web開發人員,我推薦你使用Chrome。作為前端開發的"IDE",你只需要搭配一個編

Chrome開發者工具完全指南:(三、效能篇)

<!DOCTYPE html> <html> <head> <title></title> <style type="text/css"> div{ height: 20px; widows: 20p

Chrome開發者工具完全指南(二、進階篇)

function a () { b(); } function b() { c(); } function c() { //在該處斷點,檢視call stack } a->b->c. call stack 從上到下的順序就是 c

Chrome 控制檯完全指南

Chrome的開發者工具已經強大到沒朋友的地步了,特別是其功能豐富介面友好的console,使用得當可以有如下功效: 更高「逼格」更快「開發除錯」更強「進階級的Frontender」 Bug無處遁形「Console大法好」 console.log 大家都會用log,但鮮有人很好地利用console.err

Apache Kylin 部署之完全指南

1. 引言 Apache Kylin(麒麟)是由eBay開源的分散式分析引擎,提供Hadoop之上的SQL查詢介面及多維分析(OLAP)能力以支援超大規模資料。底層儲存用的是HBase,資料輸入與cube building用的是Hive,中間資料儲存用的是HDFS。搭建環境: Kylin version =

JNI完全指南

JNI不完全指南 1.概述 JNI做為對JVM的補充,可以完成一些遊離於JVM之外的程式碼,完成一些OS嚴重依賴的功能,比如你想自己實現基於IMCP的ping(如果問為什麼,那麼請重新學習一下IP/TCP/Socket)。 JNI包括Java程式碼和Native程式碼,所謂的

webpack優化完全指南

基礎篇 最基本的一個webpack配置 const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extra

完全指南:程式設計師怎麼找海外工作

本文主要針對程式設計師群體,希望提供一些尋找海外工作機會的建議。 留學籤,工作籤還是技術移民? 想要合法地離開中國抵達海外長期生活,不外乎三種途徑: 留學簽證 工作簽證 技術移民 一一瞭解上面這些途徑的基本資訊,然後結合你自身情況,相信可以做出合適的後續規劃。至於做功課的難度,應該不會超

Android程式設計規範完全指南

1. 命名規則 1.1 類名,介面名: 以大寫開頭,如果一個類的類名由多個單片語成,所有單詞的首字母必須大寫,單詞儘量寫全稱,不要簡寫,除非約定俗成的名字,例如:URL,RTMP,RTSP 這些廣泛使用的專有名詞,可以全部大寫,也可以首字母大寫。 例如:HttpRe

前端效能優化完全指南

開發十年,就只剩下這套架構體系了! >>>   

重構完全指南

首發公眾號《andyqian》,期待你的關注!   前言   程式設計師在職業生涯中,不可避

前端勸退預警:JavaScript 工具鏈完全指南

![宇宙中最重的物質](https://user-gold-cdn.xitu.io/2020/3/11/170c7474bca121fb?w=958&h=576&f=png&s=232767) 經過這麼多年的發展,JavaScript 早已經不是當年那個不太起眼的指令碼語言。如今的 JavaScript 可