OpenResty 社區王院生:APISIX 的高性能實踐

2019 年 7 月 6 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍·上海站,OpenResty 軟件基金會聯合創始人王院生在活動上作了《APISIX 的高性能實踐》的分享。html

OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推進 OpenResty 開源項目的發展。活動將陸續在深圳、北京、武漢、上海、成都、廣州、杭州等城市巡迴舉辦。前端

王院生,OpenResty 社區、OpenResty 軟件基金會聯合創始人,《OpenResty 最佳實踐》主要做者,APISIX 項目發起人和主要做者。nginx

如下是分享全文:git

你們好,我是王院生,很高興來到上海。首先作下自我介紹,我於 2014 年加入奇虎 360,在那時認識了 OpenResty,此前我是一個純粹的 C/C++ 語言開發者。在 360 工做期間,利用工做閒暇時間寫了《OpenResty 最佳實踐》,但願能影響更多的人正確掌握 OpenResty 入門。2017 年我做爲技術合夥人和春哥(章亦春,agentzh)一塊兒創業。今年我我的的重心有所調整並在今年三月份離職,準備將更多精力投入到開源上,因而發起了 APISIX 這個項目,企業宗旨是依託開源社區,致力於微服務 API 相關技術的創新和實現。github

什麼是 API 網關

API 網關的地位愈來愈重要,它幾乎劫持了全部流量,內外之間完成了用戶的安全控制、審計,經過自定義插件的方式知足企業自身特定需求,最多見的自由身份認證等。隨着服務在數量和複雜度上的不斷增加,更多的企業採用了微服務的方式,這時經過 API 網關來完成統一的流量管理和調度就很是有必要。編程

微服務網關和傳統意義上的 API 網關有一些不一樣,主要包括下面幾點:json

  1. 動態更新:在微服務以前,服務不像如今這樣常常來回地變化。好比微服務須要作橫向擴充,或者故障恢復、熱備、切換等,IP 、節點等變更更加頻繁。舉例如微博上一旦出現了爆點事件,就急速擴充計算點,必需要很是快地擴充新機器來扛壓。波峯波谷變化明顯,分鐘級別的機器動態管理,已經愈加是常態。
  2. 更低延遲:一般動態就意味着可能會作一些延遲(複雜度增長),在微服務裏面,對於延遲要求比較高,尤爲對於如今的用戶體驗,超過 1 秒以上的延遲是徹底不可接受的。
  3. 用戶自定義插件:API 網關是給企業用戶使用的,它必定存在私有邏輯(好比特殊的認證受權等),因此微服務網關必須可以支持企業用戶自定義插件。
  4. 更集中的管理 API:如前面所說 API 網關劫持了用戶的全部流量,因此用網關來作統一的 API 管理是很是必要的。在網關角度能夠看到 API 是如何設計,是否存在延遲、安全問題,以及響應速度和健康信息等。

咱們要作的微服務 API 網關產品,除了上面的基本要求,還有一些是咱們區別於其餘人的:api

  1. 經過社區聚焦:經過開源方式聚焦有共同需求的人羣,讓更多不一樣公司的人能夠一塊兒協做,共同打磨更好的產品,減小冗餘開發。
  2. 簡潔的 core:產品的內核必須是很是簡潔的,若是內核複雜,會使得你們的上手成本高不少,望而卻步確定不是咱們指望的。
  3. 可擴展性、頂級性能、低延遲:這幾項都是要同時嚴格保障的,也是咱們會花主要精力保證的。目前 APISIX 項目的性能比空跑 OpenResty 只低 15%,這點仍是很是值得傲嬌的。

APISIX 高性能微服務網關

APISIX 架構與功能

上圖是 APISIX 的基本架構,羅列用到的幾個基本組件。其中包括 ETCD 能夠完成配置存儲,因爲 ETCD 能夠走集羣,因此咱們能夠借用它完成動態伸縮、高可用集羣等。ETCD 數據支持經過 watch 的方式增量獲取,使得 APISIX 節點規則更新能夠作到毫秒級,甚至更低。APISIX 自身是無服務狀態的,因此方便橫向擴充。緩存

另外一個組件是 JSON Schema,它是一個標準協議,主要用來驗證數據的有效性。JSON Schema 目前對外公開有四個不一樣版本,咱們最終選用 RapidJSON,由於他對這四個版本都有相對完整的支持。安全

