根據我的個人經驗,大多數人似乎傾向於透過 Helm 或者手動方式將應用程式甩給 Kubernetes,之後就坐等每天輕鬆呼叫的美好生活。但在 GumGum 公司的實踐當中,我們體會到 Kubernetes 應用的一系列“陷阱”,也希望把這些陷阱與大家分享,給您的 Kubernetes 探索之旅帶來一點啟發。
1
配置 Pod 請求與限制
我們從配置一套可以執行 Pod 的簡單環境開始。Kubernetes 在處理 Pod 排程與故障狀態方面確實表現出色,但我們也意識到,如果 Kubernetes 排程程式無法衡量 Pod 的成功執行究竟需要多少資源,那麼有時候部署工作可能面臨挑戰。而這一挑戰,也正是資源請求與限制機制的設計根源。目前,設定應用程式請求與限制方面的最佳實踐仍然存在不少爭議。實際上,這項工作更像是一門藝術,而非單純的科學。下面,我們聊聊 GumGum 公司內部對這個問題的看法:
Pod 請求: 這是排程程式用於衡量 Pod 最佳部署方法的主要指標。
下面來看 Kubernetes 說明文件中的相關描述:
過濾步驟會在可行的情況下找到一組 Pod。例如,PodFitsResources 過濾器會檢查候選節點是否具備充足的可用資源,以滿足 Pod 提出的特定資源請求。
在內部,我們透過這樣一種方式使用應用程式請求:透過設定,我們對應用程式正常執行實際工作負載時的資源需求做出估計。以此為基礎,排程程式即可更合理地放置節點。最初,我們希望將請求設定得更高一些,保證各個 Pod 都擁有充足的資源。但我們很快發現,這種方式會大大增加排程時間,並導致部分 Pod 無法完全排程。這樣的結果實際上與我們完全不指定資源請求時看到的情況類似:在後一種情況下,由於控制平面並不清楚應用程式需要多少資源,因此排程程式經常會“逐出”Pod 且不再重新加以排程。正是這一排程演算法中的關鍵組成部分,導致我們無法得到符合預期的排程效果。
Pod 限制: 即對於 Pod 的直接限制,代表著叢集允許各容器所使用的最大資源量。
同樣來看官方說明文件中的描述:
如果您為容器設定了 4GiB 的記憶體限制,則 kubelet(與容器執行時)將強制執行此限制。執行時將防止容器使用超出所配置上限的資源容量。例如,當容器中的程序所消耗的記憶體量超過獲准數量時,系統核心將終止該資源分配嘗試,並提示記憶體不足(OOM)錯誤。
容器所使用的實際資源量可以高於其請求,但永遠不能高於配置上限。很明顯,對限制指標的正確設定相當困難,但也非常重要。在理想情況下,我們希望讓 Pod 的資源需求在整個流程生命週期內發生變化,而又不致干擾到系統上的其他流程——這也正是限制機制的意義所在。遺憾的是,我們無法明確給出最合適的設定值,只能遵循以下過程進行調整:
使用負載測試工具,我們可以模擬基準流量水平,並觀察 Pod 的資源使用情況(包括記憶體與 CPU)。
我們將 Pod 請求設定在極低水平,同時將 Pod 資源限制保持在請求值的約 5 倍,而後觀察其行為。當請求過低時,程序將無法啟動,並時常引發神秘的 Go 執行時錯誤。
這裡需要強調的一點在於,資源限制越嚴格,Pod 的排程難度也就越大。這是因為 Pod 排程要求目標節點擁有充足的資源。例如,如果您的資源非常有限(記憶體只有 4GB),那麼即使是執行輕量級 Web 伺服器程序都很可能非常困難。在這種情況下,大家需要進行橫向擴充套件,而且各個新容器也應執行在同樣擁有至少 4GB 可用記憶體的節點之上。如果不存在這樣的節點,您需要在叢集中引入新節點以處理該 Pod,這無疑會令啟動時間有所增加。總之,請務必在資源請求與限制之間找到最小“邊界”,保證快速、平衡實現擴充套件。
2
配置 Liveness 與 Readiness 探針
Kubernetes 社群中經常討論的另一個有趣話題,就是如何配置 Linvess 與 Readiness 探針。合理使用這兩種探針,能夠為我們帶來一種執行容錯軟體、並最大程度減少停機時間的機制。但如果配置不正確,它們也可能對應用程式造成嚴重的效能影響。下面來看這兩種探針的基本情況,以及如何進行使用判斷:
Liveness 探針:“用於指示容器是否正在執行。如果 Liveness 探針失敗,則 kubelet 將關閉容器,且容器將開始執行重新啟動策略。如果容器並不提供 Liveness 探針,則其預設狀態被視為成功。”—Kubernetes說明文件
Liveness 探針的資源需求必須很低,因為它們需要頻繁執行,並需要在應用程式執行時向 Kubernetes 發出通知。請注意,如果將其設定為每秒執行一次,則系統將需要承擔每秒 1 次的額外請求處理量。因此,請務必認真考慮如何處理這些額外請求及相應資源。在 GumGum,我們將 Liveness 探針設定為在應用程式主元件執行時進行響應,且不考慮資料是否已經完全可用(例如來自遠端資料庫或快取的資料)。舉例來說,我們會在應用當中設定一個特定的“health”端點,單純負責返回 200 響應程式碼。只要仍在返回響應,就表明該程序已經啟動並可以處理請求(但尚未正式產生流量)。
Readiness 探針:“指示容器是否準備好處理請求。如果 Readiness 探針失敗,則端點控制器將從與該 Pod 相匹配的所有服務端點中,刪除該 Pod 的 IP 地址。”
Readiness 探針的執行成本要高得多,因為其作用在於持續告知後端,整個應用程式正處於執行狀態且準備好接收請求。關於此探針是否應該訪問資料庫,社群中存在諸多爭論。考慮到 Readiness 探針造成的開銷(需要經常執行,但頻繁可以靈活調整),我們決定在某些應用程式中只在從資料庫返回記錄後,才開始“提供流量”。透過對 Readiness 探針的精心設計,我們已經能夠實現更高的可用性水平以及零停機時間部署。
但如果大家確實有必要透過應用程式的 Readiness 探針隨時檢查資料庫請求的就緒狀態,請儘可能控制查詢操作的資源用量,例如……
以下,是我們在 Kubernetes 中為這兩種探針指定的配置值:
您還可以新增其他一些配置選項:
initialDelaySeconds- 容器啟動的多少秒後,探針開始實際執行
periodSeconds- 兩次探測之間的等待間隔
timeoutSeconds- 需要經過多少秒,才能判定某一 Pod 處於故障狀態。相當於傳統意義上的超時指標
failureThreshold- 探針失敗多少次後,才向 Pod 發出重啟訊號
successThreshold- 探針成功多少次後,才能判定 Pod 進入就緒狀態(通常使用在 Pod 啟動或者故障恢復之後)
3
設定預設 Pod 網路策略
Kubernetes 使用一種“扁平”網路拓撲;在預設情況下,所有 Pod 之間都可以直接相互通訊。但結合實際用例,這種通訊能力往往不必要甚至不可接受。由此帶來的一大潛在安全隱患在於,如果某一易受攻擊的應用程式遭到利用,則攻擊者即可由此獲取完全訪問許可權,進而將流量傳送至網路上的所有 Pod 當中。因此我們也有必要在 Pod 網路中應用最低訪問原則,在理想情況下透過網路策略明確指定哪些容器之間允許建立相互連線。
以下列簡單策略為例,可以看到其將拒絕特定名稱空間中的所有入口流量:
4
透過 Hooks 與 Init 容器執行自定義行為
我們希望在 Kubernetes 系統中實現的核心目標之一,在於嘗試為現有開發人員提供近乎零停機時間的部署支援。但不同應用程式往往擁有不同的關閉方式與資源清理過程,因此整體零停機目標很難實現。首先橫亙在我們面前的,就是 Nginx 這道難關。我們注意到在啟動 Pod 的滾動部署時,活動連線在成功終止之前就會被丟棄。經過廣泛的線上研究,事實證明 Kubernetes 在終止 Pod 之前,並不會等待 Nginx 用盡其連線資源。使用預停止 hook,我們得以注入此項功能,並由此實現了零停機時間。
isnginx-killer.sh:
另一個實用範例,是透過 Init 容器處理特定應用程式的啟動任務。部分高人氣 Kubernetes 專案還會使用 Istio 等 init-containers 將 Envoy 處理程式碼注入 Pod 當中。如果您在應用程式啟動之前,需要首先完成繁重的資料庫遷移過程,那麼 Init 容器特別適用。您也可以為此過程設定更高的資源上限,保證其不受主應用程式的限制設定影響。
另一種常見模式是向 init-conatiner 提供 secrets 訪問權,並由該容器將這些憑證公佈給主 Pod,從而防止透過主應用 Pod 本體對 secret 發出示授權訪問。同樣來看說明文件中的表述:
Init 容器能夠安全執行實用程式或自定義程式碼,避免其破壞應用程式容器映象的安全性。透過剝離這些不必要的工具,您可以限制應用程式容器映象的攻擊面。
5
核心調優
最後,我們來聊聊一項最先進的技術。Kubernetes 本身是一套高度靈活的平臺,可幫助您以最適合的方式執行工作負載。在 GumGum,我們擁有多種高效能應用程式,其對執行資源有著極為苛刻的要求。在進行了廣泛的負載測試之後,我們發現有某一款應用程式難以在使用 Kubernetes 預設設定的前提下處理必要的流量負載。但 Kubernetes 允許我們執行一個高許可權容器,透過修改為其配置適用於特定 Pod 的核心執行引數。透過以下示例程式碼,我們修改了 Pod 中的最大開啟連線數量:
這是一種使用頻率較低的高階技術。如果您的應用程式難以在高負載場景下健康執行,大家可能需要調整其中的部分引數。這裡建議各位在官方說明文件中參閱引數調優與可選值的相關細節資訊。
6
總結
雖然 Kubernetes 已經算是一種幾乎“開箱即用”的解決方案,但大家仍然需要採取一系列關鍵步驟以保證應用程式的平衡執行。在將應用程式遷移至 Kubernetes 之上的整個過程中,請務必重視負載測試“迴圈”——執行應用程式,對其進行負載測試,觀察指標與擴充套件行為,基於結果調整您的配置,而後重複。請儘量客觀地設定預期流量,並嘗試將流量增加至超限水平,藉此檢視哪些元件會最先陷入癱瘓。透過這種迭代方法,大家也許只需要採取本文中介紹的部分步驟即可獲得理想的應用程式執行效果。總之,請永遠關注以下幾個核心問題:
我的應用程式的資源佔用量是多少?佔用量會如何變化?
服務的實際擴充套件要求是什麼?預計需要處理怎樣的平均流量?峰值流量處於怎樣的水平?
服務可能多久需要進行一次橫向擴充套件?新的 Pod 要過多久才能正式開始接收流量?
我們的 Pod 終止過程優雅可控嗎?是否需要這種優雅性與可控性?我們能否實現零停機時間部署?
該如何儘可能降低安全風險,並限制 Pod 入侵狀況的“爆炸半徑”(影響範圍)?服務中是否存在某些不必要的許可權或訪問能力?
Kubernetes 是一套令人印象深刻的強大平臺,您可以在這裡運用最佳實踐為整個叢集部署數千項服務。但不同的軟體之間總是有所差別,有時候您的應用程式可能需要進一步調整,好在 Kubernetes 為我們提供不少調整“旋鈕”,儘可能讓使用者輕鬆達成與預期相符的技術目標。將資源請求與限制、Livenss 與 Readiness 檢查、init-containers、網路策略以及自定義核心調優等方法相結合,相信大家能夠在 Kubernetes 平臺之上實現更出色的基準效能、彈性與快速規模擴充套件能力。
https://medium.com/gumgum-tech/5-things-we-overlooked-when-putting-our-first-app-on-kubernetes-58583c1783e4