使用K8s的一些經驗和體會

youmen發表於2021-01-18

使用K8s的一些經驗和體會

Java應用程式的奇怪案例

​ 在微服務和容器化方面,工程師傾向於避免使用 Java,這主要是由於 Java 臭名昭著的記憶體管理。但是,現在情況發生了改變,過去幾年來 Java 的容器相容性得到了改善。畢竟,大量的系統(例如Apache Kafka和Elasticsearch)在 Java 上執行。

​ 回顧 2017-18 年度,我們有一些應用程式在 Java 8 上執行。這些應用程式通常很難理解像 Docker 這樣的容器環境,並因堆記憶體問題和異常的垃圾回收趨勢而崩潰。我們瞭解到,這是由於 JVM 無法使用Linuxcgroup和namespace造成的,而它們是容器化技術的核心。

​ 但是,從那時起,Oracle 一直在不斷提高 Java 在容器領域的相容性。甚至 Java 8 的後續補丁都引入了實驗性的 JVM標誌來解決這些,XX:+UnlockExperimentalVMOptions和XX:+UseCGroupMemoryLimitForHeap。

​ 但是,儘管做了所有的這些改進,不可否認的是,Java 在記憶體佔用方面仍然聲譽不佳,與 Python 或 Go 等同行相比啟動速度慢。這主要是由 JVM 的記憶體管理和類載入器引起的。

​ 現在,如果我們必須選擇 Java,請確保版本為 11 或更高。並且 Kubernetes 的記憶體限制要在 JVM 最大堆記憶體(-Xmx)的基礎上增加 1GB,以留有餘量。也就是說,如果 JVM 使用 8GB 的堆記憶體,則我們對該應用程式的 Kubernetes 資源限制為 9GB。

Kubernetes生命週期管理: 升級

​ Kubernetes 生命週期管理(例如升級或增強)非常繁瑣,尤其是如果已經在 裸金屬或虛擬機器 上構建了自己的叢集。對於升級,我們已經意識到,最簡單的方法是使用最新版本構建新叢集,並將工作負載從舊版本過渡到新版本。節點原地升級所做的努力和計劃是不值得的。

​ Kubernetes 具有多個活動元件,需要升級保持一致。從 Docker 到 Calico 或 Flannel 之類的 CNI 外掛,你需要仔細地將它們組合在一起才能正常工作。雖然像 Kubespray、Kubeone、Kops 和 Kubeaws 這樣的專案使它變得更容易,但它們都有缺點.

​ 我們在 RHEL 虛擬機器上使用 Kubespray 構建了自己的叢集。Kubespray 非常棒,它具有用於構建、新增和刪除新節點、升級版本的 playbook,以及我們在生產環境中操作 Kubernetes 所需的幾乎所有內容。但是,用於升級的 playbook 附帶了免責宣告,以避免我們跳過子版本。因此,必須經過所有中間版本才能到達目標版本。

​ 關鍵是,如果你打算使用 Kubernetes 或已經在使用 Kubernetes,請考慮生命週期活動以及解決這一問題的方案。構建和執行叢集相對容易一些,但是生命週期維護是一個全新的體驗,具有多個活動元件。

構建和部署

​ 在準備重新設計整個構建和部署流水線之前, 我們的構建過程和部署必須經歷 Kubernetes 世界的完整轉型。不僅在 Jenkins 流水線中進行了大量的重構,而且還使用了諸如 Helm 之類的新工具,策劃了新的 git 流和構建、標籤化 docker 映象,以及版本化 helm 的部署 chart。

​ 你需要一種策略來維護程式碼,以及 Kubernetes 部署檔案、Docker 檔案、Docker 映象、Helm chart,並設計一種方法將它們組合在一起。

經過幾次迭代,我們決定採用以下設計:

  • 應用程式程式碼及其 helm chart 放在各自的 git 儲存庫中。這使我們可以分別對它們進行版本控制(語義版本控制)。
  • 然後,我們將 chart 版本與應用程式版本關聯起來,並使用它來跟蹤釋出。例如,app-1.2.0使用charts-1.1.0進行部署。如果只更改 Helm 的 values 檔案,則只更改 chart 的補丁版本(例如,從1.1.0到1.1.1)。所有這些版本均由每個儲存庫中的RELEASE.txt中的發行說明規定。
  • 對於我們未構建或修改程式碼的系統應用程式,例如 Apache Kafka 或 Redis ,工作方式有所不同。也就是說,我們沒有兩個 git 儲存庫,因為 Docker 標籤只是 Helm chart 版本控制的一部分。如果我們更改了 docker 標籤以進行升級,則會升級 chart 標籤的主要版本。

