我們被一個 kong 的性能 bug 折騰了一個通宵

語言: CN / TW / HK

故事背景

在 Erda 的技術架構中,我們使用了 kong 作為 API 網關的技術選型。因其具備高併發低延時的特性,同時結合了 Kubernetes Ingress Controller,基於雲原生的聲明式配置方式,能夠實現豐富的 API 策略。

在我們最早交付的集羣中,kong 還是較為早期的 0.14 版本,隨着業務層面對安全的要求日益趨增,我們需要基於 kong 實現安全插件,幫助系統能夠具備更好的安全能力。由於較為早期的 0.14 版本不能使用 go-pluginserver 來擴展 kong 的插件機制,我們不得不在古老的集羣中將 kong 升級為相對較新的 2.2.0 版本。

升級過程就不在此贅述了,基本就是照着官方文檔一步步順利的升級上去,但是在升級上去之後的幾天裏,我們的 SRE 團隊收到了非常密集的諮詢甚至是聲討,部署在該集羣上的業務 間歇性的無法訪問,延遲非常高

一系列失敗的嘗試

參數調優

最開始為了快速修復這個問題,我們對 kong 的 NGINX_WORKER_PROCESSESMEM_CACHE_SIZEDB_UPDATE_FREQUENCYWORKER_STATE_UPDATE_FREQUENCY 參數以及 postgres 的 work_memshare_buffers 都進行了適當的調優。

但是,沒有任何效果 :sweat:。

清理數據

由於這個集羣的歷史原因,會頻繁的註冊或者刪除 api 數據,因此集羣中累計了大約 5 萬多條 route 或者 service 數據。

我們懷疑是數據裏量大導致的性能下降,於是結合 erda 中的數據對 kong 中的歷史數據進行刪除,在刪除的過程中出現的刪除較慢並且同時 kong 的性能急劇下降的現象。

反覆測試了幾次後我們確定了 「 只要調用 admin 的接口導致 kong 性能下降 」 這一結論,跟社區的這個問題非常相似,鏈接如下:

http://github.com/Kong/kong/...

kong 實例的讀寫分離

確定了是 admin 接口的原因後,我們決定將 admin 跟業務的 kong 實例分開,希望 admin 的調用不會影響到業務的正常流量訪問,期待達到 kong 的 admin 接口慢就慢吧,但是不要影響業務的訪問性能。

然而,沒有任何效果:tired_face:。

postgres 遷移 RDS

kong 層面的努力無果之後,我們在測試過程中同時觀察到了當調用 admin 接口試,postgres 的進程也增多了很多,CPU使用率也漲了起來,也是決定將 pg 遷移到 更為專業的RDS中。

還是,沒有任何效果:sob:。

回滾

最終我們還是回滾到了 0.14 版本,追求暫時“心靈的寧靜”。

至此,線上的嘗試基本搞一段落,也大致摸清了問題復現的條件,於是我們決定在線下構造一個環境來繼續找出問題的原因。

問題的復現之路

我們將出問題的 kong 的 postgres 數據導一份到開發環境中,模擬 「 調用 admin 接口是性能急劇下降 」的情況,並尋找解決之道。

導入數據

我們將有問題的集羣中的 postgre 的數據備份之後然後在一個新的集羣中導入:

psql -h 127.0.0.1 -U kong < kong.sql

並且開啟 kong 的 prometheus 插件, 方便使用 grafana 來查看性能圖標:

curl -X POST http://10.97.4.116:8001/plugins --data "name=prometheus"

現象一

調用 admin 服務的 / 同樣的慢,跟線上的現象吻合,當數據量大的時候調用 admin 的 / 目錄需要耗費更多的時間。

curl http://10.97.4.116:8001

現象二

然後我們來模擬在線上遇到的調用 admin 接口後業務訪問性能變差的現象,先調用 admin 接口創建個業務的 api,以供測試,我們創建了一個 service 以及一個 routes:

curl -i -X POST http://10.97.4.116:8001/services/ -d 'name=baidu2'  -d 'url=http://www.baidu.com'

