再談 APISIX 高性能實踐

2019 年 8 月 31 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍·成都站,APISIX 主要做者王院生在活動上作了《APISIX 高性能實踐》的分享。html

OpenResty × Open Talk 全國巡迴沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推進 OpenResty 開源項目的發展。nginx

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

如下是分享全文:算法

首先作下自我介紹,我大學畢業後在傳統金融行業工做九年,2014 年加入奇虎 360,期間撰寫了《OpenResty 最佳實踐》。我我的比較喜歡研究技術和開源,多是受老羅影響,喜歡嘗試理想化的事情。今年 3 月份與志同道合的夥伴一塊兒創辦了深圳支流科技公司,這是一家以開源方式創業的科技公司,在國內屈指可數,APISIX 是咱們目前的主要項目。編程

APISIX 是微服務 API 網關產品,今年 7 月份我在上海作過一次關於「 APISIX 高性能實踐」的分享,此次的內容是在上次分享的基礎上,並會將最近的新積累分享給你們。json

什麼是 API 網關

API 網關的地位愈來愈重要,它是全部流量的出入口,從圖中能夠看到請求方可能來自於瀏覽器、loT 設備以及移動設備等,API 網關做爲中間管控層須要作安全控制、流量以及日誌記錄等。愈來愈多的企業採用了微服務的方式,以此完成內部解耦、靈活部署、彈性伸縮等技術特性從而知足業務需求。微服務的數量和複雜度也都隨之水漲船高,經過 API 網關來完成統一的流量管理調度就很是必要,並對 API 網關提出了更高要求。api

APISIX 概述

上圖是 APISIX 的基本構架,因爲要支持集羣和高可用,因此在任何一個節點都須要包含 adminAPI 或 APISIX 內核,使用時能夠只啓用其中一部分或都啓用。admin API 主要用於接收管理員的提交信息,經過 json schema 完成參數的校驗,防止非法參數落到存儲的配置中心。APISIX 內部部分處理外部請求,根據請求特徵,匹配到具體路由規則,執行插件,而後把流量轉發到指定上游服務。數組

APISIX 每月會發佈一個版本,在 0.7 版本支持了路由插件化,很自豪地說這是目前惟一容許自定義路由的 API 網關實現。除了以前已有的 r3 路由,APISIX 新增了專門高性能的前綴匹配 radixtree,radixtree 是由 Redix 的做者開源出來的。radixtree 代碼的匹配效率是 r3 的 10 倍甚至更高,一些生產用戶升級 radixtree 後 CPU 使用率確實降低明顯。瀏覽器

上圖顯示的是兩個月前 APISIX 已有的功能。緩存

最近的兩個月,APISIX 增長了以上新功能,每月大概都會有 五、6 個大的新特性,若是我只準備 APISIX 裏的一些新特性與你們分享,各位受益可能會比較小,因此今天我給你們分享一些通用的 OpenResty 編程技巧。

APISIX 主打的是高性能,咱們與 OpenResty 對比性能,這樣更能突出 APISIX 性能的極致。首先用 APISIX 完整服務來壓測,對比一個沒有任何功能的空 OpenResty 服務,發現 APISIX 在加載了全部功能的狀況下只降低了 15% 的性能。換言之,你若是能接受 15% 的性能降低,就能夠直接享受上圖的全部功能。

OpenResty 優化技巧

路由:radixtree vs r3

既然已經有了 r3 ,爲何咱們還要繼續用 resty-radixtree 實現新的路由呢?

先介紹 r3 的問題:r3 的學習複雜度比較高(正則自己就有學習難度),而且不支持經過迭代器的方式迭代匹配結果,效率相比前綴樹實現低很多。相反這些問題在 resty-radixtree 上都有完美解決方案,性能、穩定性天然也就提高不少。目前的 resty-radixtree 是基於 antirez/rax 實現的,也是 Redis 的做者寫的,站在巨人肩膀可讓咱們少走很多彎路。

從數據結構上看,前綴樹理論上是比哈希算法更快,緣由是哈希算法的真正複雜度是O(K),K 是指查詢的 Key 的長度,Key 越長哈希算法把字符串變成整數就越複雜,而前綴樹是層層遞進,最壞的複雜度就是 O(K),所以前綴樹的最壞效率與哈希算法是同樣的。

固然這只是原理上的,通過專門測試發現 Lua table 的哈希查找速度秒殺前綴樹,這是由於在編譯 LuaJIT 的時候,它使用了 CPU 指令集來計算哈希值,這樣能夠完美的作到 O(1),因此 LuaJIT table 的哈希是效率是最高的,其次纔是前綴樹。

在 LuaJIT 世界匹配效率最高,永遠都是先優先使用 Lua table 的哈希匹配。咱們最終也沒直接使用前綴樹(trietree),由於它比較消耗內存,而是採用了基數樹(radixtree),在性能相差很少的狀況下,內存佔用更小。

OpenResty VS Golang、HTTP VS gRPC

2015 年我沒有選擇 Golang 而選擇 OpenResty,緣由是我認爲 OpenResty 能夠思考地更深刻,而 Golang 只能站在應用層去解決問題,出於這個緣由我選擇了 OpenResty。

APISIX 支持了這樣一個場景:HTTP(s) -> APISIX -> gRPC server,把 REST API 轉成 gRPC 請求。完成該功能後,須要作些壓力測試驗證效果。爲了方便對比,用 Golang 的方式也寫了一個協議轉換網關。測試發現 APISIX 的版本比 Golang 的版本性能略還好一點,個人電腦上都是單核 1 萬左右的 QPS。本覺得在 gRPC 領域 Golang 的性能應當是最好的,沒想到 APISIX 有機會略勝一籌。

