使用K8s的一些經驗和體會

使用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 可以極大地提高你的生產力。

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