apisix实际应用_Apache APISIX 的高性能实践
2019 年 7 月 6 日,OpenResty 社區(qū)聯(lián)合又拍云,舉辦 OpenResty × Open Talk 全國巡回沙龍·上海站,OpenResty 軟件基金會聯(lián)合創(chuàng)始人王院生在活動上做了《ApacheAPISIX 的高性能實(shí)踐》的分享。OpenResty x Open Talk 全國巡回沙龍是由 OpenResty 社區(qū)、又拍云發(fā)起,邀請業(yè)內(nèi)資深的 OpenResty 技術(shù)專家,分享 OpenResty 實(shí)戰(zhàn)經(jīng)驗(yàn),增進(jìn) OpenResty 使用者的交流與學(xué)習(xí),推動 OpenResty 開源項(xiàng)目的發(fā)展?;顒訉㈥懤m(xù)在深圳、北京、武漢、上海、成都、廣州、杭州等城市巡回舉辦。
王院生,OpenResty 社區(qū)、OpenResty 軟件基金會聯(lián)合創(chuàng)始人,《OpenResty jiao佳實(shí)踐》主要作者,ApacheAPISIX 項(xiàng)目發(fā)起人和主要作者。
以下是分享全文:
大家好,我是王院生,很高興來到上海。首先做下自我介紹,我于 2014 年加入奇虎360,在那時認(rèn)識了 OpenResty,此前我是一個純粹的 C/C++語言開發(fā)者。在 360 工作期間,利用工作閑暇時間寫了《OpenResty jiao佳實(shí)踐》,希望能影響更多的人正確掌握 OpenResty 入門。2017 年我作為技術(shù)合伙人和春哥(章亦春,agentzh)一起創(chuàng)業(yè)。今年我個人的重心有所調(diào)整并在今年三月份離職,準(zhǔn)備將更多精力投入到開源上,于是發(fā)起了Apache APISIX 這個項(xiàng)目,企業(yè)宗旨是依托開源社區(qū),致力于微服務(wù) API 相關(guān)技術(shù)的創(chuàng)新和實(shí)現(xiàn)。
什么是 API 網(wǎng)關(guān)
API 網(wǎng)關(guān)的地位越來越重要,它幾乎劫持了所有流量,內(nèi)外之間完成了用戶的安全控制、審計(jì),通過自定義插件的方式滿足企業(yè)自身特定需求,jiao常見的自由身份認(rèn)證等。隨著服務(wù)在數(shù)量和復(fù)雜度上的不斷增長,更多的企業(yè)采用了微服務(wù)的方式,這時通過 API 網(wǎng)關(guān)來完成統(tǒng)一的流量管理和調(diào)度就非常有必要。
微服務(wù)網(wǎng)關(guān)和傳統(tǒng)意義上的 API 網(wǎng)關(guān)有一些不同,主要包括下面幾點(diǎn):動態(tài)更新:在微服務(wù)之前,服務(wù)不像現(xiàn)在這樣經(jīng)常來回地變化。比如微服務(wù)需要做橫向擴(kuò)充,或者故障恢復(fù)、熱備、切換等,IP、節(jié)點(diǎn)等變動更加頻繁。舉例如微博上一旦出現(xiàn)了爆點(diǎn)事件,就急速擴(kuò)充計(jì)算點(diǎn),必須要非??斓?cái)U(kuò)充新機(jī)器來扛壓。波峰波谷變化明顯,分鐘級別的機(jī)器動態(tài)管理,已經(jīng)越發(fā)是常態(tài)。
更低延遲:通常動態(tài)就意味著可能會做一些延遲(復(fù)雜度增加),在微服務(wù)里面,對于延遲要求比較高,尤其對于現(xiàn)在的用戶體驗(yàn),超過 1 秒以上的延遲是完全不可接受的。
用戶自定義插件:API網(wǎng)關(guān)是給企業(yè)用戶使用的,它一定存在私有邏輯(比如特殊的認(rèn)證授權(quán)等),所以微服務(wù)網(wǎng)關(guān)必須能夠支持企業(yè)用戶自定義插件。
更集中的管理 API:如前面所說 API 網(wǎng)關(guān)劫持了用戶的所有流量,所以用網(wǎng)關(guān)來做統(tǒng)一的 API 管理是非常必要的。在網(wǎng)關(guān)角度可以看到 API 是如何設(shè)計(jì),是否存在延遲、安全問題,以及響應(yīng)速度和健康信息等。
我們要做的微服務(wù) API 網(wǎng)關(guān)產(chǎn)品,除了上面的基本要求,還有一些是我們區(qū)別于其他人的:通過社區(qū)聚焦:通過開源方式聚焦有共同需求的人群,讓更多不同公司的人可以一起協(xié)作,共同打磨更好的產(chǎn)品,減少冗余開發(fā)。
簡潔的 core:產(chǎn)品的內(nèi)核必須是非常簡潔的,如果內(nèi)核復(fù)雜,會使得大家的上手成本高很多,望而卻步肯定不是我們期望的。
可擴(kuò)展性、頂級性能、低延遲:這幾項(xiàng)都是要同時嚴(yán)格保障的,也是我們會花主要精力保證的。目前 APISIX 項(xiàng)目的性能比空跑 OpenResty 只低 15%,這點(diǎn)還是非常值得傲嬌的。
Apache APISIX 高性能微服務(wù)網(wǎng)關(guān)
Apache APISIX 架構(gòu)與功能
△ ApacheAPISIX 的基本架構(gòu)
上圖是 ApacheAPISIX 的基本架構(gòu),羅列用到的幾個基本組件。其中包括 ETCD 可以完成配置存儲,由于 ETCD 可以走集群,所以我們可以借用它完成動態(tài)伸縮、高可用集群等。ETCD 數(shù)據(jù)支持通過 watch 的方式增量獲取,使得 ApacheAPISIX 節(jié)點(diǎn)規(guī)則更新可以做到毫秒級,甚至更低。ApacheAPISIX 自身是無服務(wù)狀態(tài)的,所以方便橫向擴(kuò)充。
另一個組件是 JSON Schema,它是一個標(biāo)準(zhǔn)協(xié)議,主要用來驗(yàn)證數(shù)據(jù)的有效性。JSON Schema 目前對外公開有四個不同版本,我們jiao終選用 RapidJSON,因?yàn)樗麑@四個版本都有相對完整的支持。
圖中的 Admin API 和 ApacheAPISIX 可以放在一起,也可以分開。Admin API 接收用戶提交的請求,在請求參數(shù)保存到 ETCD 之前,會使用 JSON Schema 做一次完整校驗(yàn),有了校驗(yàn)可以確定到 ETCD 里的都是有效數(shù)據(jù)。
上圖右側(cè)是接收外部用戶的真實(shí)流量,ApacheAPISIX 從 ETCD 中訂閱所有配置規(guī)則,拿到配置規(guī)則后給到下面的路由引擎(libr3),目前默認(rèn)使用的路由引擎是 libr3,我之前在武漢的分享中進(jìn)行過詳細(xì)介紹(https://www.upyun.com/opentalk/428.html)。 libr3 是一個路由引擎實(shí)現(xiàn),基于前綴樹,由于他還支持正則,所以效率非常高的,同時功能也很強(qiáng)大。
ApacheAPISIX 的 v0.5 版本具備以下功能:
△ ApacheAPISIX 已有功能
Apache APISIX 的性能
△ 性能對比
通常來說,引入了前面提到的十幾項(xiàng)功能,會伴隨著性能的下降,那么究竟下降了多少呢?這里我做了一個性能的測試對比。如上圖,右側(cè)是我為了測試寫的一個虛假的服務(wù),這個服務(wù)里面空空如也,只是把 ngx_lua 里的一些變量拿出來,然后傳給了什么都不做的 fake_fetch,后面的 http filter、log 階段等一樣,沒有任何計(jì)算量。
然后對 ApacheAPISIX 和右邊的虛假服務(wù)分別跑壓力測定,對比結(jié)果發(fā)現(xiàn) ApacheAPISIX 的性能僅僅下降了 15%,也就是說在接受了 15% 的性能下降的同時,就可以享受前面提到的所有功能。
說一下具體數(shù)值,這里使用的是阿里云的計(jì)算平臺,單 worker 下可以跑到 23-24k QPS,4 worker 可以跑到 68k 的 QPS。
Apache APISIX 目前的狀態(tài)
目前jiao新版本是 v0.5,架構(gòu)是基于 ETCD+libr3+RapidJSON。這個版本加的jiao多的是代碼覆蓋率,v0.4 版本代碼覆蓋率不超過 5%,但jiao新版本中代碼覆蓋率達(dá)到 70%,這其中 95% 是核心代碼,周邊的代碼覆蓋率相對較低,主要是插件的相關(guān)測試有所欠缺。
原本計(jì)劃在 0.5 版本上線管理界面功能,這樣可以降低入門門檻,但是遺憾的是目前還沒開發(fā)完成,這與我們自身專業(yè)有關(guān)系,不擅長做前端界面,需要借助前端的專家?guī)臀覀儗?shí)現(xiàn),我們計(jì)劃會在 0.6 的版本上線(注:目前已經(jīng)發(fā)布了 v0.6 版本:https://github.com/iresty/apisix/blob/master/CHANGELOG_CN.md#060)。
OpenResty 編程哲學(xué)與優(yōu)化技巧
我從 2014 年開始做 OpenResty 開發(fā),至今已經(jīng)有六年了。在 OpenResty 的領(lǐng)域里,它的哲學(xué)是要學(xué)會大事化小,小事化了,因?yàn)?Nginx 的內(nèi)存管理方式是把所有的請求內(nèi)存默認(rèn)放到一個內(nèi)存池里,請求退出的時再把內(nèi)存池銷毀。如果不能很快地一進(jìn)一出,它就會不停申請,jiao后釋放時資源損耗很大,這是 Nginx 不擅長的。所以用 OpenResty 做長連接就需要非常小心,避免把內(nèi)存池搞大。
此外,要盡可能少地創(chuàng)建臨時對象。這里所指的臨時對象有兩類,一類是 table 類,一類是字符串拼接,比如某兩個變量拼接產(chǎn)生新的字符串,這個看似在其他很多語言都沒有問題,但在 OpenResty 里需要盡量少做這種操作。Lua 語言雖然簡單,但也是門高級語言,攜帶了優(yōu)良的 GC ,讓我們無需關(guān)心所有變量的生命周期,只負(fù)責(zé)申請就好了,但如果濫用臨時變量等,會讓 GC 比較忙碌,付出代價(jià)是整體運(yùn)行效能不高。Lua 擅長動態(tài)和流程控制,如果遇到硬核的 CPU 運(yùn)算任務(wù),還是推薦交給 C/C++ 實(shí)現(xiàn)。
今天和大家分享優(yōu)化技巧,主要還是如何寫好 Lua,畢竟他的受眾群體更多。在 ApacheAPISIX 的 core 中,我們使用了一些比較特別的優(yōu)化技巧,下面逐一給大家介紹。
技巧一:delay_json
△ delay_json
先說一下場景:比如上面的這行日志調(diào)用,如果當(dāng)前日志級別是 info ,我們期望會正常 json encode;而當(dāng)是 error 級別,我們就不期望發(fā)生 json encode 操作,如果能自動跳過是jiao完美了。那我們?nèi)绾谓频膶?shí)現(xiàn)這個目的呢?
我們看一下 delay_encode 的實(shí)現(xiàn)源碼,首先用元方法重載了 tostring ,下面 delay_encode 只是對 delay_tab 的兩個對象 data 和 force 做了賦值,然后沒有做其他的事情,這與大家平時看到的 json encode 方法都不一樣。因?yàn)檎嬲趯懭罩緯r,如果給定的參數(shù)是 table,在 OpenResty 里會把他轉(zhuǎn)成 string 的,過程是檢查是否有 tostring 的元方法注冊,如果有就調(diào)這個方法把它轉(zhuǎn)換成字符串。有了上面的封裝,我們就在高性能和易用性上做了很好的平衡。
技巧二:HASH vs 前綴樹 vs 遍歷Lua table 的 HASH:性能jiao好的匹配方式,缺點(diǎn)是只能做全量匹配。
前綴樹:借助 libr3 完成前綴等高級匹配(支持正則)。
遍歷:永遠(yuǎn)是jiao糟糕的。
在Apache APISIX 的世界里,我把 HASH 和前綴樹做了融合,如果你的請求和路由規(guī)則不包含高級規(guī)則匹配,會默認(rèn)走 HASH 來保證效率;但如果有模糊匹配邏輯,則使用前綴樹。
技巧三:ngx.log 是 NYI
△ngx.log
因?yàn)?ngx.log 是 NYI,所以我們要盡量減少下面這段代碼的觸發(fā)頻率:return ngx_log(log_level,…)
要降到j(luò)iao低,需要判斷當(dāng)前日志級別,如果當(dāng)前的日志級別和你輸入的日志級別存在大小比值關(guān)系,發(fā)現(xiàn)不需要輸入就直接 return。避免出現(xiàn)日志處理完,傳到 Nginx 內(nèi)核后再發(fā)現(xiàn)不需要寫日志,這樣就會浪費(fèi)非常多的資源。
前面提到的壓力測試,都是把日志打到 error 級別,加了非常多的調(diào)試代碼并且保留不刪,這些測試代碼的存在完全不會影響性能結(jié)果。
技巧四:gc for cdata and table
場景:當(dāng)某個 table 對象被系統(tǒng)回收時,希望觸發(fā)特定邏輯以釋放關(guān)聯(lián)資源。那么我們?nèi)绾谓o table 注冊 gc 呢?請參考下圖示例:
當(dāng)我們無法控制 Lua table 的整個生命周期,可以用上圖的方法去注冊一個 GC,當(dāng) table 對象沒有任何引用時會觸發(fā) GC,釋放關(guān)聯(lián)資源。
技巧五:如何保護(hù)常駐內(nèi)存的 cdata 對象
我們在使用 r3 這個 C 庫時遇到這么一個問題:我們給 r3 添加很多路由規(guī)則,然后生成 r3 tree,如果規(guī)則沒有變化 r3 將被反復(fù)使用,由于 r3 內(nèi)部沒有申請額外的內(nèi)存存儲,只是引用指針地址。但外面?zhèn)魅氲?Lua 變量可能是臨時變量,引用計(jì)數(shù)為 0 后會被 Lua GC 自動回收。導(dǎo)致的現(xiàn)象是 r3 內(nèi)部引用的原有內(nèi)存地址內(nèi)容突然發(fā)生變化,jiao后致使路由匹配失敗。
知道了問題原因,解決方法就比較簡單了,只需要避免變量 A 提前釋放,讓 Lua 里面變量 A 的生命周期和 r3 對象的生命周期保持一致即可。
技巧六:ngx.var.* 是比較慢的
大家知道 C 是不支持動態(tài)的,它是編譯性語言。ngx.var.* 的內(nèi)部實(shí)現(xiàn)可以查看 Nginx 源代碼,或者通過火焰圖的方式可以看到他內(nèi)部的實(shí)現(xiàn)方式。為了完成動態(tài)獲取變量,內(nèi)部必須通過一次 hash 查找,到后用內(nèi)部的規(guī)則把變量值讀出。
解決方案是用上圖這個庫(github.com/iresty/lua-var-nginx-module),非常簡單沒有技術(shù)含量的辦法。比如要獲取客戶端的 IP,在 C 里面直接把代碼摘出來,然后通過 Lua FFI 方式讀取變量的值,就是這么一段小代碼可以讓 ApacheAPISIX 性能有 5% 提升。這么做缺點(diǎn)是必須要對 OpenResty 編譯時添加這個第三方模塊,上手成本略高。
技巧七:減少每請求的垃圾對象
我們要盡可能降低每請求產(chǎn)生的垃圾對象的數(shù)量,作為 OpenResty 開發(fā)者,如果把這句話理解透徹,基本上可以進(jìn)階到前 50% 的行列。
減少不必要的字符串的拼接,并非意味著在需要做拼接字符串的時候不要拼接,而是需要在腦子里一直有這個意識,把無效的拼接降低下來,當(dāng)這些小細(xì)節(jié)累積下來,性能提升就會非常大。
技巧八:重用 table
△ 請求之間可以共享 table
首先介紹下初級版的 table.clear。當(dāng)需要使用一個臨時 table,大家習(xí)慣性的寫法是local t ={}
我們來聊聊這么做的缺點(diǎn),如果在開頭創(chuàng)建了一個臨時的 table t,當(dāng)函數(shù)退出的時候,t 會被回收;下次再進(jìn)來這個函數(shù),又會產(chǎn)生一個臨時的 table t。在 Lua 世界,table 的產(chǎn)生和銷毀是非常耗資源的,因?yàn)?table 是一個復(fù)雜對象,它不像 number、字符串等簡單對象,申請和釋放可以用一個結(jié)構(gòu)體搞定,它會讓你的 GC 一下子變得非常忙碌。
如果 worker 里只需要一個唯一實(shí)例 table 對象,那么就可以使用 table.clear 方式來反復(fù)使用這個臨時表,比如上圖的臨時表 local_plugins_hash。
重用 table :進(jìn)階版 table.pool
△ 請求不能共享 table
有些 Lua table 的生命周期是每請求的,通常是請求進(jìn)入申請對象,請求退出釋放對象,這時候使用 table.pool 會非常合適。tablepool 中文翻譯過來是表池,里面放的是可以重用的 table。官方文檔可以到 https://github.com/openresty/lua-tablepool#synopsis 查看,結(jié)合 ApacheAPISIX 的業(yè)務(wù)使用代碼,更容易理解。
在 ApacheAPISIX 中jiao集中使用的是兩個地方,除了上圖這里做回收,還有是申請的地方。在回收之后,這些 table 可以被其他請求所復(fù)用,由 tablepool 做統(tǒng)一控制,在 pool 里維持的對象可能就固定的幾十、幾百個,會反復(fù)使用,不存在銷毀的情況。這個技巧的正確使用,性能至少可以提升 20%,提升效果非常明顯。
技巧九:Irucache 的正確姿勢
簡單介紹下 Irucache,Irucache 可以完成在 worker 內(nèi)的數(shù)據(jù)的緩存和復(fù)用,Irucache 有一個非常大的優(yōu)勢是可以存儲任何對象。而共享內(nèi)存則是完成不同 worker 之間的數(shù)據(jù)共享,但它只能存儲簡單對象,有些東西是不能跨 worker 共享,比如 function、cdata 對象等。
對 Irucache 進(jìn)行二次封裝,封裝的內(nèi)容主要包括:key 要盡量短、簡單:我們在寫 key 時jiao重要的是要簡單,key jiao糟糕的設(shè)計(jì)是里面東西很長,但是有用信息不多。key 理論上大家都喜歡用字符串,但他可以是 table 等對象,key 盡量做到明確,只包含你感興趣的內(nèi)容,能省略的盡量省略,降低拼接成本。
version 可降低垃圾緩存:這點(diǎn)算是我在做 ApacheAPISIX 的突破:提取出了 version, Irucache+ version 這套組合,可以極大地降低垃圾緩存。
重用 stale 狀態(tài)的緩存數(shù)據(jù)。
△ lrucache 接?口簡化
上圖是 lrucache 的封裝,從下往上看,key 是 /routes,它跟的版本號是 conf_version,global 函數(shù)里做的事情是根據(jù) key+version 的方式,去查找有無陳舊數(shù)據(jù)的緩存,如果有就直接返回,如果沒有就調(diào) creat_r3_router 完成創(chuàng)建,creat_r3_router 是負(fù)責(zé)創(chuàng)建一個新的對象,它只接受一個傳參 routes,這個傳參是由 routes.values 傳進(jìn)去的。
這層封裝,把 Irucache new、數(shù)量等都隱藏起來,這樣很多東西我們看不到,當(dāng)我們需要自定義的時候可能還是需要關(guān)心這些。ApacheAPISIX 為了簡化插件開發(fā)者對各種東西的理解,所以必須要做一層封裝,簡化使用。
△ lrucache jiao佳實(shí)踐?例
上圖是用 version 降低垃圾緩存、重用 stale 狀態(tài)的緩存數(shù)據(jù),這 Irucache 的二次封裝的代碼。首先來看第二行,根據(jù) key 去緩存里面取對象,然后把對象的 cache_ver 拿出來和當(dāng)前傳入的 version 做比較,如果相同則判定這個緩存對象一定是可用的。
往下多了 stale_obj,stale_obj 在文檔里面說明的比較少,它只有在一種情況會發(fā)生:緩存對象在 Irucache 中已經(jīng)被淘汰了,但是它只是到了淘汰的邊緣,還沒有完全被扔掉。上圖中通過陳舊數(shù)據(jù)的 cache_ver 與進(jìn)來的 version 做比較,如果 version 一致那就是有效的。所以只要源頭的數(shù)據(jù)沒有變化,就可以再次使用。這樣我們就可以復(fù)用 stale_obj 從而避免再次創(chuàng)建新的對象。
到這里可以解釋一下前面提到的:version 可降低垃圾緩存。如果沒有 version,我們需要把 version 寫到 key 里面,每次 version 變化都會產(chǎn)生一個新的 key,那些被淘汰的舊數(shù)據(jù)會一直存在,沒辦法剔除掉。同時意味著 Irucache 里面的對象數(shù)會不停增加。而我們前面的方式是保證 key 如果是一個對象,只會有一個 table 與它對應(yīng),不會根據(jù)不同的 version 產(chǎn)生不同的對象緩存,進(jìn)而降低緩存總數(shù)。
以上是我今天的全部分享,謝謝大家!
總結(jié)
以上是生活随笔為你收集整理的apisix实际应用_Apache APISIX 的高性能实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: idea 升级到2020后 无法启动_I
- 下一篇: cron表达式 每隔1个小时_2-1!1