「Kubernetes 將延遲增加了 10 倍」:誰該為此負責?

筆記。 翻譯。:這篇文章由歐洲公司 Adevinta 首席軟體工程師 Galo Navarro 撰寫,是基礎設施營運領域的引人入勝且富有啟發性的「調查」。其原始標題在翻譯中略有擴展,原因作者在一開始就解釋過。

「Kubernetes 將延遲增加了 10 倍」:誰該為此負責?

作者註: 好像是這個帖子 被吸引 比預期受到更多關注。我仍然收到一些憤怒的評論,認為文章的標題具有誤導性,一些讀者感到難過。我了解發生這種情況的原因,因此,儘管有毀掉整個陰謀的風險,我還是想立即告訴你這篇文章的內容。當團隊遷移到 Kubernetes 時,我看到的一件奇怪的事情是,每當出現問題(例如遷移後延遲增加)時,第一個受到指責的是 Kubernetes,但後來發現編排器並沒有真正負責責備。本文講述了這樣一個案例。它的名字重複了我們一位開發人員的感嘆(稍後你會發現 Kubernetes 與它無關)。在這裡你不會發現任何關於 Kubernetes 的令人驚訝的啟示,但你可以期待一些關於複雜系統的好課程。

幾週前,我的團隊將單一微服務遷移到一個核心平台,其中包括 CI/CD、基於 Kubernetes 的執行時間、指標和其他功能。此次搬遷屬於試點性質:我們計劃以此為基礎,在未來幾個月內再轉移約150個服務。他們都負責西班牙一些最大的線上平台(Infojobs、Fotocasa 等)的營運。

在我們將應用程式部署到 Kubernetes 並將一些流量重定向到它之後,一個令人震驚的驚喜等著我們。延遲 (潛伏) Kubernetes 中的請求比 EC10 中的請求高 2 倍。一般來說,要嘛找到這個問題的解決方案,要嘛放棄微服務(可能還有整個專案)的遷移。

為什麼 Kubernetes 中的延遲比 EC2 中的延遲高得多?

為了找到瓶頸,我們收集了整個請求路徑上的指標。我們的架構很簡單:API 閘道 (Zuul) 將請求代理程式到 EC2 或 Kubernetes 中的微服務執行個體。在 Kubernetes 中,我們使用 NGINX Ingress Controller,後端是普通對象,例如 部署 使用 Spring 平台上的 JVM 應用程式。

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

該問題似乎與後端的初始延遲有關(我在圖表上將問題區域標記為“xx”)。在 EC2 上,應用程式回應大約需要 20 毫秒。在 Kubernetes 中,延遲增加到 100-200 毫秒。

我們很快就排除了與運行時變更相關的可能嫌疑。 JVM 版本維持不變。容器化問題也與此無關:應用程式已經在 EC2 上的容器中成功運行。載入中?但我們觀察到即使每秒 1 個請求,延遲也很高。垃圾收集的暫停也可以被忽略。

我們的一位 Kubernetes 管理員想知道應用程式是否具有外部依賴項,因為 DNS 查詢過去曾引起類似的問題。

假設 1:DNS 名稱解析

對於每個請求,我們的應用程式都會在以下網域中存取 AWS Elasticsearch 實例一到三次 elastic.spain.adevinta.com。在我們的容器內 有一個殼,因此我們可以檢查搜尋網域是否確實需要很長時間。

來自容器的 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 解析確實導致了延遲的增加。

然而,這很奇怪,原因有二:

  1. 我們已經擁有大量 Kubernetes 應用程序,它們可以與 AWS 資源交互,而不會遭受高延遲的影響。不管是什麼原因,都與本案有關。
  2. 我們知道 JVM 會在記憶體中進行 DNS 快取。在我們的影像中,TTL 值寫入 $JAVA_HOME/jre/lib/security/java.security 並設定為 10 秒: networkaddress.cache.ttl = 10。換句話說,JVM 應該將所有 DNS 查詢快取 10 秒。

為了證實第一個假設,我們決定暫時停止呼叫 DNS,看看問題是否消失。首先,我們決定重新配置應用程序,使其透過 IP 位址直接與 Elasticsearch 通信,而不是透過網域名稱。這需要更改程式碼和新的部署,因此我們只需將網域對應到其 IP 位址 /etc/hosts:

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 查詢沒有任何可疑之處(除了我稍後將討論的一件小事)。但我們的服務處理每個請求的方式存在某些奇怪之處。下面是捕獲的螢幕截圖,顯示在回應開始之前請求已被接受:

「Kubernetes 將延遲增加了 10 倍」:誰該為此負責?

包裹編號顯示在第一列。為了清楚起見,我對不同的 TCP 流進行了顏色編碼。

