用了3年Kubernetes,我們得到的5個教訓(xùn)
我們從 2017 年開始基于 1.9.4 版本構(gòu)建第一個 Kubernetes 集群。我們有兩個集群,一個集群在裸機的 RHEL 虛擬機上運行,另一個集群在 AWS EC2 上運行。
現(xiàn)在,我們的 Kubernetes 基礎(chǔ)設(shè)施平臺由分布在多個數(shù)據(jù)中心的 400 多臺虛擬機組成。該平臺托管了高可用的關(guān)鍵任務(wù)軟件應(yīng)用程序和系統(tǒng),以管理具有近四百萬個活動設(shè)備的大型實時網(wǎng)絡(luò)。
Kubernetes 最終使我們變得更輕松,但是這個過程很艱難,是一種思維上的轉(zhuǎn)變。不僅讓我們的技能和工具有了徹底的轉(zhuǎn)變,還讓我們的設(shè)計和思維也得到了徹底的轉(zhuǎn)變。我們不得不采用多種新技術(shù),并進(jìn)行大量投資以擴展和提高我們的團(tuán)隊和基礎(chǔ)架構(gòu)的技能。
回顧 Kubernetes 在生產(chǎn)環(huán)境中運行的這三年,我們記下了一些很重要的經(jīng)驗教訓(xùn)。
在微服務(wù)和容器化方面,工程師傾向于避免使用 Java,這主要是由于 Java 臭名昭著的內(nèi)存管理。但是,現(xiàn)在情況發(fā)生了改變,過去幾年來 Java 的容器兼容性得到了改善。畢竟,大量的系統(tǒng)(例如Apache Kafka和Elasticsearch)在 Java 上運行。
回顧 2017-18 年度,我們有一些應(yīng)用程序在 Java 8 上運行。這些應(yīng)用程序通常很難理解像 Docker 這樣的容器環(huán)境,并因堆內(nèi)存問題和異常的垃圾回收趨勢而崩潰。我們了解到,這是由于 JVM 無法使用Linuxcgroup和namespace造成的,而它們是容器化技術(shù)的核心。
但是,從那時起,Oracle 一直在不斷提高 Java 在容器領(lǐng)域的兼容性。甚至 Java 8 的后續(xù)補丁都引入了實驗性的 JVM 標(biāo)志來解決這些問題,XX:+UnlockExperimentalVMOptions和XX:+UseCGroupMemoryLimitForHeap。
但是,盡管做了所有的這些改進(jìn),不可否認(rèn)的是,Java 在內(nèi)存占用方面仍然聲譽不佳,與 Python 或 Go 等同行相比啟動速度慢。這主要是由 JVM 的內(nèi)存管理和類加載器引起的。
現(xiàn)在,如果我們必須選擇 Java,請確保版本為 11 或更高。并且 Kubernetes 的內(nèi)存限制要在 JVM 最大堆內(nèi)存(-Xmx)的基礎(chǔ)上增加 1GB,以留有余量。也就是說,如果 JVM 使用 8GB 的堆內(nèi)存,則我們對該應(yīng)用程序的 Kubernetes 資源限制為 9GB。
Kubernetes 生命周期管理(例如升級或增強)非常繁瑣,尤其是如果已經(jīng)在 裸金屬或虛擬機 上構(gòu)建了自己的集群。對于升級,我們已經(jīng)意識到,最簡單的方法是使用最新版本構(gòu)建新集群,并將工作負(fù)載從舊版本過渡到新版本。節(jié)點原地升級所做的努力和計劃是不值得的。
Kubernetes 具有多個活動組件,需要升級保持一致。從 Docker 到 Calico 或 Flannel 之類的 CNI 插件,你需要仔細(xì)地將它們組合在一起才能正常工作。雖然像 Kubespray、Kubeone、Kops 和 Kubeaws 這樣的項目使它變得更容易,但它們都有缺點。
我們在 RHEL 虛擬機上使用 Kubespray 構(gòu)建了自己的集群。Kubespray 非常棒,它具有用于構(gòu)建、添加和刪除新節(jié)點、升級版本的 playbook,以及我們在生產(chǎn)環(huán)境中操作 Kubernetes 所需的幾乎所有內(nèi)容。但是,用于升級的 playbook 附帶了免責(zé)聲明,以避免我們跳過子版本。因此,必須經(jīng)過所有中間版本才能到達(dá)目標(biāo)版本。
關(guān)鍵是,如果你打算使用 Kubernetes 或已經(jīng)在使用 Kubernetes,請考慮生命周期活動以及解決這一問題的方案。構(gòu)建和運行集群相對容易一些,但是生命周期維護(hù)是一個全新的體驗,具有多個活動組件。
在準(zhǔn)備重新設(shè)計整個構(gòu)建和部署流水線之前, 我們的構(gòu)建過程和部署必須經(jīng)歷 Kubernetes 世界的完整轉(zhuǎn)型。不僅在 Jenkins 流水線中進(jìn)行了大量的重構(gòu),而且還使用了諸如 Helm 之類的新工具,策劃了新的 git 流和構(gòu)建、標(biāo)簽化 docker 鏡像,以及版本化 helm 的部署 chart。
你需要一種策略來維護(hù)代碼,以及 Kubernetes 部署文件、Docker 文件、Docker 鏡像、Helm chart,并設(shè)計一種方法將它們組合在一起。
經(jīng)過幾次迭代,我們決定采用以下設(shè)計。
應(yīng)用程序代碼及其 helm chart 放在各自的 git 存儲庫中。這使我們可以分別對它們進(jìn)行版本控制(語義版本控制)。 然后,我們將 chart 版本與應(yīng)用程序版本關(guān)聯(lián)起來,并使用它來跟蹤發(fā)布。例如, app-1.2.0使用charts-1.1.0進(jìn)行部署。如果只更改 Helm 的 values 文件,則只更改 chart 的補丁版本(例如,從1.1.0到1.1.1)。所有這些版本均由每個存儲庫中的RELEASE.txt中的發(fā)行說明規(guī)定。對于我們未構(gòu)建或修改代碼的系統(tǒng)應(yīng)用程序,例如 Apache Kafka 或 Redis ,工作方式有所不同。也就是說,我們沒有兩個 git 存儲庫,因為 Docker 標(biāo)簽只是 Helm chart 版本控制的一部分。如果我們更改了 docker 標(biāo)簽以進(jìn)行升級,則會升級 chart 標(biāo)簽的主要版本。
Kubernetes 的存活探針和就緒探針是自動解決系統(tǒng)問題的出色功能。它們可以在發(fā)生故障時重啟容器,并將流量從不正常的實例進(jìn)行轉(zhuǎn)移。但是,在某些故障情況下,這些探針可能會變成一把雙刃劍,并會影響應(yīng)用程序的啟動和恢復(fù),尤其是有狀態(tài)的應(yīng)用程序,例如消息平臺或數(shù)據(jù)庫。
我們的 Kafka 系統(tǒng)就是這個受害者。我們運行了一個3 Broker 3 Zookeeper有狀態(tài)副本集,該狀態(tài)集的ReplicationFactor為 3,minInSyncReplica為 2。當(dāng)系統(tǒng)意外故障或崩潰導(dǎo)致 Kafka 啟動時,問題發(fā)生了。這導(dǎo)致它在啟動期間運行其他腳本來修復(fù)損壞的索引,根據(jù)嚴(yán)重性,此過程可能需要 10 到 30 分鐘。由于增加了時間,存活探針將不斷失敗,從而向 Kafka 發(fā)出終止信號以重新啟動。這阻止了 Kafka 修復(fù)索引并完全啟動。
唯一的解決方案是在存活探針設(shè)置中配置initialDelaySeconds,以在容器啟動后延遲探針評估。但是,問題在于很難對此加以評估。有些恢復(fù)甚至需要一個小時,因此我們需要提供足夠的空間來解決這一問題。但是,initialDelaySeconds越大,彈性的速度就越慢,因為在啟動失敗期間 Kubernetes 需要更長的時間來重啟容器。
因此,折中的方案是評估initialDelaySeconds字段的值,以在 Kubernetes 中的彈性與應(yīng)用程序在所有故障情況(磁盤故障、網(wǎng)絡(luò)故障、系統(tǒng)崩潰等)下成功啟動所花費的時間之間取得更好的平衡 。
更新:如果你使用最新版本,Kubernetes 引入了第三種探針類型,稱為“啟動探針”,以解決此問題。從 1.16 版開始提供 alpha 版本,從 1.18 版開始提供 beta 版本。
啟動探針會禁用就緒和存活檢查,直到容器啟動為止,以確保應(yīng)用程序的啟動不會中斷。
我們了解到,使用靜態(tài)外部 IP 公開服務(wù)會對內(nèi)核的連接跟蹤機制造成巨大代價。除非進(jìn)行完整的計劃,否則它很輕易就破壞了擴展性。
我們的集群運行在Calico for CNI上,在 Kubernetes 內(nèi)部采用BGP作為路由協(xié)議,并與邊緣路由器對等。對于 Kubeproxy,我們使用IP Tables模式。我們在 Kubernetes 中托管著大量的服務(wù),通過外部 IP 公開,每天處理數(shù)百萬個連接。由于來自軟件定義網(wǎng)絡(luò)的所有 SNAT 和偽裝,Kubernetes 需要一種機制來跟蹤所有這些邏輯流。為此,它使用內(nèi)核的Conntrack and netfilter工具來管理靜態(tài) IP 的這些外部連接,然后將其轉(zhuǎn)換為內(nèi)部服務(wù) IP,然后轉(zhuǎn)換為 pod IP。所有這些都是通過conntrack表和 IP 表完成的。
但是conntrack表有其局限性。一旦達(dá)到限制,你的 Kubernetes 集群(如下所示的 OS 內(nèi)核)將不再接受任何新連接。在 RHEL 上,可以通過這種方式進(jìn)行檢查。
$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_maxnet.netfilter.nf_conntrack_count = 167012net.netfilter.nf_conntrack_max = 262144
解決此問題的一些方法是使用邊緣路由器對等多個節(jié)點,以使連接到靜態(tài) IP 的傳入連接遍及整個集群。因此,如果你的集群中有大量的計算機,累積起來,你可以擁有一個巨大的conntrack表來處理大量的傳入連接。
回到 2017 年我們剛開始的時候,這一切就讓我們望而卻步,但最近,Calico 在 2019 年對此進(jìn)行了詳細(xì)研究,標(biāo)題為“為什么 conntrack 不再是你的朋友”。
三年過去了,我們每天仍然在繼續(xù)發(fā)現(xiàn)和學(xué)習(xí)新知識。它是一個復(fù)雜的平臺,具有自己的一系列挑戰(zhàn),尤其是在構(gòu)建和維護(hù)環(huán)境方面的開銷。它將改變你的設(shè)計、思維、架構(gòu),并需要提高技能和擴大團(tuán)隊規(guī)模以適應(yīng)轉(zhuǎn)型。
但是,如果你在云上并且能夠?qū)?Kubernetes 作為一種“服務(wù)”使用,它可以減輕平臺維護(hù)帶來的大部分開銷,例如“如何擴展內(nèi)部網(wǎng)絡(luò) CIDR?”或“如何升級我的 Kubernetes 版本?”
今天,我們意識到,你需要問自己的第一個問題是“你是否一定需要 Kubernetes?”。這可以幫助你評估所遇到的問題以及 Kubernetes 解決該問題的重要性。
Kubernetes 轉(zhuǎn)型并不便宜,為此支付的價格必須確實證明“你的”用例的必要性及其如何利用該平臺。如果可以,那么 Kubernetes 可以極大地提高你的生產(chǎn)力。
記住,為了技術(shù)而技術(shù)是沒有意義的。
來源:https://www.infoq.cn/article/XFN54h7ctSX0O59VVkfi
關(guān)注「開源Linux」加星標(biāo),提升IT技能

