在網際網路公司,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應用。
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、開發環境
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黑名單、限流等功能;
緩存伺服器:可以對響應内容進行緩存,減少到後端的請求,進而提升性能;
其他:如靜态資源伺服器、消息推送服務、縮略圖裁剪等。
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的;對于這種應用非常簡單,不需要依賴資料源,直接單應用閉環即可。
還一些業務型應用場景如下圖所示
商品頁面是由商品架構和其他次元的頁面片段(面包屑、相關分類、商家資訊、規格參數、商品詳情)組成;或者首頁是由首頁架構和一些頁面片段(分類、輪播圖、樓層1、樓層n)組成;分次元是因為不同的次元是獨立變化的。對于這種靜态内容但是需要進行架構内容嵌入的方式,nginx自帶的ssi(server
side include)可以很輕松的完成;也可以使用lua程式更靈活的完成(讀取架構、讀取頁面片段、合并輸出)。
比如商品頁面的架構我們可以這樣:
對于首頁的架構是類似的,因為其特點(架構變化少,樓層變化較頻繁)和個性化的要求,樓層一般實作為異步加載。
2.3、
第三張圖和第二張圖的不同處是不再直接讀取檔案系統,而是讀取本機的redis或者redis叢集或者如ssdb這種持久化存儲或者其他存儲系統都是可以的,比如直接說的商品頁面可以使用ssdb進行存儲實作。檔案系統一個很大的問題是當多台伺服器時需要worker去寫多台伺服器,而這個過程可以使用ssdb的主從實作。
此處可以看到,不管是圖二還是圖三架構,都需要worker去進行資料推送;假設本機資料丢了可怎麼辦?是以實際大部分應用不會是完全單機閉環的,而是會采用如下架構:
即首先讀本機,如果沒資料會回源到相應的web應用從資料源拉取原始資料進行處理。這種架構的大部分場景本機都可以命中資料,隻有很少一部分情況會回源到web應用。
如京東的實時價格/動态服務就是采用類似架構。
3、分布式閉環
單機閉環會遇到如下兩個主要問題: 1、資料不一緻問題(比如沒有采用主從架構導緻不同伺服器資料不一緻);2、遇到存儲瓶頸(磁盤或者記憶體遇到了天花闆)。
解決資料不一緻的比較好的辦法是采用主從或者分布式集中存儲;而遇到存儲瓶頸就需要進行按照業務鍵進行分片,将資料分散到多台伺服器。
如采用如下架構,按照尾号将内容分布到多台伺服器。
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狀态碼錯誤來了解服務的可用率。
5、web應用
整體處理過程和普通web應用沒什麼差別:首先接收請求并進行解析;然後讀取jimdb叢集資料、如果沒有則回源到tomcat擷取;然後進行業務邏輯處理;渲染模闆;将響應内容傳回給使用者。
開發一個web應用我們需要從項目搭建、功能開發、項目部署幾個層面完成。
3.1、項目搭建
java代碼
/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然後進行測試。
到此我們對于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等非正常響應不要設定緩存。
<a target="_blank" href="http://blog.jobbole.com/88766/">深入 nginx:我們是如何為性能和規模做設計的</a>
<a target="_blank" href="http://blog.sina.com.cn/openresty">nginx變量漫談/配置指令的執行順序</a>
<a target="_blank" href="https://github.com/openresty/lua-nginx-module#readme">ngx_lua文檔</a>
<a target="_blank" href="https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/brief.html">openresty最佳實踐</a>
<a target="_blank" href="http://jinnianshilongnian.iteye.com/blog/2190344">跟我學nginx+lua開發</a>
<a target="_blank" href="http://jinnianshilongnian.iteye.com/blog/2235572">建構需求響應式億級商品詳情頁</a>
<a target="_blank" href="http://jinnianshilongnian.iteye.com/blog/2258111%20">京東商品詳情頁服務閉環實踐</a>
<a target="_blank" href="http://toutiao.com/a6254279391729139970/">upsync:微網誌開源基于nginx容器動态流量管理方案</a>
<a target="_blank" href="http://toutiao.com/a6254279391729139970/"> http://toutiao.com/a6254279391729139970/ </a>
原文連結:[http://wely.iteye.com/blog/2353184]