圖中的 Admin API 和 APISIX 能夠放在一塊兒,也能夠分開。Admin API 接收用戶提交的請求,在請求參數保存到 ETCD 以前,會使用 JSON Schema 作一次完整校驗,有了校驗能夠肯定到 ETCD 裏的都是有效數據。

上圖右側是接收外部用戶的真實流量,APISIX 從 ETCD 中訂閱全部配置規則,拿到配置規則後給到下面的路由引擎(libr3),目前默認使用的路由引擎是 libr3,我以前在武漢的分享中進行過詳細介紹(www.upyun.com/opentalk/42…)。 libr3 是一個路由引擎實現,基於前綴樹,因爲他還支持正則,因此效率很是高的,同時功能也很強大。

APISIX 的 v0.5 版本具有如下功能:

APISIX 的性能

一般來講,引入了前面提到的十幾項功能,會伴隨着性能的降低,那麼究竟降低了多少呢?這裏我作了一個性能的測試對比。如上圖,右側是我爲了測試寫的一個虛假的服務,這個服務裏面空空如也,只是把 ngxlua 裏的一些變量拿出來,而後傳給了什麼都不作的 fakefetch,後面的 http filter、log 階段等同樣,沒有任何計算量。

而後對 APISIX 和右邊的虛假服務分別跑壓力測定,對比結果發現 APISIX 的性能僅僅降低了 15%,也就是說在接受了 15% 的性能降低的同時,就能夠享受前面提到的全部功能。

說一下具體數值,這裏使用的是阿里雲的計算平臺,單 worker 下能夠跑到 23-24k QPS,4 worker 能夠跑到 68k 的 QPS。

APISIX 目前的狀態

目前最新版本是 v0.5,架構是基於 ETCD+libr3+RapidJSON。這個版本加的最多的是代碼覆蓋率,v0.4 版本代碼覆蓋率不超過 5%,但最新版本中代碼覆蓋率達到 70%,這其中 95% 是核心代碼,周邊的代碼覆蓋率相對較低,主要是插件的相關測試有所欠缺。

本來計劃在 0.5 版本上線管理界面功能,這樣能夠下降入門門檻,可是遺憾的是目前還沒開發完成,這與咱們自身專業有關係,不擅長作前端界面,須要藉助前端的專家幫咱們實現,咱們計劃會在 0.6 的版本上線(注:目前已經發布了 v0.6 版本:github.com/iresty/apis…)。

OpenResty 編程哲學與優化技巧

我從 2014 年開始作 OpenResty 開發,至今已經有六年了。在 OpenResty 的領域裏,它的哲學是要學會大事化小,小事化了,由於 Nginx 的內存管理方式是把全部的請求內存默認放到一個內存池裏,請求退出的時再把內存池銷燬。若是不能很快地一進一出,它就會不停申請,最後釋放時資源損耗很大,這是 Nginx 不擅長的。因此用 OpenResty 作長鏈接就須要很是當心,避免把內存池搞大。

此外,要儘量少地建立臨時對象。這裏所指的臨時對象有兩類,一類是 table 類,一類是字符串拼接,好比某兩個變量拼接產生新的字符串,這個看似在其餘不少語言都沒有問題,但在 OpenResty 裏須要儘可能少作這種操做。Lua 語言雖然簡單,但也是門高級語言,攜帶了優良的 GC ,讓咱們無需關心全部變量的生命週期,只負責申請就行了,但若是濫用臨時變量等,會讓 GC 比較忙碌,付出代價是總體運行效能不高。Lua 擅長動態和流程控制,若是遇到硬核的 CPU 運算任務,仍是推薦交給 C/C++ 實現。

今天和你們分享優化技巧,主要仍是如何寫好 Lua,畢竟他的受衆羣體更多。在 APISIX 的 core 中,咱們使用了一些比較特別的優化技巧,下面逐一給你們介紹。

技巧一:delay_json

先說一下場景:好比上面的這行日誌調用,若是當前日誌級別是 info ,咱們指望會正常 json encode;而當是 error 級別,咱們就不指望發生 json encode 操做,若是能自動跳過是最完美了。那咱們如何近似的實現這個目的呢?

