[nginx] 由Lua 粘合的Nginx生態環境-- agentzh tech-club.org 演講聽錄 [複製連接]javascript
kindlephp
LT管理團隊前端
Rank: 9Rank: 9Rank: 9java
未綁定新浪微博node
簽到222python
註冊時間1970-1-1最後登陸2015-6-5在線時間168 小時閱讀權限200積分19025帖子119主題35精華2UID9223mysql
LT總司令 LT元老 LT教授linux
串個門加好友打招呼發消息 nginx
電梯直達跳轉到指定樓層 1#git
發表於 2013-1-12 12:43:47 |只看該做者 |倒序瀏覽
轉自:http://blog.zoomquiet.org/pyblosxom/2012/03/
活動: Tech-Club技術沙龍(2012年2月)活動小結
幻燈: ngx_openresty: an Nginx ecosystem glued by Lua
錄音: http://vdisk.weibo.com/s/2Qcon
筆錄: Zoom.Quiet <zoomquiet+nginx@gmail.com>
Chnangelog:
120312 fixed as s/hhttp/http/g ,thanx muxueqz
120309 fixed kindel->kindle, thanx for milel liu;
120308 fixed ahcking->hacking,thanx weakish
120306 fixed agentzh ID name,thanx himself alert
120305 finished
120301 init.
很早就一直關注 agentzh 對 nginx 的給力 hacking,此次總算有個階段性的說明,雖然沒法現場交流, 好在有錄音,爲了其它沒有時間聽的人們,以及給搜索引擎更好的搜索數據,俺義務聽錄全文;
1. 免責聲明
錄音/幻燈來自做者,版權固然屬於他們
文字聽錄來自 Zoom.Quiet,一切文字問題都是我形成的,與原著無關
由於本人技術有限,僅經過幻燈和錄音,記錯的地方負責在我,與原著者無關
任何不滿和意見,請直接與我聯繫以便改進
zoomquiet+nginx@gmail.com
2. Lua 粘合的 Nginx 生態環境
很高今天和你們進行分享,以前,在北京進行過相關的分享; 今天咱們的話題是 Nginx 也能夠說是關於 Lua 的; 介紹過去3年以來咱們的工做, 工程名字是,openresty,能夠追溯到2007年,那會兒,我剛剛進入 Yahoo! 中國, 第一份工做就是架構一個開放平臺, Yahoo! 自個兒的開放平臺, 系統做到後來逐漸偏離了初衷, 咱們開始爲大型的互聯網公司做一些和web 前端打交道的系統支持;
我在 Yahoo!和 TaoBao 分別工做了兩年,就辭職了; 主要由於,咱們的開源做品,愈來愈多人使用了, 而我一方面,要應付所謂業務需求,另方面要響應來自國內外積極開發者們的要求或是bug; 因此,乾脆辭了專心做事兒; 原本,我想搬到廈門,但是我老婆在福州找到了工做,因而,, 如今,我不拿工資,義務爲全球的愛好者開發 ;-) 如今,已經在福州呆了7個月,這是我老婆給拍的照片; 我習慣,先在紙上寫好代碼,而後輸入電腦,
前面放的是 kindle ; 這臺 kindle 的來歷比較有意思,
在TaoBao 的時候,我打算將 openresty 重寫,由於一開始是用 Perl 來寫的
而在Yahoo! 的時候雖然已經使用 openresty 統一了搜索功能,但性能的確通常
當時,本想基於 Apache 來改寫,不過一位師傅跟我講:"你就直接拿c 寫吧,基於 Apache 寫沒有前途的!"
俺很鬱悶,就問,那怎麼整? 師傅回答,你研究一下 nginx 的源代碼吧,而後就沒再理我 而看代碼是很累的,因此,俺一到 TaoBao 就買了臺 Kindel 來看代碼...
2.1. openresty
剛剛提過, openresty 在開發過程當中逐偏離了原計劃; 再面對後來,更加具體的公司業務後, 這時,已經能夠看出所謂 Ajax/Servise 化了, 在我接觸過的各類繁忙的互聯網公司,都有種趨勢,就是:
對看起來是個總體的web 應用
習慣在後臺拆成不少 Service
有些Service 是供給客戶端發起請求來訪問的,
而有些Service 根本就是爲其它服務而服務的,也使用了 http 協議進行發佈
這種結構,致使總體系統變得很是分散
由多個部門,分別實現一部分系統
而每一個部門,暴露給其它部門的,都是 http 協議,resful 形式的接口而已
好比說, 去哪兒 網,就是很是很是鬆散的服務組合成的;
一個請求進入後,當即分解成各類請求分別進行
而有些就在 Service 之間進行了
既然,http 協議如此常見,咱們就須要強大的實現基礎; nginx 是咱們調研的各類平臺中,最不爛的一個!
其它真心都特別爛,,, Apache 最大的問題是其 I/O 模型,沒法完成很是高效的響應; 可是優勢是:開發接口規整,基於它來寫 mod 很是方便; Lighttpd 正好相反,其 I/O 很是高效,可是開發接口不怎麼友好; 而 Nginx 融合了二者的優勢 ;-)
<<< 5:11
一方面使用了 lighttpd 多路複用的 I/O 模型
另外一方面以借鑑了 apache 的模塊開發支持
在(openresty)開發過程當中,常常有人問,爲何 nginx 如此之快?
咱們知道 nginx 是單線程的,
而單線程的模型,爲何能夠承擔上萬甚至上幾十萬的併發請求?! 由於 nginx 的工做方式,如動畫所示,這是我剛剛用 perl 生成的一個簡單 git 動畫:
這實際上是操做系統線程做的事兒
前面3個,分別對應不一樣的 http 請求
每一個方塊表明一個讀或是寫操做
最後的 epoll_wait 就是 linux 系統中最高效的一種事件接口
也就是説 nginx 內部實際上是種事件驅動的機制
只有相關事件發生時,才處理具體數據
若是當前接口沒有數據時,就會當即切換出去,處理其它請求
因此,雖然只有一個線程,可是,能夠同時處理不少不少線程的請求處理 那麼,這種形式的 web 系統,能夠很輕易的將 cpu 跑滿,即便帶寬沒有跑滿的狀況下; 而 apache 這類多進程多線程模型的服務器,則很難將 cpu 跑滿:
由於併發達到必定量時
內存首先將耗盡
由於在 linux 系統中,線程數是有限的,每一個線程必須預分配8m大小的棧,不管是否使用!
因此,線程增長時,內存首先成爲瓶頸
即便挺過內存問題,當併發請求足夠多時,cpu 爭用線程的調度問題又成爲系統瓶頸
<<< 8:31
因此,nginx 這樣簡單的單進單線模型,反而被 memcached 等高性能系統定爲I/O 模型; 那麼,咱們做了什麼呢?
主要是爲 nginx 提供了不少補丁,進行了 bugfix
同時利用 nginx 提供的開發者接口,貢獻了不少模塊
咱們還將以前說起的 Lua 嵌入 nginx ,使其具備全功能的交互能力
更加把 Lux 一些經常使用庫,也放進去了
而後打成一個大包,命名爲 openresty ...這是使用 Tiddlywiki 隨便做的一個 主頁: http://openresty.org
2.2. 配置小語言
nginx 自己有個很重要的特色,這在維基百科的條目中也強調過:
其配置文件記法是很是靈活,並可讀的
nginx.conf 配置文件,本地其實就是個小語言 好比:
location = '/hello' {
set_unescape_uri $person $arg_person;
set_if_empty $person 'anonymous';
echo "hello, $person!";
}
複製代碼
這段配置,對於 apache 用戶來説,也很熟悉
咱們首先使用相似正則表達式的形式來約定一個響應的 url
而後,可使用各類 nginx 的指令對內部變量進行到系列操做
變量也是配置文件的一部分,很象一種編程語言
好比,這裏,咱們就將 person 這個變量使用 arg_person 進行賦值
而後,用 'anonymous' 做爲空值時的默認值給 $person
最後直接使用 echo 將結果輸出 這樣,咱們就可使用 curl 模擬瀏覽器訪問,給 /hello 提供一個utf8 編碼的字串值, 以 ?person= 的GET 方式變量, 就能夠得到預期的反饋: hello, 章亦春 不給參數的話,剛剛的 anonymouse 就起做用了;
因此,總體上,咱們指望在 nginx 中實現服務接口,就這樣寫點配置就好,不用寫什麼認真的c 代碼;-) 而跑起來就象飛同樣,由於,這麼來寫,實際和用c 現實沒有什麼區別;
事實上,全世界的開發者都在使用 nginx 的開發接口,在拼命豐富這種配置文件小語言的詞彙表!
而真正決定其表達能力的是:"vicabulary"
好比說,咱們看這個例子,這是我寫向第2或是第3個nginx 模塊:
用以直接訪問 memcached 的所謂上游模塊
nginx 有自個兒的一套術語,在其後的各類服務好比memcached ,在nginx 而言就是上游
對應的,那些訪問 nginx 的瀏覽器等等客戶端,就視爲下游
# (not quite) REST interface to our memcached server
# at 127.0.0.1:11211
location = /memc {
set $memc_cmd $arg_cmd;
set $memc_key $arg_key;
set $memc_value $arg_val;
set $memc_exptime $arg_exptime;
memc_pass 127.0.0.1:11211;
}
複製代碼
這樣簡單的配置一下,經過 set 將url 上的各類參數映射給幾個變量,
而後經過 memc_pass 鏈接到遠端一個memcached 服務,固然後面也能夠是個集羣
當即,咱們就獲得一個,應該說是種僞 restfule 的 memcached 的使用接口服務
咱們可使用 curl 來操做目標 memcached 了
好比說,著名的 flush_all 命令,就能夠直接經過 url 來執行
經過這種形式,咱們能夠快速擴展成對memcached 集羣的簡潔管理服務,進行各類操做
這樣做的好處在於::
不論其它相關應用使用 php 什麼亂七八糟的語言寫的,均可以統一包裝成 http 接口
令整個業務系統變成http 協議,這樣系統的複雜度就可以有效下降
一樣能夠這樣對 MySQL 等等其它集羣服務進行包裝
包括你們知道的 taobao 集羣,對外部開發來說,好象是專門爲外部擴展發佈的服務,
其實在 taobao 內部各類服務也是以兩樣形式組合起來的
你們知道 taobao 是java 系的,它不少服務是經過定製 jvm 完成的
因此,對於ali 原先業務,以及合做方的業務,還有咱們數據統計部門的業務,對於jvm是沒法直接使用的
怎麼辦?因此,經過開放平臺業務,將各類內部服務,封裝成一系列 http 接口方便使用
包括taobao 的登陸,其實也封裝成 http 接口,供給,taobao 子域名應用來使用
不論使用什麼開發語言,老是能夠對http 協議進行訪問的
並且 http 協議自己很是簡單
咱們能夠方便的獲取許多現成的工具進行調試/追蹤/優化,,,
另外,因爲選擇了 nginx,這使得http 的開銷,代價變的很是很是的低
記得 去哪兒網,原先有業務使用了幾十臺 MySQL
前端使用 java 的jodb 進行鏈接
而由於代碼寫的比較糟糕,由於業務部門嘛,寫的時候不會注意鏈接池的效率,
因此,每臺主機的負載都很是 很是 高
而,咱們後來改成nginx 做前端,結果一臺nginx 就將之前幾十臺java 主機的業務抗了下來
經過封裝成 http 接口,業務代碼隨便長鏈接/短鏈接,隨便它搞,都撐得住了!
因而,被他們java 程序員描述成不可能的任務,被一臺 nginx 主機就解決了
<<< 17:00 (插入提問): 封裝具體做了什麼?爲何比原先的方式效率高? 雖然改爲了 http 實際鏈接MySQL 時不一樣樣要消耗?
由於,封裝成 http 接口的數據庫,咱們內部使用了鏈接池
已經優化的高效數據庫鏈接池,而通常工程師不用關注鏈接池的技巧,專心完成業務代碼就好,不容易出錯
並且,使用語言專用中間件的話,牽涉到其它問題:
中間件自己是否穩定?高效率?
中間件自己是否易於擴展好維護?
等等一系列問題,遠沒有統一成 http 服務於全部語言實現的應用來的乾脆簡潔
甚至於,咱們後來引入了完整的 Lua 語言,它基本足夠完備,能夠支持咱們直接完成業務
taobao 的數據魔方,就直接使用腳本在 nginx 中完成的
相比原先php的版本,僅僅這一項,就提升響應速度一個量級!
因此,不論 memcached 仍是什麼數據庫,咱們能夠統一到一箇中間件
並且 http 協議的中間件,還有個好處是能夠直接公開給外部使用
由於 http 上的訪問控制很好做,複雜度也低
咱們的量子統計,就是直接和taobao 主站服務經過 http 良好整合在了一塊兒
能夠簡單的一個參數處理就發佈給外部或是內部來安全使用
2.3. ngx_drizzle
經過模塊,咱們能夠創建應用和 MySQL 間的非阻塞通訊
這點很是重要!
由於,當前端訪問後端很大的數據集羣的時候,其自己的併發能力就成爲瓶頸
設想後端有近百臺 MySQL 時,後臺自己的併發量就已經很是大了
而前端相似 php 技術根本沒法將後端全部主機的能力都應用起來
因此,咱們很是須要非阻塞技術
須要一種數據庫代理,就象很高能的網關同樣,將後端全部MySQL 服務器的能力都激發出來
而不用期待前端應用來自行完成併發調度 基於以上認知,咱們開發了各類數據的非阻塞上游模塊:
包括對 MySQL/Postgres/redis 等等
也嘗試過對 Oracole ,可是,其官方的 c 驅動有些限制,雖然也提供了非阻塞接口,可是不完整
在創建鏈接和銷毀鏈接時,只能以阻塞方式進行,因此,很糾結
MySQL 官方的 c 驅動也只提供了阻塞方式!
那隻好尋求第三方的驅動,咱們選擇了 Drizzle 這個驅動,並整合進來 成爲 ngx_drizzle 模塊
upstream my_mysql_backend {
drizzle_server 127.0.0.1:3306 dbname=test
password=some_pass user=monty
protocol=mysql;
# a connection pool that can cache up to
# 200 mysql TCP connections
drizzle_keepalive max=200 overflow=reject;
}
複製代碼
咱們這樣簡單配置:
經過 drizzle_server 配置鏈接口令和協議,由於模塊能夠鏈接 MySQL 和 drizzle 兩種數據源,因此,要聲明協議模式
使用 drizzle_keepalive 創建一個鏈接池,限定上限爲200,當超過鏈接限制時就 reject,至關對數據庫的簡單保護
而後這樣定義一個 cat 接口
location ~ '^/cat/(.*)' {
set $name $1;
set_quote_sql_str $quoted_name $name;
drizzle_query "select *
from cats
where name=$quoted_name";
drizzle_pass my_mysql_backend;
rds_json on;
}
複製代碼
cat 以後是這貓的名字,使用 set 得到,這是 nginx 自己的功能
而後使用 set_quote_sql_str 對查詢語句進行轉義,以防止SQL注入攻擊
經過 drizzle_query 組合成查詢語句
drizzle_pass 來完成對後端數據集羣的查詢,由於前面的 drizzle_server·可聲明一組 MySQL服務器
甚至於,咱們爲查詢返回的結果集,定製了一種格式,叫 rds_json
這種格式是面向各類關係型數據庫的
咱們針對這種格式,開發了一系列過濾器能夠自由輸出 csd或是json格式
這樣,幾乎全部報表接口,都經過這種方式實現的
taobao 直通車就使用了 csd 格式,由於他們是將這當成中間件來使用的
而咱們是直接經過 json 以 Ajax 形式對外的
這樣,經過 curl 訪問 cat 接口查詢 Jerry ,就能夠得到名叫Jerry 的貓的相關數據
這裏json 的輸出,能夠經過一系列方式,進行自由的調整
好比說,有的要求每行數據都是 key/value 的格式,有的要求緊湊格式,第一行包含key以後,之後的所有是數據等等,,,
2.4. ngx_postgres
那麼 portsgres 訪問接口模塊名叫:ngx_postgres
這是一位波蘭的 hacker 在咱們的ngx_drizzle 基礎上完成的
由於它仿造了咱們的接口形式
pg 的官方模塊是沒法使用的,因而他花了兩個月的時間,完成了這個模塊
去哪兒網,有不少地方就使用了這一模塊
咱們能夠看到如何使用 Lua 來調用這個標準模塊 由於在 web 開發中,每向上一層,速度會降低一級,可是,功能會豐富不少
可是,使用 nginx 模塊來完成,速度損失頗有限
upstream my_pg_backend {
postgres_server 10.62.136.3:5432 dbname=test
user=someone password=123456;
postgres_keepalive max=50 mode=single overflow=ignore;
}
複製代碼
這裏,咱們配置 overflow 時 ignore ,忽略,就是説,鏈接超過限定時,直接進入短鏈接模式
location ~ '^/cat/(.*)' {
set $name $1;
set_quote_pgsql_str $quoted_name $name;
postgres_query "select *
from cats
where name=$quoted_name";
postgres_pass my_pg_backend;
rds_json on;
}
複製代碼
這樣定義一個 pg 版本的 cat 接口
注意,進行SQL 轉義時問的是 set_quote_pgsql_str, 由於pg 的SQL轉義和其它的不一樣
2.5. ngx_redis2
而後,去年的時候,我爲了好玩,寫了個 redis 的模塊: ngx_redis2
依然是100%非阻塞,去哪兒和天涯也都大量使用了這一模塊
upstream my_redis_node {
server 127.0.0.1:6379;
keepalive 1024 single;
}
複製代碼
一樣使用 upstream 定義一個或是多個鏈接池
使用 keepalive 定義併發策略,這種場景中 tcp 在 http 的鏈接消耗是很是低的
# multiple pipelined queries
location /foo {
set $value 'first';
redis2_query set one $value;
redis2_query get one;
redis2_pass my_redis_node;
}
複製代碼
這裏,我使用 redis2_query 定義了兩個請求
經過流水線形式,一次請求發送了兩個命令過去,響應時,就有兩個響應,按照順序返回
2.6. ngx_srcache
ngx_srcache 是個頗有趣的通用緩存模塊
以前爲 apache 寫過一些模塊,其中一個比較有趣的,就是針對mod_cache 模塊,寫了個 memcached 的模塊,就能夠經過 memcached 對apache 中任意的響應進行緩存!
這模塊當初是爲 Yahoo! 的搜索業務中,爬蟲的抽取系統進行設計的
固然我就發現,apache 裏對 memcached 進行阻塞訪問時,有點虛焦? 隨着併發數增長,響應速度極速降低
因此,在nginx 時,就不會有這種問題,保證全部處理都是非阻塞的!包括訪問 memcached
因此,咱們能夠在配置文件中自行決定使用什麼後端來存儲緩存
location /api {
set $key "$uri?$args";
srcache_fetch GET /memc key=$key;
srcache_store PUT /memc key=$key&exptime=3600;
# proxy_pass/drizzle_pass/postgres_pass/etc
}
複製代碼
這裏咱們定義兩種調用,所謂 fetch 是在 apache 中一種模板,c級別的調用可是,技法和 http 的 get 一樣
這樣聲明的 location,咱們能夠同時即對外提供調用,也能夠對配置內部其它 location 進行調用!
location /memc {
internal;
set_unescape_uri $memc_key $arg_key;
set $memc_exptime $arg_exptime;
set_hashed_upstream $backend my_memc_cluster $memc_key;
memc_pass $backend;
}
複製代碼
這樣,其實就是在收到請求時,實際調用了 /memc 接口,訪問後端緩存
收到結果後,再使用 srcache_store 接口整理put 回請求的入口 location, 設置相應的格式
而 /memc 接口經過 internal 標記,成爲僅僅對內服務的接口
後面咱們經過一系列指令,從 url 參數 ;-)
即便是內部調用,依然是個標準的 http 請求界面
而後使用 set_hashed_upstream 對 memcached 的集羣.進行基於鍵的模的 hash 將結果放到 $backend
最後使用 memc_pass 完成對集羣的查詢
這裏的 my_memc_cluster 是怎麼定義的呢?
upstream memc1 {
server 10.32.126.3:11211;
}
upstream memc2 {
server 10.32.126.4:11211;
}
複製代碼
upstream_list my_memc_cluster memc1 memc2;
使用 upstream 定義兩個服務,使用upstream_list 聲明爲一個集羣
這裏其實也有限制的:
在咱們動態追加主機時
咱們要從新生成配置文件,而後使用 touch 命令通知 nginx 從新加載
而這一限制,咱們將看到,在基於 Lua 的實現中會不存在 ;-)
前面咱們看到,通過簡單的配置,咱們就能夠得到一系列強大的 api 服務;
<<< 29:51
2.7. ngx_iconv
實際使用中,還有一個重要的需求就是字符串編碼:
由於,有的業務是基於 gbk的,有的又是 utf-8 的
通常咱們能夠在數據庫層面進行處理
可是,對於一些功能弱些的產品,好比說,memcache/redis 等,就沒辦法了
因此,咱們完成了本身的動態編碼轉換模塊:
ngx_iconv
無論你們在訪問 MySQL 時,使用的什麼途徑,好比習慣的反向代理什麼的
均可以經過 iconv_filter 對響應體進行編碼轉換!
並且是流式的轉換,也就是說,不須要 buffer,來一點數據就當即完成轉換
location /api {
# drizzle_pass/postgres_pass/etc
iconv_filter from=UTF-8 to=GBK;
}
複製代碼
以上這是從 utf-8 到 gbk 的轉換
<<< 30:54
2.8. 嵌入 Lua
後面咱們化了很大力氣將 Lua 嵌入到了裏面:
這樣使得,能夠實現任意複雜的業務了
# nginx.conf
location = /hello {
content_by_lua '
ngx.say("Hello World")
';
}
複製代碼
這樣咱們就完成了一個 hallo world
ngx.say 是 lua 顯露給模塊的接口
另外固然也能夠調用外部腳本
如同咱們寫php 應用時,習慣將業務腳本單獨組織在 .php 文件中同樣
# nginx.conf
location = /hello {
content_by_lua_file conf/hello.lua;
}
複製代碼
經過 content_by_lua_file 調用外部文件:
-- hello.lua
ngx.say("Hello World")
複製代碼
這裏的腳本能夠任意複雜,也可使用Lua 本身的庫
早先,咱們很是依賴,ngninx 的子請求,來複用 nginx 的請求模塊:
好比說,咱們一個模塊,須要同時訪問 memcached/mysql/pg 等許多後端
這時,怎麼辦? 這麼來:
location = /memc {
internal;
memc_pass ...;
}
location = /api {
content_by_lua '
local resp = ngx.location.capture("/memc")
if resp.status ~= 200 then
ngx.exit(500)
end
ngx.say(resp.body)
';
}
複製代碼
先在 /memc 中創建到 memcache 的鏈接,並聲明爲內部接口
而後,在 /api 中使用 ngx.location.capture 發起一個 location 請求
就象發起一個正當的 http 請求同樣,請求它,可是,其實沒有http的開銷,由於,這是c 級別的內部調用!
並且是個異步調用,雖然咱們是以同步的方式來寫的
而後咱們能夠檢驗響應是否 200,不然訪問 500
最後就能夠將響應體輸出出來
2.8.1. 同步形式異步執行!
這裏爲何能夠同步的寫?
寫過 javascript 前端程序的朋友,應該知道要實現異步效果,咱們不少時候,要使用回調
而在 Lua 中咱們能夠這麼來,由於 Lua 支持協程,即,concurrent
這樣,咱們能夠在一個 Lua 線程中分割出多個Lua 用戶級的邏輯線程
這種僞線程,能夠實現比操做系統高的多的多的併發能力,由於系統開銷很是的小
近年有一些技術,也都支持了 concurrent 的技術,能夠象http 請求順序同樣,順着寫
不用象js 程序員那些糾結倒着寫,在須要順序操做時,又必須借重一些技法,而應用技法的代碼,又實在難看,沒法習慣
因此,咱們當初選擇 Lua 一個很重要的緣由就是支持 協程
這裏咱們假定,同時要訪問多個數據源
並且,查詢是沒有依賴關係的,那咱們就能夠同時發出請求
這樣我總的延時, 是我全部請求中最慢的一個所用時間,而不是原先的全部請求用時的疊加!
這種方式,就是用併發換取了響應時間
location = /api {
content_by_lua '
local res1, res2, res3 =
ngx.location.capture_multi{
{"/memc"}, {"/mysql"}, {"/postgres"}
}
ngx.say(res1.body, res2.body, res3.body)
';
}
複製代碼
這裏咱們就同時發出了3個請求
同時到 memcached/mysql/pg
而後全新響應後,將結果放到 res1/2/3 三個變量中返回 因此,這種模型裏,實現併發訪問也是很方便的 ;-)
<<< 35:20
2.9. lua_shared_dict
這是我去年,花力氣完成的 nginx 共享內存字典模塊: lua_shared_dict
由於 nginx 是多 worker 模型,能夠有多個進程
可是,其實 workder 數量和併發無關,這不一樣於 apache
nginx 多worker 的目的是將 cpu 跑滿,由於它是單進程的嘛
nginx 實際只跑了操做系統的一個線程,因此,多核主機中,若是有8核心,咱們通常就起8個 worker 的
若是業務有硬盤 I/O 的操做時,咱們通常會起比核數略多的 worker 數
由於在 linux 中,磁盤很難有非阻塞的操做
雖然有什麼 aio 的模型,可是有不少其它問題
因此,本質上 nginx 多 worker 是爲了跑滿 cpu
那麼,一但多進程了,就存在滿滿的共存問題
好比説,咱們想在多個進程間共存配置/業務數據
因此,基於共存內存來做
lua_shared_dict dogs 10m;
server {
location = /set {
content_by_lua '
local dogs = ngx.shared.dogs
dogs:set("Tom", ngx.var.arg_n)
ngx.say("OK")
'
}
location = /get {
content_by_lua '
local dogs = ngx.shared.dogs
ngx.say("Tom: ", dogs.get("Tom"))
';
}
複製代碼
這有個例子:
首先,使用 lua_shared_dict 分配一 10M 的空間
而後,使用 OOP 方式,來定義兩個接口:一個 /set 一個/get
而後,不論哪一個 worker 具體調用哪一個操做
可是結果,是終保存一致的
使用 curl 先set 一下,再 get 就變成了 59,由於內部進行了自增
共存的實現是經過紅黑樹+自旋鎖來達成的:
紅黑樹的查找相似 hash 表查找的一種算法
爲保持讀寫的數據一致性,使用 自旋鎖來保證
因此,當併發增大或是更新量增大時, 自旋鎖可能有問題,將來咱們準備進一步修改爲報灰的模型
其實,共享內存的方式,在鎖開銷很是小時,效率是很是高的,在騰訊單機併發跑到20萬都是小意思;
另外,在 Lua 中,咱們須要對大數據量的一種非緩存的輸出:
由於,在不少 web 框架中或多或少都有緩存,有的甚至使用了全緩存
那麼,當你輸出體積很大的數據時,就很易囧掉
而,在 Lua 中,咱們就很容易控制這點
-- api.lua
-- asynchronous emit data as a response body part
ngx.say("big data chunk")
-- won`t return until all the data flushed out
ngx.flush(true)
-- ditto
ngx.say("another big data chunk")
ngx.flush(true)
複製代碼
好比,這裏咱們先 ngx.say ,異步的輸出一個數據
這段數據不必定刷得出去,若是網卡沒來得及輸出這投數據的話,這會在 nginx 的進程中緩存
若是,我想等待數據輸出後,再繼續,就使用 ngx.flush ,這時,只有數據真正刷到系統的緩衝區後,才返回
這樣保證咱們 nginx 的緩存是非低的,而後咱們再處理下一個數據段
如此就實現了流式的大數據輸出
這樣,有時網絡很慢,而數據量又大,最好的方式就是:
既然你發的慢,那我也收的慢: 你一點點發,我就一點點收
這樣咱們就可使用不多的資源,來支持不少大數據量的慢鏈接用戶
2.10. Socket形式
然而,還有些慢鏈接就是惡意攻擊:
我能夠生成不少 http 鏈接,接進來後,慢的發送,甚至就不發送,來拖死你的應用
這種狀況中,你一不注意,服務分配給太多資源的話,整個系統就很容易被拖垮 因此,去年年末,今年年初,我下決心,完成了一個 同步非阻塞的 socket 接口:
<<< 40:50
這樣,我就不用經過 nginx 的上游模塊來訪問http 請求:
咱們就可讓 Lua 直接經過 http,或是 unix socket 協議,訪問任意後端服務
local sock = ngx.socket.tcp()
sock:settimeout(1000) -- one second
local ok, err = sock:connect("127.0.0.1", 11211)
if not ok then
ngx.say("failed to connect: ", err)
return
end
複製代碼
象這樣,創建 socket 端口,並能夠設定超時
咱們就能夠進行非阻塞的訪問控制,當超時時,nginx 就能夠自動掛起,切入其它協程進行處理
若是全部鏈接都不活躍,我也能夠等待系統的 epoll 調用了 就不用傻傻的徹底呆在那兒了
local bytes, err = sock:send("flush_all\r\n")
if not bytes then
ngx.say("failed to send query: ", err)
return
end
local line, err = sock:receive()
if not line then
ngx.say("failed to receive a line: ", err)
return
end
ngx.say("result: ", line)
複製代碼
或是使用 sock:send 直接返回,就能夠繼續其它請求了
使用 receive 來接收查詢的返回,讀失敗有失敗處理,成功就打印出來 一切都是天然順序
local ok, err = sock:setkeepalive(60000, 500)
if not ok then
ngx.say("failed to put the connection into pool "
.. "with pool capacity 500 "
.. "and maximal idle time 60 sec")
return
end
複製代碼
這是鏈接池的調用
經過 sock:setkeepalive , Lua 模塊,就會將當前鏈接,放入另外一鏈接池中以供其它請求複用
也就是說,若是其它請求,請求到同一個url 時, nginx 會直接交給它原先的鏈接,而省去了開新鏈接的消耗
keepalive 的參數比較少:
頭一個是,最大空閒時間,即,一個鏈接放在鏈接池裏沒有任何人來使用的最大時間
這裏是60秒,由於維持一鏈接的代價仍是很昂貴的,若是一分鐘了也沒有人來用,我就主動關閉你節省資源
對於負載比較大的應用,這樣能夠減小浪費
第二個參數是,最大鏈接數,
這裏是500,若是鏈接數超過限制,就自動進入轉移鏈接的模式
Unix 域套接字 是 Linux/Unix 系統獨特的進程接口
雖然不走 http 協議,可是調用形式和 tcp 的 socket 徹底相似
local sock = ngx.socket.tcp()
local ok, err = sock:connect("/tmp/some.sock")
if not ok then
ngx.say("failed to connect to /tmp/some.sock: ", err)
return
end
複製代碼
一樣經過 ngx.socket.tcp 來創建鏈接
而後,使用 sock:connect 來指定一個特殊文件,接入套接字
就能夠進行各類平常的操做了
2.11. concurrent ~ "cosocket"
這個模塊是基於 concurrent 的:
寫是順序寫,可是執行是非阻塞的! 這點很是重要!
協程技術誕生也有些年頭了,
可是,至今 99.9% 的 web 應用依然是阻塞式的
由於早年,基於阻塞的應用開發太習慣了
而基於異步的開發,對於工程師的思維能力要求過高,這也是爲何 node.js 工程師在開發時的主要痛苦
由於,要求改變思維方式來考慮問題,咱們的程序員可能是 php 的,要求他們改變思惟是很痛苦的
因此,不只僅是爲了推廣咱們的平臺
更是爲了兼容工程師的阻塞式思維,同時又能夠利用協程來提升系統性能,達到單機上萬的響應能力
咱們引入了 Lua 的協程,並稱之爲: "cosocket"
即,concurrent based socket
而一位資深的 python 粉絲告訴我,python也有優秀的協程庫:
是基於 greenlet 的 Gevent
固然,相似咱們的系統,都是能夠支撐很是高併發的響應
可是,咱們當初選擇 Lua 還有個很重要的緣由是:
cpu 的執行效率
當你的併發模式,已是極致的時候
cpu 很容易成爲瓶頸!
通常狀況下是 帶寬首先不夠了,而後 cpu 被跑滿
而在 apache 模型中,反而是內存首先不足
常常是24個進程,swap 8G/24G 不斷的增加,卡住什麼也玩不了了
而cpu 光在那兒進行上下文切換,沒有做什麼有意義的事兒 即,所謂內耗
當咱們將應用從 I/O 模型解放後,拼的都是 CPU:
由於,內存通常消耗都不太大
咱們常常在 256M 內存的虛擬機,或是64M 內存的嵌入式設備中跑生產服務 內存,真心不該該是問題所在,,,
可是,要進行計算時就必定要快!
而 Lua 近年發展編譯器到什麼地步?
有種編譯器,能夠運行時動態生成機器碼
在咱們的測試中,高過了末啓用優化的 gpc
而啓用優化的 gpc ,消耗資源又高過 Lua
因此, Lua 的性能沒有問題
而後咱們實際,按照 ruby 社區的説法,就是直接基於Lua 擴展出了一種專用小語言
業務團隊實際並無直接使用 Lua 來寫,而是使用咱們爲業務專門定製的一種專用腳本(DSL)
因此,代碼量很是的少 並且,咱們的定製小語言,是強類型的:
強類型語言有不少好處
並且,能夠在小語言中,定義對業務領域的高層次約束
你就能夠很方便的查找出業務工程師常範的錯誤,轉化成語言特性包含到約束中,在編譯器中實現!
最後編譯成包含優化的 Lua 代碼,讓它跑的象飛同樣! 並且! 哪天,我高興了,也可讓它生成 C 代碼讓它跑到極致!
這樣,業務不用改一行代碼,可是,系統效能能夠提升幾倍
等等,這些都是能夠實現的,,,
要,實現這些,要求咱們的基礎必須很是很是的高效,同時又很是很是小巧!
這樣咱們才能在上面搭上層建築
即,所謂的: "勿在浮沙築高臺"!
在這一過程當中,咱們也吃過不少苦,,,好在有 nginx ...
再有,咱們發現 socket 模型,同樣能夠用來讀取下游,即客戶端請求數據!
當請求體很大,好比說,上傳一個很大的文件時
也須要異步處理 ,就省的我操心了
因此,我就對下游請求,包裝了一個只讀的 socket,能夠對請求數據進行流式讀取
local sock, err = ngx.req.socket()
if not sock then
ngx.say("failed to get request socket: ", err)
return
end
sock:settimeout(10000) -- 10 sec timeout
while true do
local chunk, err = sock:receive(4096)
if not chunk then
if err == "closed" then
break
end
ngx.say("faile to read: ", err)
return
end
process_chunk(chunk)
end
複製代碼
這樣,創建一個下游 socket 後,以 4096 字節爲一個塊(trunk)進行讀取
而後檢查是否結束,即便沒有結束,我也能夠一塊塊的進行處理
好比,讀一塊就寫到硬盤上,或是寫到遠程的一個 tcp 鏈接,這鏈接也是非阻塞的!
象這樣,我這層就很是很是高效!
2.12. 高層實現
進行各類高層次的實現就很是方便了
之前我用幾年時間才能實現純 Lua 的 MySQL 的鏈接模塊
如今用幾百行 Lua 腳本就實現了: lua-resty-mysql
並且是很是完備的實現
支持多結果/存儲過程等等高級功能
並且性能很是接近純 C 寫的模塊,我評測下來,也就差 10~20% 的響應
若是將來,我用C 改寫其中計算密集型的處理模塊,那性能能夠進一步大幅度提高!
lua-resty-memcached 也就500多行就搞掂了!
是完整的 memcached 協議的支持
因此,用這種技術,能夠很方便的實現公司裏固定的或是全新的後端服務;
redis協議自己設計的很是巧妙,雖然命令多,可是底層傳輸協議很是簡潔
因此,我只用 200 多行,就實現了:lua-resty-redis
後面兩個模塊都比較粗糙,僅僅封裝了傳輸協議,因此,執行效率,高於它們官方c 實現的等價物 ;-)
lua-resty-upload 就是說起的大文件上傳模塊
不過,這模塊寫的比較粗糙
api 暴露的不夠 優美,,,,
3. abt.
我在 http://github.com/agentzh 上每天提交代碼;
也刷weibo : http://weibo.com/agentzh/ 不過,最近刷的比較少,,,
<<< 53:00
QA:
將 Lua 當成什麼來用? 直接業務嘛?
簡單的能夠直接來
也能夠架構更高層的領域腳本,編譯成 Lua 來執行
不過,最終,都是經過寄生在 nginx 平臺上的 Lua 來實際跑
那 openresty 主要解決了nginx 的什麼問題?是nginx 的缺陷嘛?
分兩個方面來想:
1.做爲 nginx 的補充,不少人也是這麼用的,好比說負載的接入,簡化 F5 的前端配置,訪問的邏輯控制,,,
2. 直接做爲 web 應用的機制,直接實現全部的應用,輸出網頁,發佈 web service,等等
和 apache 什麼的性能差異主要在哪裏?
主要是 I/O 模型的本質差別
nginx + Lua 能夠完成數量級上的提高
並且,做爲應用或是做爲 httpd 能夠同時勝任雙重角色!
的, 錄音, kindle
分享到:
QQ空間QQ空間
騰訊微博騰訊微博
騰訊朋友騰訊朋友