綠色串流從資料包 328 開始,顯示客戶端 (172.17.22.150) 如何與容器 (172.17.36.147) 建立 TCP 連線。初次握手(328-330)後,包 331 帶來了 HTTP GET /v1/.. — 對我們服務的傳入請求。整個過程耗時1毫秒。

灰色流(來自資料包 339)顯示我們的服務向 Elasticsearch 實例發送了一個 HTTP 請求(沒有 TCP 握手,因為它正在使用現有連線)。這花了 18 毫秒。

到目前為止,一切都很好,時間大致符合預期的延遲(從客戶端測量時為 20-30 毫秒)。

然而,藍色部分需要 86ms。這裡面到底發生了什麼事?透過資料包 333,我們的服務向 /latest/meta-data/iam/security-credentials,緊接著,透過同一個 TCP 連接,另一個 GET 請求 /latest/meta-data/iam/security-credentials/arn:...

我們發現整個追蹤過程中的每個請求都會重複這種情況。在我們的容器中,DNS 解析確實有點慢(對這種現象的解釋很有趣,但我將把它保存在單獨的文章中)。事實證明,長時間延遲的原因是每個請求都會呼叫 AWS 實例元資料服務。

假設 2:不必要的 AWS 調用

兩個端點都屬於 AWS 實例元資料 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 開發工具包會更新憑證:

  • 截止日期 (Expiration) 掉進 EXPIRATION_THRESHOLD,硬編碼為 15 分鐘。
  • 自上次嘗​​試更新憑證以來已經過了更多時間 REFRESH_THRESHOLD,硬編碼 60 分鐘。

為了查看我們收到的憑證的實際到期日期,我們從容器和 EC2 執行個體執行上述 cURL 命令。從貨櫃收到的證書的有效期限要短得多:正好 15 分鐘。

現在一切都變得清楚了:對於第一個請求,我們的服務收到了臨時憑證。由於它們的有效期超過 15 分鐘,AWS 開發工具包將決定根據後續請求更新它們。每次請求都會發生這種情況。

為什麼憑證的有效期限變短了?

AWS 執行個體元資料旨在與 EC2 執行個體搭配使用,而不是與 Kubernetes 搭配使用。另一方面,我們不想改變應用程式介面。為此我們使用了 凱姆 - 該工具使用每個 Kubernetes 節點上的代理,允許使用者(將應用程式部署到叢集的工程師)將 IAM 角色指派給 Pod 中的容器,就像它們是 EC2 執行個體一樣。 KIAM 會攔截對 AWS 實例元資料服務的調用,並從其快取中處理這些調用(先前已從 AWS 接收到這些調用)。從應用程式的角度來看,沒有任何變化。

KIAM 提供 Pod 短期憑證。考慮到 Pod 的平均壽命比 EC2 執行個體短,這是有道理的。證書預設有效期 等於同樣的15分鐘.

因此,如果將兩個預設值疊加在一起,就會出現問題。提供給應用程式的每個憑證都會在 15 分鐘後過期。但是,AWS Java SDK 會強制續約距離到期日不到 15 分鐘的任何憑證。

因此,每次請求都必須更新臨時證書,這需要多次呼叫 AWS API,並導致延遲顯著增加。在AWS Java SDK中我們發現 功能要求,其中也提到了類似的問題。

事實證明,解決方案很簡單。我們只是重新配置 KIAM 以請求具有更長有效期的憑證。一旦發生這種情況,請求就開始在沒有 AWS 元資料服務參與的情況下流動,並且延遲下降到比 EC2 更低的水平。

發現

根據我們的遷移經驗,最常見的問題來源之一不是 Kubernetes 或平台其他元素中的錯誤。它也沒有解決我們正在移植的微服務中的任何根本缺陷。 問題常常因為我們將不同的元素放在一起而出現。

我們將以前從未相互作用過的複雜系統混合在一起,期望它們一起形成一個更大的系統。唉,元素越多,出錯的空間就越大,熵越高。

在我們的案例中,高延遲並不是 Kubernetes、KIAM、AWS Java SDK 或我們的微服務中的錯誤或錯誤決策造成的。這是兩個獨立的預設設定組合的結果:一個在 KIAM 中,另一個在 AWS Java SDK 中。單獨來看,這兩個參數都是有意義的:AWS Java SDK 中的主動憑證續約策略和 KAIM 中的憑證有效期較短。但當你把它們放在一起時,結果就變得不可預測。 兩個獨立且合乎邏輯的解決方案組合起來不一定有意義。

譯者PS

您可以了解有關用於將 AWS IAM 與 Kubernetes 整合的 KIAM 公用程式架構的詳細資訊: 這篇文章 來自它的創造者。

也請閱讀我們的部落格:

來源: www.habr.com

添加評論