Kubernetes用了,延遲高了10倍,問題在哪?

沒想到文章受到這麼多關注,也有不少朋友反映標題具有誤導性。我能理解大家的想法,所以在這裏澄清下本文的寫作意圖。當我們團隊將業務遷移至Kubernetes之後,一旦出現問題,總有人覺得“這是遷移之後的陣痛”,並把矛頭指向Kubernetes。我相信很多朋友肯定聽人說過標題裏這句話,但最終事實證明犯錯的並不是Kubernetes。雖然文章並不涉及關於Kubernetes的突破性啓示,但我認爲內容仍值得各位管理複雜系統的朋友借鑑。

近期,我所在的團隊將一項微服務遷移到中央平臺。這套中央平臺捆綁有CI/CD,基於Kubernetes的運行時以及其他功能。這項演習也將作爲先頭試點,用於指導未來幾個月內另外150多項微服務的進一步遷移。而這一切,都是爲了給西班牙的多個主要在線平臺(包括Infojobs、Fotocasa等)提供支持。

在將應用程序部署到Kubernetes並路由一部分生產流量過去後,情況開始發生變化。Kubernetes部署中的請求延遲要比EC2上的高出10倍。如果不找到解決辦法,不光是後續微服務遷移無法正常進行,整個項目都有遭到廢棄的風險。

爲什麼Kubernetes中的延遲要遠高於EC2?

爲了查明瓶頸,我們收集了整個請求路徑中的指標。這套架構非常簡單,首先是一個API網關(Zuul),負責將請求代理至運行在EC2或者Kubernetes中的微服務實例。在Kubernetes中,我們僅代表和NGINX Ingress控制器,後端則爲運行有基於Spring的JVM應用程序的Deployment對象。

                          EC2
                          +---------------+
                        |  +---------+  |
                        |  |         |  |
                     +-------> BACKEND |  |
                    |    |  |         |  |
                    |    |  +---------+  |                   
                   |    +---------------+
         +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

問題似乎來自後端的上游延遲(我在圖中以「xx」進行標記)。將應用程序部署至EC2中之後,系統大約需要20毫秒就能做出響應。但在Kubernetes中,整個過程卻需要100到200毫秒。

我們很快排除了隨運行時間變化而可能出現的可疑對象。JVM版本完全相同,而且由於應用程序已經運行在EC2容器當中,所以問題也不會源自容器化機制。另外,負載強度也是無辜的,因爲即使每秒只發出1項請求,延遲同樣居高不下。另外,GC暫停時長几乎可以忽略不計。

我們的一位Kubernetes管理員詢問這款應用程序是否具有外部依賴項,因爲DNS解析之前就曾引起過類似的問題,這也是我們目前找到的可能性最高的假設。

假設一:DNS解析

在每一次請求時,我們的應用程序都像域中的某個AWS ElasticSearch實例(例如elastic.spain.adevinta.com)發出1到3條查詢。我們在容器中添加了一個shell,用於驗證該域名的DNS解析時間是否過長。

來自容器的DNS查詢結果:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

來自運行這款應用程序的EC2實例的相同查詢結果:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

前者的平均解析時間約爲30毫秒,很明顯,我們的應用程序在其ElasticSearch上造成了額外的DNS解析延遲。

但這種情況非常奇怪,原因有二:

  • Kubernetes當中已經包含大量與AWS資源進行通信的應用程序,而且都沒有出現延遲過高的情況。因此,我們必須弄清引發當前問題的具體原因。
  • 我們知道JVM採用了內存內的DNS緩存。從配置中可以看到,TTL在$JAVA_HOME/jre/lib/security/java.security位置進行配置,並被設置爲networkaddress.cache.ttl = 10。JVM應該能夠以10秒爲週期緩存所有DNS查詢。

爲了確認DNS假設,我們決定剝離DNS解析步驟,並查看問題是否可以消失。我們的第一項嘗試是讓應用程序直接與ELasticSearch IP通信,從而繞過域名機制。這需要變更代碼並進行新的部署,即需要在/etc.hosts中添加一行代碼以將域名映射至其實際IP:

34.55.5.111 elastic.spain.adevinta.com

通過這種方式,容器能夠以近即時方式進行IP解析。我們發現延遲確實有所改進,但距離目標等待時間仍然相去甚遠。儘管DNS解析時長有問題,但真正的原因還沒有被找到。

網絡管道

我們決定在容器中進行tcpdump,以便準確觀察網絡的運行狀況。

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

我們隨後發送了多項請求並下載了捕捉結果(kubectl cp my-service:/capture.pcap capture.pcap),而後利用Wireshark進行檢查。

DNS查詢部分一切正常(少部分值得討論的細節,我將在後文提到)。但是,我們的服務處理各項請求的方式有些奇怪。以下是捕捉結果的截圖,顯示出在響應開始之前的請求接收情況。

數據包編號顯示在第一列當中。爲了清楚起見,我對不同的TCP流填充了不同的顏色。

從數據包328開始的綠色部分顯示,客戶端(172.17.22.150)打開了容器(172.17.36.147)間的TCP連接。在最初的握手(328至330)之後,數據包331將HTTP GET /v1/…(傳入請求)引向我們的服務,整個過程耗時約1毫秒。