咱們看一下 delayencode 的實現源碼,首先用元方法重載了 tostring ,下面 delayencode 只是對 delay_tab 的兩個對象 data 和 force 作了賦值,而後沒有作其餘的事情,這與你們平時看到的 json encode 方法都不同。由於真正在寫日誌時,若是給定的參數是 table,在 OpenResty 裏會把他轉成 string 的,過程是檢查是否有 tostring 的元方法註冊,若是有就調這個方法把它轉換成字符串。有了上面的封裝,咱們就在高性能和易用性上作了很好的平衡。

技巧二:HASH vs 前綴樹 vs 遍歷

  • Lua table 的 HASH:性能最好的匹配方式,缺點是隻能作全量匹配。
  • 前綴樹:藉助 libr3 完成前綴等高級匹配(支持正則)。
  • 遍歷:永遠是最糟糕的。

在 APISIX 的世界裏,我把 HASH 和前綴樹作了融合,若是你的請求和路由規則不包含高級規則匹配,會默認走 HASH 來保證效率;但若是有模糊匹配邏輯,則使用前綴樹。

技巧三:ngx.log 是 NYI

由於 ngx.log 是 NYI,因此咱們要儘可能減小下面這段代碼的觸發頻率:

return ngx_log(log_level,…)複製代碼

要降到最低,須要判斷當前日誌級別,若是當前的日誌級別和你輸入的日誌級別存在大小比值關係,發現不須要輸入就直接 return。避免出現日誌處理完,傳到 Nginx 內核後再發現不須要寫日誌,這樣就會浪費很是多的資源。

前面提到的壓力測試,都是把日誌打到 error 級別,加了很是多的調試代碼而且保留不刪,這些測試代碼的存在徹底不會影響性能結果。

技巧四:gc for cdata and table

場景:當某個 table 對象被系統回收時,但願觸發特定邏輯以釋放關聯資源。那麼咱們如何給 table 註冊 gc 呢?請參考下圖示例:

當咱們沒法控制 Lua table 的整個生命週期,能夠用上圖的方法去註冊一個 GC,當 table 對象沒有任何引用時會觸發 GC,釋放關聯資源。

技巧五:如何保護常駐內存的 cdata 對象

咱們在使用 r3 這個 C 庫時遇到這麼一個問題:咱們給 r3 添加不少路由規則,而後生成 r3 tree,若是規則沒有變化 r3 將被反覆使用,因爲 r3 內部沒有申請額外的內存存儲,只是引用指針地址。但外面傳入的 Lua 變量多是臨時變量,引用計數爲 0 後會被 Lua GC 自動回收。致使的現象是 r3 內部引用的原有內存地址內容忽然發生變化,最後導致路由匹配失敗。

知道了問題緣由,解決方法就比較簡單了,只須要避免變量 A 提早釋放,讓 Lua 裏面變量 A 的生命週期和 r3 對象的生命週期保持一致便可。

技巧六:ngx.var.* 是比較慢的

你們知道 C 是不支持動態的,它是編譯性語言。ngx.var.* 的內部實現能夠查看 Nginx 源代碼,或者經過火焰圖的方式能夠看到他內部的實現方式。爲了完成動態獲取變量,內部必須經過一次 hash 查找,到後用內部的規則把變量值讀出。

解決方案是用上圖這個庫(github.com/iresty/lua-…),很是簡單沒有技術含量的辦法。好比要獲取客戶端的 IP,在 C 裏面直接把代碼摘出來,而後經過 Lua FFI 方式讀取變量的值,就是這麼一段小代碼可讓 APISIX 性能有 5% 提高。這麼作缺點是必需要對 OpenResty 編譯時添加這個第三方模塊,上手成本略高。

技巧七:減小每請求的垃圾對象

咱們要儘量下降每請求產生的垃圾對象的數量,做爲 OpenResty 開發者,若是把這句話理解透徹,基本上能夠進階到前 50% 的行列。

減小沒必要要的字符串的拼接,並不是意味着在須要作拼接字符串的時候不要拼接,而是須要在腦子裏一直有這個意識,把無效的拼接下降下來,當這些小細節累積下來,性能提高就會很是大。

技巧八:重用 table

首先介紹下初級版的 table.clear。當須要使用一個臨時 table,你們習慣性的寫法是

local t ={}複製代碼

咱們來聊聊這麼作的缺點,若是在開頭建立了一個臨時的 table t,當函數退出的時候,t 會被回收;下次再進來這個函數,又會產生一個臨時的 table t。在 Lua 世界,table 的產生和銷燬是很是耗資源的,由於 table 是一個複雜對象,它不像 number、字符串等簡單對象,申請和釋放能夠用一個結構體搞定,它會讓你的 GC 一會兒變得很是忙碌。