存活和就緒探針(雙刃劍)

​ Kubernetes 的存活探針和就緒探針是自動解決系統問題的出色功能。它們可以在發生故障時重啟容器,並將流量從不正常的例項進行轉移。但是,在某些故障情況下,這些探針可能會變成一把雙刃劍,並會影響應用程式的啟動和恢復,尤其是有狀態的應用程式,例如訊息平臺或資料庫。

​ 我們的 Kafka 系統就是這個受害者。我們執行了一個3 Broker 3 Zookeeper有狀態副本集,該狀態集的ReplicationFactor為 3,minInSyncReplica為 2。當系統意外故障或崩潰導致 Kafka 啟動時,問題發生了。這導致它在啟動期間執行其他指令碼來修復損壞的索引,根據嚴重性,此過程可能需要 10 到 30 分鐘。由於增加了時間,存活探針將不斷失敗,從而向 Kafka 發出終止訊號以重新啟動。這阻止了 Kafka 修復索引並完全啟動。

​ 唯一的解決方案是在存活探針設定中配置initialDelaySeconds,以在容器啟動後延遲探針評估。但是,問題在於很難對此加以評估。有些恢復甚至需要一個小時,因此我們需要提供足夠的空間來解決這一問題。但是,initialDelaySeconds越大,彈性的速度就越慢,因為在啟動失敗期間 Kubernetes 需要更長的時間來重啟容器。

​ 因此,折中的方案是評估initialDelaySeconds欄位的值,以在 Kubernetes 中的彈性與應用程式在所有故障情況(磁碟故障、網路故障、系統崩潰等)下成功啟動所花費的時間之間取得更好的平衡 。

​ 更新:如果你使用最新版本,Kubernetes 引入了第三種探針型別,稱為“啟動探針”,以解決此問題。從 1.16 版開始提供 alpha 版本,從 1.18 版開始提供 beta 版本。
啟動探針會禁用就緒和存活檢查,直到容器啟動為止,以確保應用程式的啟動不會中斷。

公開外部IP

​ 我們瞭解到,使用靜態外部 IP 公開服務會對核心的連線跟蹤機制造成巨大代價。除非進行完整的計劃,否則它很輕易就破壞了擴充套件性。

​ 我們的叢集執行在Calico for CNI上,在 Kubernetes 內部採用BGP作為路由協議,並與邊緣路由器對等。對於 Kubeproxy,我們使用IP Tables模式。我們在 Kubernetes 中託管著大量的服務,通過外部 IP 公開,每天處理數百萬個連線。由於來自軟體定義網路的所有 SNAT 和偽裝,Kubernetes 需要一種機制來跟蹤所有這些邏輯流。為此,它使用核心的Conntrack and netfilter工具來管理靜態 IP 的這些外部連線,然後將其轉換為內部服務 IP,然後轉換為 pod IP。所有這些都是通過conntrack表和 IP 表完成的。

​ 但是conntrack表有其侷限性。一旦達到限制,你的 Kubernetes 叢集(如下所示的 OS 核心)將不再接受任何新連線。在 RHEL 上,可以通過這種方式進行檢查。

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_maxnet.netfilter.nf_conntrack_count = 167012
net.netfilter.nf_conntrack_max = 262144

​ 解決此問題的一些方法是使用邊緣路由器對等多個節點,以使連線到靜態 IP 的傳入連線遍及整個叢集。因此,如果你的叢集中有大量的計算機,累積起來,你可以擁有一個巨大的conntrack表來處理大量的傳入連線。
回到 2017 年我們剛開始的時候,這一切就讓我們望而卻步,但最近,Calico 在 2019 年對此進行了詳細研究,標題為“為什麼 conntrack 不再是你的朋友”。

安全方面

