Kong 0.12.3 的一處記憶體洩漏分析
Kong 0.12.3 是最後一個以 API 形式組織介面的版本,後續的版本中 Kong 新增了 Service 和 Route 的概念,對於外掛的應用規則更加複雜,當然也更為靈活。不過就我個人而言,我更喜歡直接以 API 的形式來管理介面,簡單粗暴,所以也就用 0.12.3 這個版本多一些。
然而這個版本當開啟 bot-detection 外掛的時候會有比較嚴重的記憶體洩漏問題。不過,復現這個 Bug 有兩個前提:
-
OpenResty 需要配置多個 worker,對應 Kong 配置:
nginx_worker_processes
-
db_cache_ttl
必須配置為 0,也就是讓 Kong 的快取永不過期
滿足上面的條件之後,我們為 Kong 新增一個 API 並配置一個全域性 bot-detection 外掛:
接下來通過 ab
壓測這個 API:
壓測過程中,可以看到 Nginx 的一個 worker 的記憶體佔用在持續上升:
通過 pmap-d2310
可以進一步確認實際記憶體佔用情況:
不過現在依然無法確定到底是 OpenResty 的問題,還是 LuaJit 導致的洩漏。這裡我們繼續使用 lj-gc 來分析 LuaJit GC 的情況:
至此, 可以基本確定是 LuaJit 的問題了 。接下來,繼續使用 lj-gc-objs 來分析 GC 的詳細資料:
可以看到 cdata 的佔用極大,似乎是一個 FFI 的問題 。接著使用 sample-bt-leaks 來生成火焰圖,看下到底是哪個呼叫導致:
貌似並沒啥異常。不過,在 bot-detection 中涉及到 FFI 呼叫的只有兩處:
-
ngx.re.find
用來匹配 UA -
resty.lrucache
用來快取 UA
然而,這個記憶體洩漏的問題在 Kong 1.0 中卻無法復現出來。通過對比這兩個版本的 bot-detection 外掛原始碼,幾乎沒有什麼區別,所以可以暫時斷定: 記憶體洩漏不是由 bot-detection 外掛自身導致,同時也可以排除掉 ngx.re.find
、 resty.lrucache
自身的問題 。
問題視乎走到了僵局。再次回顧 bot-detection 外掛原始碼,看到下面這段:
這一段的主要功能就是初始化 ua 的快取, ua_caches
是一張 weak table。這麼做的好處就是,一旦外掛的 blacklist
、 whitelist
發生變更,那麼這個 ua_caches
就會被自動 GC 掉,以降低 worker 的記憶體開銷。
理論上,只要外掛配置不做變更,在 TTL 內這個 cache 應該會一直被命中 。實際上是不是這樣?我們不妨來埋個點:
坦率的講,當看到這個日誌時,我的內心是奔潰的。原來發生洩漏的 worker 2310 快取一直沒有命中,所以一直在初始化 lrucache,導致記憶體洩漏。沒有命中的原因就是,Kong 在每次呼叫外掛的時候,傳入的 conf 都是一個新物件(通過上圖 table 的地址可以看出)。
理論上,這個 conf 應該是由 Kong 的 mlcache 快取在 worker 的 lrucache 內,也就是在 L1 上,應該始終指向同一記憶體地址。這裡為什麼沒有生效呢?通過檢視 mlcache.lua
原始碼,問題最終定位在了這裡:
當請求第一次進入 worker 時,會去 L1 查詢 cache,這時候不會命中,所以接著去 L2(ngx.shared.DICT)查詢。查詢到之後,反序列化返回並更新 L1 快取。更新 L1 時,會計算 L1 的 TTL 時長,也就是這句 localremaining_ttl=ttl-(now()-at)
。
當 Kong 配置 db_cache_ttl=0 時,那麼這個時長 remaining_ttl 將會是一個負數,這樣就會導致每次更新 L1 後,L1 會立即過期。所以 L1 永遠不會命中,接下來返回的始終都是反序列化後的 L2,永遠都是一個新的物件。
弄清楚了這個 Bug 之後,修復起來也很簡單:
就是先判斷 TTL 的時長,如果為 0 的話,就將 L1 設定為永不過期即可。
當然,這並不是一個完美的解決方案。因為 remaining_ttl
也有可能為 0,如果不巧這個 L2 還是一個 SENTINEL 狀態,那麼這個狀態將會被永遠快取在 L1。鑑於篇幅原因這裡不再贅述,解決方案可以參考這裡:Fix expiration when the remaining TTL is exactly 0