在 K8S 中,Pod 間實現共享記憶體的解決方案
作者:王濤(騰訊雲)
編輯:小君君(才雲)
來源:EAWorld
你是否經常會遇到這樣的困難:處理不同程序的應用程式時,需求方會要求包含所有程序以實現更多隔離。在這種情況下,一個常見的問題是:如何在同一 Node 中的 Pod 間實現共享記憶體。王濤是騰訊雲的高階工程師,在本文中,他將闡述一種在 Pod 間利用 Posix/SystemV 來實現共享記憶體的解決方案,一起來看看吧。
一些公共服務元件在追求效能的過程中,大多會出現與業務耦合過緊的情況。同時,工程師們在製作基礎映象時,會把這些基礎元件都打包進去。因此當業務映象啟動後,容器內部就會存在大量程序,使得 Kubernetes 對 Pod 的管理產生隱患。
為了實現業務容器瘦身,更是為了基礎元件自身的管理更加獨立。工程師們可以將基礎元件從業務映象中剝離並進行 DaemonSet 容器化部署。 但是,一些基礎元件 Agent 與業務 Pod 之間是通過共享記憶體的方式進行通訊的,所以整個部署的首要問題是:在同一 Node 中,Pod 之間如何去實現共享記憶體?
通過閱讀本文你將瞭解:
-
為什麼要將公共基礎元件 Agent 進行 DaemonSet 部署;
-
Linux 共享記憶體機制;
-
同一 Node 上跨 Pod 的共享記憶體方案;
-
灰度上線。
為什麼要將公共基礎元件 Agent 進行 DaemonSet 部署
工程師們自研的公共基礎元件,比如服務路由元件、安全元件等,通常以程序的方式部署在 Node 上,併為所有的業務提供服務。但是在上微服務和容器化之後, 如果工程師還是慣用 sidecar 或者將元件打包到業務 Image 中,繼續以 Per Pod Per Agent 的方式部署,基礎元件 Server 端的壓力將會成百上千的增長。
這樣做的結果對於整個部署來說風險很大。因此,工程師們會更希望能以 DaemonSet 方式部署這些元件的 Agents。
先說說如果不將這些基礎元件從業務的 Pod 中剝離,業務會存在哪些問題:
-
業務容器中存在一大堆程序,當工程師為 Pod 申請資源(cpu/mem request and limit)時,不僅要考慮業務應用本身的資源消耗,還要考慮這些基礎元件的資源消耗。當某些 Agent 存在 Bug(比如記憶體洩漏)時,必將導致 Pod 被重建,甚至 cgroup OOM 會將業務程序 kill;
-
違背了 Kubernetes&微服務部署的最佳實踐:業務程序在容器前臺執行,與容器共生死。這將導致 Kubernetes 無法根據業務程序狀態關聯到容器狀態,進而使得 Kubernetes 無法及時自愈;
-
如果一個 Node 上執行 10 個 Pod,那麼就會有 ×10 的基礎元件數量在 Node 上。在沒有容器化之前,一個 Node 只要部署一個元件程序即可。容器化之後,叢集中元件 Agent 的數量會幾十倍的增長。如果業務進行了微服務拆分,這個指數會更大,這些基礎元件服務端是否能承受比以往高出幾十倍甚至上百倍的通訊請求,人們猶未可知;
-
如果你要全網升級某個基礎元件 Agent,那你可能會瘋掉!因為你需要重新打所有業務映象,同時全網業務也需要進行灰度升級。因為只要有一個 Agent 升級,你就需要重新構建業務的 Pod,這是一個令人厭煩的過程。你可能會說,基礎元件 Agent 都會有自己的熱升級方案,只要通過它們的方案升級就好了。如果你這樣做,必將引入更大的麻煩:Agents 的熱升級會因為無法被 Kubernetes 感知,而引發 Kubernetes 叢集資料不一致的問題。此時,你就需要利用虛擬機器或者物理機來部署叢集了。當然,這個問題,工程師們也可以通過 Operator 來實現,但這樣做的代價太大了,並且很不雲原生!
如果將基礎元件 Agent 從業務 Pod 中剝離,那麼以上的問題就都迎刃而解,並且架構上的解耦會帶來很多好處。工程師們也可以通過 Kubernetes 管理這些基礎元件的 Agents,並享受其自愈、滾動升級等功能。
Linux 共享記憶體機制
理想很美好,現實很殘酷!在整個業務中,工程師們首先要解決的問題是: 有些元件 Agent 與業務 Pod 之間是通過共享記憶體通訊的,這跟 Kubernetes&微服務的最佳實踐方案背道而馳。
眾所周知,Kubernetes 單個 Pod 內是共享 IPC 的,並且它們可以通過掛載 Medium,與 Memory 中的 EmptyDir Volume 共享同一塊記憶體 Volume。
Linux 共享記憶體的兩種機制:
-
POSIX 共享記憶體(shm_open()、shm_unlink());
-
System V 共享記憶體(shmget()、shmat()、shmdt())。
其中,System V 共享記憶體歷史悠久,一般的 UNIX 系統上都有這套機制;而 POSIX 共享記憶體機制介面更加方便易用,一般是結合記憶體對映 mmap 使用。
mmap 和 System V 共享記憶體的主要區別在於:
-
SystemV 是持久化的。除非被一個程序明確地刪除,否則它始終存在於記憶體裡,直到系統關機;
-
mmap 對映的記憶體不是持久化的。如果程序關閉,對映隨即失效,除非事先已經對映到了一個檔案上;
-
/dev/shm 是 Linux 下 SystemV 共享記憶體的預設掛載點。
POSIX 共享記憶體是基於 tmpfs 實現的。在核心中,不僅 PSM(POSIX shared memory),SSM(System V shared memory)也是基於 tmpfs 來實現的。
tmpfs 主要有兩個作用:
-
用於 SystemV 共享記憶體、匿名記憶體對映。這部分由核心管理,使用者不可見;
-
用於 POSIX 共享記憶體,由使用者負責 mount。mount 到 /dev/shm 依賴於 CONFIG_TMPFS。
雖然 System V 與 POSIX 共享記憶體都是通過 tmpfs 來實現的,但是它們所受的限制卻不相同。/proc/sys/kernel/shmmax 只會影響 SystemV 的共享記憶體。/dev/shm 只會影響 POSIX 的共享記憶體 。也就是說,System V 與 POSIX 的共享記憶體使用的是兩個不同的 tmpfs 例項(instance)。SystemV 共享記憶體能夠使用的記憶體空間只受 /proc/sys/kernel/shmmax 的限制;而使用者通過掛載的 /dev/shm,預設為實體記憶體的 1/2。
概括一下:
-
POSIX 共享記憶體與 SystemV 共享記憶體在核心上都是通過 tmpfs 來實現的,但對應兩個不同的 tmpfs 例項,它們相互獨立;
-
通過 /proc/sys/kernel/shmmax 可以限制 SystemV 共享記憶體的最大值。通過 /dev/shm 可以限制 POSIX 共享記憶體的最大值(所有之和)。
同一 Node 上跨 Pod 的共享記憶體方案
當基礎元件 Agents 通過 DaemonSet 部署後,Agents 和業務程序就在 Node 上的不同 Pod 中。此時,Kubernetes 該如何支援跨 Pod 的共享記憶體場景呢?
如上圖所示,在整個方案中,業務對 POSIX Type IPC 的共享支援是通過掛載 /dev/shm 來實現的;對 SystemV Type IPC 的共享支援是通過 Share HostIPC 來實現的。但是這樣的做法會使存於共享記憶體中的資訊被其他 Pod 誤操作。在業務安全性上,它們沒有被完全隔離。但其實在非容器化之前,各個業務共享記憶體也存在同樣的風險,所以這一點對於使用者來說是可以接受的。
如果工程師們確實發現有些業務存在共享記憶體的使用衝突,也可以再通過以下規則 進行隔離部署:
requiredDuringSchedulingIgnoredDuringExecution PodAntiAffinity
灰度上線
對於叢集中的存量業務,之前都是將 Agents 與業務打包在同一 docker image 中, 因此需要有灰度上線方案,以保證存量業務不受影響。
-
首先建立好對應的 Kubernetes ClusterRole、SA、ClusterRoleBinding 和 PSP Object。關於 PSP 的內容,請參考官方文件(關於 pod-security-policy 的內容);
-
在叢集中任意選擇部分 Node,給 Node 打上 Label(AgentsDaemonSet:YES)和 Taint(AgentsDaemonSet=YES:NoSchedule)。
$ kubectl label node $nodeName AgentsDaemonSet=YES $ kubectl taint node $nodeName AgentsDaemonSet=YES:NoSchedule
-
部署 Agent 對應的 DaemonSet(注意 DaemonSet 需要加上對應的 NodeSelector 和 Toleration, Critical Pod Annotations)、Sample as follows:
apiVersion: apps/v1 kind: DaemonSet metadata: name: demo-agent namespace: kube-system labels: k8s-app: demo-agent spec: selector: matchLabels: name: demo-agent template: metadata: annotations: scheduler.alpha.kubernetes.io/critical-pod: ""labels: name: demo-agent spec: tolerations: - key: "AgentsDaemonSet"operator: "Equal"value: "YES"effect: "NoSchedule"hostNetwork: truehostIPC: truenodeSelector: AgentsDaemonSet: "YES"containers: - name: demo-agent image: demo_agent:1.0volumeMounts: - mountPath: /dev/shm name: shm resources: limits: cpu: 200m memory: 200Mi requests: cpu: 100m memory: 100Mi volumes: - name: shm hostPath: path: /dev/shm type: Directory
-
在該 Node 上部署不包含基礎元件 Agent 的業務 Pod,檢查所有基礎元件和業務是否正常工作。
-
如果正常,再分批次選擇剩餘的 Nodes,加上 Label(AgentsDaemonSet:YES)和 Taint(AgentsDaemonSet=YES:NoSchedule);
-
DaemonSet Controller 會自動在這些 Nodes 建立 DaemonSet Agents Pod。這樣就可以逐批次完成叢集中基礎元件 Agents 的灰度上線。
總結
在高併發業務下,尤其還是以 C/C++ 程式碼實現的基礎元件,工程師們經常會使用共享記憶體通訊機制來追求高效能的標準。本文給出了 Kubernetes Pod 間 Posix/SystemV 共享記憶體方式的折中方案(以犧牲一定的安全性為代價)。
當然,業務在微服務/容器化部署後,基礎服務的 Server 端是不會有壓力的。在此,我建議以 SideCar Container 方式將基礎服務的 Agents 與業務 Container 部署在同一 Pod 中,利用 Pod 的共享 IPC 特性及 Memory Medium EmptyDir Volume 方式共享記憶體。
王濤,騰訊雲高階工程師,西安電子科大碩士畢業,持續深耕雲端計算領域七年,目前在騰訊基於 TKE(Tencent Kubernetes Engine)構建 DevOps 平臺,幫助集團自研業務上雲,曾在華為、唯品會、vivo 從事企業私有云平臺的建設工作,2014 年開始專注於 Kubernetes/Docker 等容器技術棧在 DevOps、AI Platform 方向的應用,積累了大量生產經驗。