來自數據包339的灰色部分表明,我們的服務向ElasticSearch實例發送了HTTP請求(這裏沒有顯示TCP握手,是因爲其使用原有TCP連接),整個過程耗費了18毫秒。

到這裏,一切看起來還算正常,而且時間也基本符合整體響應延遲預期(在客戶端一側測量爲20到30毫秒)。

但在兩次交換之間,藍色部分佔用了86毫秒。這到底是怎麼回事?在數據包333,我們的服務向/latest/meta-data/iam/security-credentials發送了一項HTTP GET請求,而後在同一TCP連接上,又向/latest/meta-data/iam/security-credentials/arn:…發送了另一項GET請求。

我們進行了驗證,發現整個流程中的每項單一請求都發生了這種情況。在容器內,DNS解析確實有點慢(理由同樣非常有趣,有機會的話我會另起一文詳加討論)。但是,導致高延遲的真正原因,在於針對每項單獨請求的AWS Instance Metadata Service查詢。

假設二:指向AWS的流氓調用

兩個端點都是AWS Instance Metadata API的組成部分。我們的微服務會在從ElasticSearch中讀取信息時使用該服務。這兩條調用屬於授權工作的基本流程,端點通過第一項請求產生與實例相關的IAM角色。

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

第二條請求則向第二個端點查詢實例的臨時憑證:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

客戶可以在短時間內使用這些憑證,且端點會定期(在Expiration過期之前)檢索新憑證。這套模型非常簡單:出於安全原因,AWS經常輪換臨時密鑰,但客戶端可以將密鑰緩存幾分鐘,從而抵消檢索新憑證所帶來的性能損失。

照理來說,整個過程應該由AWS Java SDK爲我們處理。但不知道爲什麼,實際情況並非如此。搜索了一遍GitHub問題,我們從 #1921 當中找到了需要的線索。

AWS SDK會在滿足以下兩項條件之一時刷新憑證:

  • Expiration 已經達到 EXPIRATION_THRESHOLD之內,硬編碼爲15分鐘。
  • 最後一次刷新憑證的嘗試大於REFRESH_THRESHOLD,硬編碼爲60分鐘。

我們希望查看所獲取憑證的實際到期時間,因此我們針對容器API運行了cURL命令——分別指向EC2實例與容器。但容器給出的響應要短得多:正好15分鐘。現在的問題很明顯了:我們的服務將爲第一項請求獲取臨時憑證。由於有效時間僅爲15分鐘,因此在下一條請求中,AWS SDK會首先進行憑證刷新,每一項請求中都會發生同樣的情況。

爲什麼憑證的過期時間這麼短?

AWS Instance Metadata Service在設計上主要代EC2實例使用,而不太適合Kubernetes。但是,其爲應用程序保留相同接口的機制確實很方便,所以我們轉而使用KIAM,一款能夠運行在各個Kubernetes節點上的工具,允許用戶(即負責將應用程序部署至集羣內的工程師)將IAM角色關聯至Pod容器,或者說將後者視爲EC2實例的同等對象。其工作原理是攔截指向AWS Instance Metadata Service的調用,並利用自己的緩存(預提取自AWS)及時接上。從應用程序的角度來看,整個流程與EC2運行環境沒有區別。

KIAM恰好爲Pod提供了週期更短的臨時憑證,因此可以合理做出假設,Pod的平均存在週期應該短於EC2實例——默認值爲15分鐘。如果將兩種默認值放在一起,就會引發問題。提供給應用程序的每一份證書都會在15分鐘之後到期,但AWS Java SDK會對一切剩餘時間不足15分鐘的憑證做出強制性刷新。

結果就是,每項請求都將被迫進行憑證刷新,這使每項請求的延遲提升。接下來,我們又在AWS Java SDK中發現了一項功能請求,其中也提到了相同的問題。

相比之下,修復工作非常簡單。我們對KIAM重新配置以延長憑證的有效期。在應用了此項變更之後,我們就能夠在不涉及AWS Instance Metadata Service的情況下開始處理請求,同時返回比EC2更低的延遲水平。

總結

根據我們的實際遷移經驗,最常見的問題並非源自Kubernetes或者該平臺其他組件,與我們正在遷移的微服務本身也基本無關。事實上,大多數問題源自我們急於把某些組件粗暴地整合在一起。

我們之前從來沒有複雜系統的整合經驗,所以這一次我們的處理方式比較粗糙,未能充分考慮到更多活動部件、更大的故障面以及更高熵值帶來的實際影響。

在這種情況下,導致延遲升高的並不是Kubernetes、KIAM、AWS Java SDK或者微服務層面的錯誤決策。相反,問題源自KIAM與AWS Java SDK當中兩項看似完全正常的默認值。單獨來看,這兩個默認值都很合理:AWS Java SDK希望更頻繁地刷新憑證,而KIAM設定了較低的默認過期時間。但在二者結合之後,卻產生了病態的結果。是的,各個組件能夠正常獨立運行,並不代表它們就能順利協作並構成更龐大的系統。

原文鏈接:

https://srvaroa.github.io/kubernetes/migration/latency/dns/java/aws/microservices/2019/10/22/kubernetes-added-a-0-to-my-latency.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章