curl -i -X POST http://10.97.4.116:8001/services/baidu2/routes \
-d "name=test2" \
     -d "paths[1]=/baidu2"

後面可以使用 curl http://10.97.4.116:8000/baidu2 來模擬業務接口進行測試。

準備 admin 接口的測試腳本, 創建並刪除一個 service/route,中間穿插一個 service list。

#!/bin/bash

curl -i -X POST http://10.97.4.116:8001/services/ -d 'name=baidu'  -d 'url=http://www.baidu.com'

curl -i -X POST http://10.97.4.116:8001/services/baidu/routes \
     -d "name=test" \
     -d "paths[1]=/baidu" 

curl -s http://10.97.4.116:8001/services

curl -i -X DELETE      http://10.97.4.116:8001/services/baidu/routes/test

然後持續調用該腳本:

for i in `seq 1 100`; do sh 1.sh ; done

在持續調用該腳本的過程中去訪問一個業務的接口,會發現非常慢,跟線上的現象完全吻合。

curl  http://10.97.4.116:8000/baidu2

PS:精簡腳本,後只觸發一條寫入,或者刪除也會觸發該現象

伴隨現象

  • kong 實例的 cpu 跟 mem 都持續上漲,且當 admin 接口調用結束後此現象依然沒有結束。mem 上漲到一定程度會到時 nginx worker 進程 oom 掉,然後重啟,這個也許是訪問慢的原因;
  • 我們設置了 KONG_NGINX_WORKER_PROCESSES 為 4,並且為 pod 的內存為 4G 的時候,pod 的整體內存會穩定在 2.3G, 但是 調用 admin 接口試,pod 的內存就會一直上漲至超過 4G,觸發 worker 的 OOM,於是我將 pod 的內存調整到了 8G。再次調用 admin 接口,發現 pod 的內存依然一直上漲,只是上漲到了 4.11 G 就結束了,這似乎意味着我們是要設置 pod 的內存為 KONG_NGINX_WORKER_PROCESSES 兩倍,這個問題就解決了(但是還有個重要的問題是為什麼調用一次 admin 接口,會導致內存漲了那麼多);
  • 另外,當我持續調用 admin 接口的時候, 最終的內存會持續增長並且穩定到 6.9G。

這個時候我們將問題抽象一下:

調用「kong admin 接口」導致內存一直上漲,進而觸發 oom 導致 worker 被 kill 掉,最終導致業務訪問慢。

繼續調查內存是被什麼佔用了:

我使用 pmap -x [pid] 前後兩次查看了 worker 進程的內存分佈,變化的是第二張如中框起來來的部分,從地址上看整塊內存都被換過了,但是將內存數據導出並且字符串化之後,沒有什麼有效的信息可供進一步排查。

結論

  • 該問題跟 kong 的升級(0.14 --> 2.2.0) 沒有關係,直接使用 2.2.0 版本也會有這個問題;
  • kong 每隔 worker_state_update_frequency 時間後會在內存中重建 router,一旦開始重建就會導致 內存上漲,看了下代碼問題出在了 Router.new 的方法這裏,會申請 lrucache 但是使用後沒有 flush_all,根據最新的 2.8.1 版本的 lrucache 進行了釋放問題依然存在;
  • 也就是 kong 的 Router.new 方法裏的其他邏輯到時的內存上漲;

  • 這也就表明這個問題是 kong 存在的一個性能 bug,及時在最新的版本中依然存在,當 route 跟 service 達到某個量級的時候會出現調用 admin 接口,導致 kong 的 worker 內存迅速上漲,使得 oom 進而引發業務訪問性能變差的現象,臨時的解決辦法可以是減少 NGINX_WORKER_PROCESSES 的數量並且增加 kong pod 的內存量,保證調用 admin 接口後需要的內存足夠使用不觸發 oom,才能保證業務的正常使用。

最後,我們會在 http://github.com/Kong/kong/... 這個 issue 中將該現象補充進去,歡迎大家持續關注,一起討論~

更多技術乾貨請關注【爾達 Erda】公眾號,與眾多開源愛好者共同成長~