使用 Nginx + Lua(OpenResty)開發高效能Web應用
在網際網路公司,Nginx可以說是標配元件,但是主要場景還是負載均衡、反向代理、代理快取、限流等場景;而把Nginx作為一個Web容器使用的還不是那麼廣泛。Nginx的高效能是大家公認的,而Nginx開發主要是以C/C++模組的形式進行,整體學習和開發成本偏高;如果有一種簡單的語言來實現Web應用的開發,那麼Nginx絕對是把好的瑞士軍刀;目前Nginx團隊也開始意識到這個問題,開發了nginxScript:可以在Nginx中使用JavaScript進行動態配置一些變數和動態指令碼執行;而目前市面上用的非常成熟的擴充套件是由章亦春將Lua和Nginx粘合的ngx_lua模組,並且將Nginx核心、LuaJIT、ngx_lua模組、許多有用的Lua庫和常用的第三方Nginx模組組合在一起成為OpenResty,這樣開發人員就可以安裝OpenResty,使用Lua編寫指令碼,然後部署到Nginx Web容器中執行。從而非常輕鬆就能開發出高效能的Web服務。
接下來我們就認識下Nginx、Lua、ngx_lua模組和ngx_lua到底能開發哪些型別的web應用。
一、ngx_lua簡介
1、Nginx優點
Nginx設計為一個主程式多個工作程式的工作模式,每個程式是單執行緒來處理多個連線,而且每個工作程式採用了非阻塞I/O來處理多個連線,從而減少了執行緒上下文切換,從而實現了公認的高效能、高併發;因此在生成環境中會通過把CPU繫結給Nginx工作程式從而提升其效能;另外因為單執行緒工作模式的特點,記憶體佔用就非常少了。
Nginx更改配置重啟速度非常快,可以毫秒級,而且支援不停止Nginx進行升級Nginx版本、動態過載Nginx配置。
Nginx模組也是非常多,功能也很強勁,不僅可以作為http負載均衡,Nginx釋出1.9.0版本還支援TCP負載均衡,還可以很容易的實現內容快取、web伺服器、反向代理、訪問控制等功能。
2、Lua的優點
Lua是一種輕量級、可嵌入式的指令碼語言,這樣可以非常容易的嵌入到其他語言中使用。另外Lua提供了協程併發,即以同步呼叫的方式進行非同步執行,從而實現併發,比起回撥機制的併發來說程式碼更容易編寫和理解,排查問題也會容易。Lua還提供了閉包機制,函式可以作為First Class Value 進行引數傳遞,另外其實現了標記清除垃圾收集。
因為Lua的小巧輕量級,可以在Nginx中嵌入Lua VM,請求的時候建立一個VM,請求結束的時候回收VM。
3、什麼是ngx_lua
ngx_lua是Nginx的一個模組,將Lua嵌入到Nginx中,從而可以使用Lua來編寫指令碼,這樣就可以使用Lua編寫應用指令碼,部署到Nginx中執行,即Nginx變成了一個Web容器;這樣開發人員就可以使用Lua語言開發高效能Web應用了。
ngx_lua提供了與Nginx互動的很多的API,對於開發人員來說只需要學習這些API就可以進行功能開發,而對於開發web應用來說,如果接觸過Servlet的話,其開發和Servlet類似,無外乎就是知道接收請求、引數解析、功能處理、返回響應這幾步的API是什麼樣子的。
4、開發環境
我們可以使用OpenResty來搭建開發環境,OpenResty將Nginx核心、LuaJIT、許多有用的Lua庫和Nginx第三方模組打包在一起;這樣開發人員只需要安裝OpenResty,不需要了解Nginx核心和寫複雜的C/C++模組就可以,只需要使用Lua語言進行Web應用開發了。
如何安裝可以參考《跟我學Nginx+Lua開發》。
5、OpenResty生態
OpenResty提供了一些常用的ngx_lua開發模組:如
lua-resty-memcached
lua-resty-mysql
lua-resty-redis
lua-resty-dns
lua-resty-limit-traffic
lua-resty-template
這些模組涉及到如mysql資料庫、redis、限流、模組渲染等常用功能元件;另外也有很多第三方的ngx_lua元件供我們使用,對於大部分應用場景來說現在生態環境中的元件已經足夠多了;如果不滿足需求也可以自己去寫來完成自己的需求。
6、場景
理論上可以使用ngx_lua開發各種複雜的web應用,不過Lua是一種指令碼/動態語言,不適合業務邏輯比較重的場景,適合小巧的應用場景,程式碼行數保持在幾十行到幾千行。目前見到的一些應用場景:
web應用:會進行一些業務邏輯處理,甚至進行耗CPU的模板渲染,一般流程:mysql/redis/http獲取資料、業務處理、產生JSON/XML/模板渲染內容,比如京東的列表頁/商品詳情頁;
接入閘道器:實現如資料校驗前置、快取前置、資料過濾、API請求聚合、AB測試、灰度釋出、降級、監控等功能,比如京東的交易大Nginx節點、無線部門正在開發的無線閘道器、單品頁統一服務、實時價格、動態服務;
Web防火牆:可以進行IP/URL/UserAgent/Referer黑名單、限流等功能;
快取伺服器:可以對響應內容進行快取,減少到後端的請求,從而提升效能;
其他:如靜態資源伺服器、訊息推送服務、縮圖裁剪等。
二、基於Nginx+Lua的常用架構模式
1、負載均衡
如上圖,我們首先通過LVS+HAProxy將流量轉發給核心Nginx 1和核心Nginx 2,即實現了流量的負載均衡,此處可以使用如輪訓、一致性雜湊等排程演算法來實現負載的轉發;然後核心Nginx會根據請求特徵如“Host:item.jd.com”,轉發給相應的業務Nginx節點如單品頁Nginx 1。此處為什麼分兩層呢?
1、核心Nginx層是無狀態的,可以在這一層實現流量分組(內網和外網隔離、爬蟲和非爬蟲流量隔離)、內容快取、請求頭過濾、故障切換(機房故障切換到其他機房)、限流、防火牆等一些通用型功能;
2、業務Nginx如單品頁Nginx,可以在在業務Nginx實現業務邏輯、或者反向代理到如Tomcat,在這一層可以實現內容壓縮(放在這一層的目的是減少核心Nginx的CPU壓力,將壓力分散到各業務Nginx)、AB測試、降級;即這一層的Nginx跟業務有關聯,實現業務的一些通用邏輯。
不管是核心Nginx還是業務Nginx,都應該是無狀態設計,可以水平擴容。
業務Nginx一般會把請求直接轉發給後端的業務應用,如Tomcat、PHP,即將請求內部轉發到相應的業務應用;當有的Tomcat出現問題了,可以在這一層摘掉;或者有的業務路徑變了在這一層進行rewrite;或者有的後端Tomcat壓力太大也可以在這一層降級,減少對後端的衝擊;或者業務需要灰度釋出時也可以在這一層Nginx上控制。
2、單機閉環
所謂單機閉環即所有想要的資料都能從本伺服器直接獲取,在大多數時候無需通過網路去其他伺服器獲取。
如上所示,主要有三種應用模式:
2.1、第一張圖應用場景是Nginx應用誰也不依賴,比如我們的Cookie白名單應用,其目的是不在白名單中的Cookie將被清理,防止大家隨便將Cookie寫到jd.om根下;大家訪問http://www.jd.com時,會看到一個http://ccc.jd.com/cookie_check的請求用來清理Cookie的;對於這種應用非常簡單,不需要依賴資料來源,直接單應用閉環即可。
2.2、第二張圖,是讀取本機檔案系統,如靜態資源合併:比如訪問http://item.jd.com/1856584.html,檢視原始碼會發現【<link type=”text/css” rel=”stylesheet” href=”//misc.360buyimg.com/jdf/1.0.0/unit/??ui-base/1.0.0/ui-base.css,shortcut/2.0.0/shortcut.css,global-header/1.0.0/global-header.css,myjd/2.0.0/myjd.css,nav/2.0.0/nav.css,shoppingcart/2.0.0/shoppingcart.css,global-footer/1.0.0/global-footer.css,service/1.0.0/service.css”/>】這種請求,即多個請求合併為一個發給服務端,服務端進行了檔案資源的合併;
目前有成熟的Nginx模組如nginx-http-concat進行靜態資源合併;因為我們使用了OpenResty,那麼我們完全可以使用Lua編寫程式實現該功能,比如已經有人寫了nginx-lua-static-merger來實現這個功能。
還一些業務型應用場景如下圖所示
商品頁面是由商品框架和其他維度的頁面片段(麵包屑、相關分類、商家資訊、規格引數、商品詳情)組成;或者首頁是由首頁框架和一些頁面片段(分類、輪播圖、樓層1、樓層N)組成;分維度是因為不同的維度是獨立變化的。對於這種靜態內容但是需要進行框架內容嵌入的方式,Nginx自帶的SSI(Server Side Include)可以很輕鬆的完成;也可以使用Lua程式更靈活的完成(讀取框架、讀取頁面片段、合併輸出)。
比如商品頁面的架構我們可以這樣:
首先接收到商品變更訊息,商品頁面同步Worker會根據訊息維度生成相關的頁面推送到Nginx伺服器;Nginx應用再通過SSI輸出。目前京東商品詳情頁沒有再採用這種架構,具體架構可以參考《構建需求響應式億級商品詳情頁》。
對於首頁的架構是類似的,因為其特點(框架變化少,樓層變化較頻繁)和個性化的要求,樓層一般實現為非同步載入。
2.3、 第三張圖和第二張圖的不同處是不再直接讀取檔案系統,而是讀取本機的Redis或者Redis叢集或者如SSDB這種持久化儲存或者其他儲存系統都是可以的,比如直接說的商品頁面可以使用SSDB進行儲存實現。檔案系統一個很大的問題是當多臺伺服器時需要Worker去寫多臺伺服器,而這個過程可以使用SSDB的主從實現。
此處可以看到,不管是圖二還是圖三架構,都需要Worker去進行資料推送;假設本機資料丟了可怎麼辦?因此實際大部分應用不會是完全單機閉環的,而是會採用如下架構:
即首先讀本機,如果沒資料會回源到相應的Web應用從資料來源拉取原始資料進行處理。這種架構的大部分場景本機都可以命中資料,只有很少一部分情況會回源到Web應用。
如京東的實時價格/動態服務就是採用類似架構。
3、分散式閉環
單機閉環會遇到如下兩個主要問題: 1、資料不一致問題(比如沒有采用主從架構導致不同伺服器資料不一致);2、遇到儲存瓶頸(磁碟或者記憶體遇到了天花板)。
解決資料不一致的比較好的辦法是採用主從或者分散式集中儲存;而遇到儲存瓶頸就需要進行按照業務鍵進行分片,將資料分散到多臺伺服器。
如採用如下架構,按照尾號將內容分佈到多臺伺服器。
即第一步先讀取分散式儲存(JIMDB是京東的一個分散式快取/儲存系統,類似於Redis);如果不命中則回源到Tomcat叢集(其會呼叫資料庫、服務匯流排獲取相關資料)來獲取相關資料。可以參考《構建需求響應式億級商品詳情頁》來獲取更詳細的架構實現。
JIMDB叢集會進行多機房主從同步,各自機房讀取自己機房的從JIMDB叢集,如下圖
4、接入閘道器
接入閘道器也可以叫做接入層,即接收到流量的入口,在入口我們可以進行如下事情:
4.1、核心接入Nginx會做如下事情:
1、動態負載均衡;1、普通流量走一致性雜湊,提升命中率;熱點流量走輪訓減少單伺服器壓力;2、根據請求特徵將流量分配到不同分組並限流(爬蟲、或者流量大的IP);3、動態流量(動態增加upstream或者減少upstream或者動態負載均衡)可以使用balancer_by_lua或者微博開源的upsync;
2、防DDOS攻擊限流:可以將請求日誌推送到實時計算叢集,然後將需要限流的IP推送到核心Nginx進行限流;
3、非法請求過濾:比如應該有Referer卻沒有,或者應該帶著Cookie卻沒有Cookie;
4、請求聚合:比如請求的是http://c.3.cn/proxy?methods=a,b,c,核心接入Nginx會在服務端把Nginx併發的請求並把結果聚合然後一次性吐出;
5、請求頭過濾:有些業務是不需要請求頭的,因此可以在往業務Nginx轉發時把這些資料過濾掉;
6、快取服務:使用Nginx Proxy Cache實現內容頁面的快取;
4.2、業務Nginx會做如下事情:
1、快取:對於讀服務會使用大量的快取來提升效能,我們在設計時主要有如下快取應用:首先讀取Nginx本地快取 Shared Dict或者Nginx Proxy Cache,如果有直接返回內容給使用者;如果本地快取不命中,則會讀取分散式快取如Redis,如果有直接返回;如果還是不命中則回源到Tomcat應用讀取DB或呼叫服務獲取資料。另外我們會按照維度進行資料的快取。
2、業務邏輯:我們會進行一些資料校驗/過濾邏輯前置(如商品ID必須是數字)、業務邏輯前置(獲取原子資料,然後在Nginx上寫業務邏輯)。
3、細粒度限流:按照介面特徵和介面吞吐量來實現動態限流,比如後端服務快扛不住了,那我們就需要進行限流,被限流的請求作為降級請求處理;通過lua-resty-limit-traffic可以通過程式設計實現更靈活的降級邏輯,如根據使用者、根據URL等等各種規則,如降級了是讓使用者請求等待(比如sleep 100ms,這樣使用者請求就慢下來了,但是服務還是可用)還是返回降級內容。
4、降級:降級主要有兩種:主動降級和被動降級;如請求量太大扛不住了,那我們需要主動降級;如後端掛了或者被限流了或者後端超時了,那我們需要被動降級。降級方案可以是:1、返回預設資料如庫存預設有貨;2、返回靜態頁如預先生成的靜態頁;3、部分使用者降級,告訴部分使用者等待下再操作;4、直接降級,服務沒資料,比如商品頁面的規格引數不展示;5、只降級回源服務,即可以讀取快取的資料返回,實現部分可用,但是不會回源處理;
5、AB測試/灰度釋出:比如要上一個新的介面,可以通過在業務Nginx通過Lua寫複雜的業務規則實現不同的人看到不同的版本。
6、服務質量監控:我們可以記錄請求響應時間、快取響應時間、反向代理服務響應時間來詳細瞭解到底哪塊服務慢了;另外記錄非200狀態碼錯誤來了解服務的可用率。
京東的交易大Nginx節點、無線部門正在開發的無線Nginx閘道器、和單品頁統一服務都是接入閘道器的實踐,而單品頁統一服務架構可以參考《京東商品詳情頁服務閉環實踐》。
5、Web應用
此處所說的Web應用指的是頁面模板渲染型別應用或者API服務型別應用;比如京東列表頁/商品詳情頁就是一個模板渲染型別的應用,核心業務邏輯都是使用Lua寫的,部署到Nginx容器。目前核心業務程式碼行數有5000多行,模板頁面有2000多行,涉及到大量的計算邏輯,效能資料可以參考《構建需求響應式億級商品詳情頁》。
整體處理過程和普通Web應用沒什麼區別:首先接收請求並進行解析;然後讀取JIMDB叢集資料、如果沒有則回源到Tomcat獲取;然後進行業務邏輯處理;渲染模板;將響應內容返回給使用者。
三、如何使用Nginx+Lua開發Web應用
開發一個Web應用我們需要從專案搭建、功能開發、專案部署幾個層面完成。
3.1、專案搭建
/export/App/nginx-app -------bin(指令碼) ------------start.sh ------------stop.sh -------config(配置檔案) ------------nginx.conf ------------domain ----------------nginx_product.conf ------------resources.properties -------lua(業務程式碼) ------------init.lua ------------product_controller.lua -------template(模板) --------------prodoct.html -------lualib(公共Lua庫) ------------jd ----------------product_util.lua ----------------product_data.lua ------------resty ----------------redis.lua ----------------template.lua
整個專案結構從啟停指令碼、配置檔案、公共元件、業務程式碼、模板程式碼幾塊進行劃分。
1、啟停指令碼
啟停指令碼放在專案目錄/export/App/nginx-app/bin/下。
start.sh是啟動和更新指令碼,即如果nginx沒有啟動則啟動起來,否則reload:
if nginx沒啟動 then sudo /export/servers/nginx/sbin/nginx -t -c /export/App/nginx-app/config/nginx.conf sudo /export/servers/nginx/sbin/nginx -c /export/App/nginx-app/config/nginx.conf else sudo /export/servers/nginx/sbin/nginx -t sudo /export/servers/nginx/sbin/nginx -s reload end
stop.sh是停止Nginx指令碼:
sudo /export/servers/nginx/sbin/nginx -s quit
2、配置檔案
配置檔案放在/export/App/nginx-app/config目錄下,包括了nginx.conf配置檔案、nginx專案配置檔案和資源配置檔案。
nginx.confg配置檔案
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type text/html; #gzip相關 #超時時間 #日誌格式 #反向代理配置 #lua依賴路徑 lua_package_path "/export/App/nginx-app/lualib/?.lua;;"; lua_package_cpath "/export/App/nginx-app/lualib/?.so;;"; #server配置 include /export/App/nginx-app/config/domains/*; #初始化指令碼 init_by_lua_file "/export/App/nginx-app/lua/init.lua"; }
對於nginx.conf會進行一些通用的配置,如工作程式數、超時時間、壓縮、日誌格式、反向代理等相關配置;另外需要指定如下配置:
lua_package_path、lua_package_cpath指定我們依賴的通用Lua庫從哪裡載入;
include /export/App/nginx-app/config/domains/*:用於載入server相關的配置,此處通過*可以在一個nginx下指定多個server配置;
init_by_lua_file “/export/App/nginx-app/lua/init.lua”:執行專案的一些初始化配置,比如載入配置檔案。
nginx專案配置檔案
/export/App/nginx-app/config/domains/nginx_product.conf用於配置當前web應用的一些server相關的配置:
#upstream upstream item_http_upstream { server 192.168.1.1 max_fails=2 fail_timeout=30s weight=5; server 192.168.1.2 max_fails=2 fail_timeout=30s weight=5; } #快取 lua_shared_dict item_local_shop_cache 600m; server { listen 80; server_name item.jd.com item.jd.hk; #模板檔案從哪載入 set $template_root "/export/App/nginx-app/template "; #url對映 location ~* "^/product/(\d+)\.html$" { rewrite /product/(.*) http://item.jd.com/$1 permanent; } location ~* "^/(\d{6,12})\.html$" { default_type text/html; charset gbk; lua_code_cache on; content_by_lua_file "/export/App/nginx-app/lua/product_controller.lua"; } }
我們需要指定如upstream、共享字典配置、server配置、模板檔案從哪載入、url對映,比如我們訪問http://item.jd.com/1856584.html將交給/export/App/nginx-app/lua/product_controller.lua處理;也就是說我們專案的入口就有了。
資源配置檔案resources.properties包含了我們的一些比如開關的配置、快取伺服器地址的配置等等。
3、業務程式碼
/export/App/nginx-app/lua/目錄裡存放了我們的lua業務程式碼,init.lua用於讀取如resources.properties來進行一些專案初始化;product_controller.lua可以看成Java Web中的Servlet,接收、處理、響應使用者請求。
4、模板
模板檔案放在/export/App/nginx-app/template/目錄下,使用相應的模板引擎進行編寫頁面模板,然後渲染輸出。
5、公共Lua庫
存放了一些如redis、template等相關的公共Lua庫,還有一些我們專案中通用的工具庫如product_util.lua。
到此一個簡單的專案的結構就介紹完了,對於開發一個專案來說還會牽扯到分模組等工作,不過對於我們這種Lua應用來說,建議不要過度抽象,儘量小巧即可。
3.2、功能開發
接下來就需要使用相應的API來實現我們的業務了,比如product_controller.lua:
--載入Lua模組庫 local template = require("resty.template") --1、獲取請求引數中的商品ID local skuId = ngx.req.get_uri_args()["skuId"]; --2、呼叫相應的服務獲取資料 local data = api.getData(skuId) --3、渲染模板 local func = template.compile("product.html") local content = func(data) --4、通過ngx API輸出內容 ngx.say(content)
開發完成後將專案部署到測試環境,執行start.sh啟動nginx然後進行測試。
詳細的開發過程和API的使用,請參考《跟我學Nginx+Lua開發》。此處不做具體編碼實現。
參考原始碼:Nginx+Lua(OpenResty) HelloWorld
四、基於Nginx+Lua的常用功能總結
到此我們對於Nginx開發已經有了一個整體的認識,對於Nginx粘合Lua來開發應用可以說是一把鋒利的瑞士軍刀,可以幫我們很容易的解決很多問題,可以開發Web應用、接入閘道器、API閘道器、訊息推送、日誌採集等應用,不過個人認為適合開發業務邏輯單一、核心程式碼行數較少的應用,不適合業務邏輯複雜、功能繁多的業務型或者企業級應用;最後我們總結下基於Nginx+Lua的常用架構模式中一些常見實踐和場景:
動態負載均衡;
防火牆(DDOS、IP/URL/UserAgent/Referer黑名單、防盜鏈等);
限流;
降級;
AB測試/灰度釋出;
多級快取模式;
服務端請求聚合;
服務質量監控。
一些問題
1、在開發nginx應用時使用UTF-8編碼可以減去很多麻煩;
2、GBK轉碼解碼時使用GB18030,否則一些特殊字元會出現亂碼;
3、cjson庫對於如\uab1這種錯誤的unicode轉碼會失敗,可以使用純Lua編寫的dkjson;
4、社群版nginx不支援upstream的域名動態解析;可以考慮proxy_pass http://p.3.local/prices/mgets$is_args$args,然後配合resolver來實現;或者在lua中進行http呼叫;如果DNS遇到效能瓶頸可以考慮在本機部署如dnsmasq來快取;或者考慮使用balancer_by_lua功能實現動態upstream;
5、為響應新增處理伺服器IP的響應頭,方便定位問題;
6、根據業務設定合理的超時時間;
7、走CDN的業務當發生錯誤時返回的500/503/302/301等非正常響應不要設定快取。
相關文章
- 用 Nginx + Lua(OpenResty) 開發高效能 Web 應用NginxRESTWeb
- 編譯安裝基於nginx與lua的高效能web平臺-openresty編譯NginxWebREST
- openresty使用lua操作mysqlRESTMySql
- nginx+lua(OpenResty),實現訪問限制NginxREST
- Node助力Web應用開發——在新的開發平臺,打造高效能Web應用Web
- 安裝lua和openrestyREST
- 開始使用 Python 開發 Web 應用PythonWeb
- Nginx+Lua開發環境搭建Nginx開發環境
- 開發Web應用Web
- 高效能iOS應用開發iOS
- 安裝Nginx+Lua開發環境Nginx開發環境
- 使用 Flutter 開發簡單的 Web 應用FlutterWeb
- 使用Lua和OpenResty搭建驗證碼伺服器REST伺服器
- Lua OpenResty容器化(考古歷程)REST
- 使用JavaServer Pages2.0開發Web應用 (轉)JavaServerWeb
- OpenResty + Lua 動態增加 Zuul 節點RESTZuul
- openresty及lua的隨機函式REST隨機函式
- OpenResty debugger: lua-resty-replREST
- Web高效能開發總結Web
- 使用Nginx將大模型Web應用部署到公網Nginx大模型Web
- cetnos7下openresty使用luarocks 進行lua的包管理REST
- openresty前端開發序REST前端
- 讀了《JavaScript Web 應用開發》JavaScriptWeb
- WEB應用開發中的ServletWebServlet
- 急聘JAVA Web應用開發高手JavaWeb
- 利用ASP開發Web應用 (轉)Web
- Nginx使用Lua模組實現WAFNginx
- 《高效能iOS應用開發》打造使用者稀飯的AppiOSAPP
- [Apache][Nginx]構建僅對團隊內部公開使用的web應用ApacheNginxWeb
- 【Nginx】Openresty增加waf配置NginxREST
- 如何為 Flask Web 應用配置 NginxFlaskWebNginx
- 用JavaServer Faces開發Web應用(4) (轉)JavaServerWeb
- 用JavaServer Faces開發Web應用(3) (轉)JavaServerWeb
- 使用SAP UI5 Web Components開發React應用UIWebReact
- 開發Web應用程式中Cookie使用的問題 (轉)WebCookie
- OpenResty+lua+redis+mysql多級快取RESTRedisMySql快取
- openresty通過lua增加隨機traceidREST隨機
- 伺服器後端開發系列——《實戰Nginx高效能Web伺服器》伺服器後端NginxWeb