​ 對於大部分 Kubernetes 使用者來說,安全是無關緊要的,或者說沒那麼緊要,就算考慮到了,也只是敷衍一下,草草了事。實際上 Kubernetes 提供了非常多的選項可以大大提高應用的安全性,只要用好了這些選項,就可以將絕大部分的攻擊抵擋在門外。為了更容易上手,我將它們總結成了幾個最佳實踐配置,大家看完了就可以開幹了。當然,本文所述的最佳安全實踐僅限於 Pod 層面,也就是容器層面,於容器的生命週期相關,至於容器之外的安全配置(比如作業系統啦、k8s 元件啦),以後有機會再嘮。

為容器配置Security Context

​ 大部分情況下容器不需要太多的許可權,我們可以通過 Security Context 限定容器的許可權和訪問控制,只需加上 SecurityContext 欄位:

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
    securityContext:

禁用allowPrivilegeEscalation

​ allowPrivilegeEscalation=true 表示容器的任何子程式都可以獲得比父程式更多的許可權。最好將其設定為 false,以確保 RunAsUser 命令不能繞過其現有的許可權集。

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
    securityContext:
      allowPrivilegeEscalation: false

不要使用root使用者

​ 為了防止來自容器內的提權攻擊,最好不要使用 root 使用者執行容器內的應用。UID 設定大一點,儘量大於 3000。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
    runAsUser: <UID higher than 1000>
    runAsGroup: <UID higher than 3000>

限制CPU和記憶體資源

requests和limits都加上

不比掛載Service Account Token

​ ServiceAccount 為 Pod 中執行的程式提供身份標識,怎麼標識呢?當然是通過 Token 啦,有了 Token,就防止假冒偽劣程式。如果你的應用不需要這個身份標識,可以不必掛載:

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  automountServiceAccountToken: false

確保seccomp設定正確

​ 對於 Linux 來說,使用者層一切資源相關操作都需要通過系統呼叫來完成,那麼只要對系統呼叫進行某種操作,使用者層的程式就翻不起什麼風浪,即使是惡意程式也就只能在自己程式記憶體空間那一分田地晃悠,程式一終止它也如風消散了。seccomp(secure computing mode)就是一種限制系統呼叫的安全機制,可以可以指定允許那些系統呼叫。

​ 對於 Kubernetes 來說,大多數容器執行時都提供一組允許或不允許的預設系統呼叫。通過使用 runtime/default 註釋或將 Pod 或容器的安全上下文中的 seccomp 型別設定為 RuntimeDefault,可以輕鬆地在 Kubernetes 中應用預設值。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: "runtime/default"

​ 預設的seccomp 配置檔案應該為大多數工作負載提供足夠的許可權,如果你有更多的需求,可以自定義配置檔案.

限制容器的capabilities

​ 容器依賴於傳統的Unix安全模型,通過控制資源所屬使用者和組的許可權,來達到對資源的許可權控制。以 root 身份執行的容器擁有的許可權遠遠超過其工作負載的要求,一旦發生洩露,攻擊者可以利用這些許可權進一步對網路進行攻擊。
​ 預設情況下,使用 Docker 作為容器執行時,會啟用 NET_RAW capability,這可能會被惡意攻擊者進行濫用。因此,建議至少定義一個PodSecurityPolicy(PSP),以防止具有 NET_RAW 功能的容器啟動。
通過限制容器的 capabilities,可以確保受攻擊的容器無法為攻擊者提供橫向攻擊的有效路徑,從而縮小攻擊範圍。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: <specific user>
  capabilities:
  drop:
    -NET_RAW
    -ALL

是否一定需要Kubernetes?

​ 它是一個複雜的平臺,具有自己的一系列挑戰,尤其是在構建和維護環境方面的開銷。它將改變你的設計、思維、架構,並需要提高技能和擴大團隊規模以適應轉型。

​ 但是,如果你在雲上並且能夠將 Kubernetes 作為一種“服務”使用,它可以減輕平臺維護帶來的大部分開銷,例如“如何擴充套件內部網路 CIDR?”或“如何升級我的 Kubernetes 版本?”

​ 今天,我們意識到,你需要問自己的第一個問題是“你是否一定需要 Kubernetes?”。這可以幫助你評估所遇到的問題以及 Kubernetes 解決該問題的重要性。

​ Kubernetes 轉型並不便宜,為此支付的價格必須確實證明“你的”用例的必要性及其如何利用該平臺。如果可以,那麼 Kubernetes 可以極大地提高你的生產力。

記住,為了技術而技術是沒有意義的。

相關文章