咱們以前粗淺地認爲 HTTP 的性能必定沒有 gRPC 的性能好,如今看有點武斷。gRPC 的不少優點是 HTTP 不具有的,好比它的體積更小且內置 schema 檢查等。但若是你的請求體比較小,在 HTTP 上使用 json 加 json schema,它們倆的性能幾乎相同,尤爲是在內網環境下相差仍是很是小的。若是請求體比較大編碼複雜,那麼 gRPC 會有明顯優點。

ngx.var 的加速

對獲取 Nginx 變量的加速,最簡單的就是用 iresty/lua-var-nginx-module 倉庫,把它做爲一個 lua module 編譯到 OpenResty 項目裏。當咱們提取對應的 ngx.var 的時,使用庫裏提供的方法來獲取,可讓 APISIX 總體有 5% 的性能提高,單純某個變量性能對比,至少有 10 倍差異。固然也能夠把這個模塊編譯成動態庫,而後用動態方式加載,這樣就不用從新編譯 OpenResty。

APISIX 網關會從 ngx.var 裏獲取大量變量信息,好比 host 地址等變量更是可能會被反覆獲取,每次都與 Nginx 交互效率會比較低。所以咱們在 APISIX/core 里加了一層 ctx 緩存,也就是第一次與 Nginx 交互獲取變量,後面將直接使用緩存。

題外篇:再次推薦你們多參考借鑑 APISIX/core 中的代碼,這些代碼是通用的,對大多數項目都應該有借鑑意義。

fail to json encode

當咱們用 json 的方式去 encode 一個 table時,可能會失敗。失敗緣由有如下幾種:好比 table 中包含 cdata 或者 userdata 沒法 encode ,又或者包含 function 等,但實際上咱們作 encode 並非想要一個能夠完美支持序列化/反序列化的結果,有時候只是爲了調試。

因此我在 APISIX 的 core/json_encode 增長了一個布爾參數,表示是否進行強制轉碼,這樣當遇到不能轉碼時就把強制它變成一個字符串。此外 table 套 table 是一個常見的狀況,即有一個 table A,在 A 的 table 裏面的內部又引用了A 自身,造成了一個循環嵌套。這個問題的解決比較簡單,在發生嵌套時,到達某一個位置點後就不要再往裏嵌了。這兩個場景下容許強制 table encode 對咱們開發調試很是有用。

在調試時,若是須要打一下 table 結果,當日志級別不夠時,不該該觸發無心義的 jsonencode 行爲,這時候推薦使用 delay_encode 來調試日誌,只有當日志真正須要寫到磁盤上時,纔會觸發 json encode,避免那些不須要 encode 。這個問題在APISIX 裏面效果很是好,終於不須要註釋代碼就能夠完成不一樣級別日誌的測試,有點 C 語言中宏定義的味道,對性能和易用是個極好的平衡。

靜態代碼檢測工具

目前 APISIX 進行 CI 迴歸,都會運行代碼檢查工具進行檢查:Lua -check 和 lj-releng。對當前代碼目錄的內容作靜態檢測,好比有沒有加全局的變量,代碼行的長度是否是超了等。

rapid json 的生命週期

調試過程當中發現的一個特別有意思的關於 rapid json 聲明週期的 bug。關於這個週期的緣由能夠看一下上圖的最後一行,咱們真正使用的是 validator,並且只調用了validator 的一個驗證,它是從上邊的 create-validator 得來的。這裏值得注意的是,爲何用一個數組緩存住另外一個叫 sd 的對象呢?

由於 validator 是個 cdata ,內部有對 sd 對象的指針引用依賴,他們兩個也就必需要有相同的聲明週期,不能有某一項提早釋放的狀況。若是咱們須要讓兩個對象有相同的生命週期,那麼把它們放到同一個 table 中是最簡單的方法。

第三方庫使用 pcre

若是你選擇效率最高的 C 庫,而這個 C 庫裏還引用了 pcre 這個庫,那就須要考慮到一個問題,這個對象的跨請求就會有很是大的風險,爲此必需要單獨給這個庫建立獨立的內存池,決不能使用當前請求的內存池,由於當前請求很快就被釋放。

怎麼解決這個問題呢?若是如今 OpenResty 有相關的 API,那麼直接去申請內存池是最好的,可是惋惜 OpenResty 並不具有。看看 Ningx 源碼,能夠看到建立 Nginx 的內存池函數定義是 ngx_create_pool(size_tsize, void *log) ,只要能獲取到全局日誌句柄便可。

咱們選擇從全局的 ngx_cycle 獲取 log 對象,這裏我定義了一個虛假的 fake_ngx_cycle 結構體,這個結構體和 Nginx 的 ngx_cycle 構體的前三項是同樣的,可是截掉了後面的部分,而後咱們作內存拷貝,從而獲得了 log 對象指針位置。

開啓 prometheus 插件

我當時在研究 Kong 的 prometheus 插件時粗略看了一下它的代碼,發現他的實現邏輯是有問題的,會很是影響性能。因此沒有直接使用 Kong 的方式,在 APISIX 中開啓這個 插件,性能只會降低5% 左右。這個插件咱們比 Kong 高近 10 倍性能。我也和 Kong 的技術負責人聊過這裏,後續會把 APISIX 的一些作法貢獻給 Kong,相互學習一塊兒成長。

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

演講PPT下載及視頻觀看:

Apache APISIX 微服務網關極致性能架構解析

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索