若是 worker 裏只須要一個惟一實例 table 對象,那麼就可使用 table.clear 方式來反覆使用這個臨時表,好比上圖的臨時表 localpluginshash。

重用 table :進階版 table.pool

有些 Lua table 的生命週期是每請求的,一般是請求進入申請對象,請求退出釋放對象,這時候使用 table.pool 會很是合適。tablepool 中文翻譯過來是表池,裏面放的是能夠重用的 table。官方文檔能夠到 github.com/openresty/l… 查看,結合 APISIX 的業務使用代碼,更容易理解。

在 APISIX 中最集中使用的是兩個地方,除了上圖這裏作回收,還有是申請的地方。在回收以後,這些 table 能夠被其餘請求所複用,由 tablepool 作統一控制,在 pool 裏維持的對象可能就固定的幾10、幾百個,會反覆使用,不存在銷燬的狀況。這個技巧的正確使用,性能至少能夠提高 20%,提高效果很是明顯。

技巧九:Irucache 的正確姿式

簡單介紹下 Irucache,Irucache 能夠完成在 worker 內的數據的緩存和複用,Irucache 有一個很是大的優點是能夠存儲任何對象。而共享內存則是完成不一樣 worker 之間的數據共享,但它只能存儲簡單對象,有些東西是不能跨 worker 共享,好比 function、cdata 對象等。

對 Irucache 進行二次封裝,封裝的內容主要包括:

  • key 要儘可能短、簡單:咱們在寫 key 時最重要的是要簡單,key 最糟糕的設計是裏面東西很長,可是有用信息很少。key 理論上你們都喜歡用字符串,但他能夠是 table 等對象,key 儘可能作到明確,只包含你感興趣的內容,能省略的儘可能省略,下降拼接成本。
  • version 可下降垃圾緩存:這點算是我在作 APISIX 的突破:提取出了 version, Irucache+ version 這套組合,能夠極大地下降垃圾緩存。
  • 重用 stale 狀態的緩存數據。

上圖是 lrucache 的封裝,從下往上看,key 是 /routes,它跟的版本號是 confversion,global 函數裏作的事情是根據 key+version 的方式,去查找有無陳舊數據的緩存,若是有就直接返回,若是沒有就調 creatr3router 完成建立,creatr3_router 是負責建立一個新的對象,它只接受一個傳參 routes,這個傳參是由 routes.values 傳進去的。

這層封裝,把 Irucache new、數量等都隱藏起來,這樣不少東西咱們看不到,當咱們須要自定義的時候可能仍是須要關心這些。APISIX 爲了簡化插件開發者對各類東西的理解,因此必需要作一層封裝,簡化使用。

image

△ lrucache 最佳實踐示例

△ lrucache 最佳實踐⽤例

上圖是用 version 下降垃圾緩存、重用 stale 狀態的緩存數據,這 Irucache 的二次封裝的代碼。首先來看第二行,根據 key 去緩存裏面取對象,而後把對象的 cache_ver 拿出來和當前傳入的 version 作比較,若是相同則斷定這個緩存對象必定是可用的。

往下多了 staleobj,staleobj 在文檔裏面說明的比較少,它只有在一種狀況會發生:緩存對象在 Irucache 中已經被淘汰了,可是它只是到了淘汰的邊緣,尚未徹底被扔掉。上圖中經過陳舊數據的 cachever 與進來的 version 作比較,若是 version 一致那就是有效的。因此只要源頭的數據沒有變化,就能夠再次使用。這樣咱們就能夠複用 staleobj 從而避免再次建立新的對象。

到這裏能夠解釋一下前面提到的:version 可下降垃圾緩存。若是沒有 version,咱們須要把 version 寫到 key 裏面,每次 version 變化都會產生一個新的 key,那些被淘汰的舊數據會一直存在,沒辦法剔除掉。同時意味着 Irucache 裏面的對象數會不停增長。而咱們前面的方式是保證 key 若是是一個對象,只會有一個 table 與它對應,不會根據不一樣的 version 產生不一樣的對象緩存,進而下降緩存總數。

以上是我今天的所有分享,謝謝你們!

演講視頻及PPT下載傳送門:

APISIX 的高性能實踐

相關文章
相關標籤/搜索