本文經受權摘自杜軍大佬的新書《Kubernetes 網絡權威指南》,詳情請看 微信公衆號。
當檢查你的 Kubernetes 集羣的節點時,在節點上執行 docker ps
命令,你可能會注意到一些被稱爲「暫停」(pause)的容器,例如:nginx
🐳 → docker ps CONTAINER ID IMAGE COMMAND ... 3b45e983c859 gcr.io/google_containers/pause-amd64:3.1 「/pause」 dbfc35b00062 gcr.io/google_containers/pause-amd64:3.1 「/pause」 c4e998ec4d5d gcr.io/google_containers/pause-amd64:3.1 「/pause」 508102acf1e7 gcr.io/google_containers/pause-amd64:3.1 「/pause」
你會疑惑這些容器並非你建立的。是的,這些容器是 Kubernetes」免費贈送「的。docker
Kubernetes 中所謂的 pause 容器有時候也稱爲 infra
容器,它與用戶容器」捆綁「運行在同一個 Pod 中,最大的做用是維護 Pod 網絡協議棧(固然,也包括其餘工做,下文會介紹)。shell
都說 Pod 是 Kubernetes 設計的精髓,而 pause 容器則是 Pod 網絡模型的精髓,理解 pause 容器可以更好地幫助咱們理解 Kubernetes Pod 的設計初衷。爲何這麼說呢?還得從 Pod 沙箱(Pod Sandbox)提及。segmentfault
熟悉 Pod 生命週期的同窗應該知道,建立 Pod 時 Kubelet 先調用 CRI 接口 RuntimeService.RunPodSandbox
來建立一個沙箱環境,爲 Pod 設置網絡(例如:分配 IP)等基礎運行環境。當 Pod 沙箱(Pod Sandbox)創建起來後,Kubelet 就能夠在裏面建立用戶容器。當到刪除 Pod 時,Kubelet 會先移除 Pod Sandbox 而後再中止裏面的全部容器。後端
可能有讀者會疑惑,Pod Sandbox 是啥玩意兒啊?其實,這只是同一個事物經過不一樣角度看獲得的不一樣稱謂。從 Kubernetes 的底層容器運行時 CRI 看,Pod 這種在統一隔離環境裏資源受限的一組容器,就叫 Sandbox。api
Tips:一個隔離的應用運行時環境叫容器,一組共同被 Pod 約束的容器就叫 Pod Sandbox。她們同生共死,共享底層資源。
瞭解 KVM 底層的讀者應該知道,虛擬機與容器同樣底層都使用 cgroups
作資源配額,並且概念上都抽離出一個隔離的運行時環境,只是區別在於資源隔離的實現。所以,從字面是上看,虛擬機和容器仍是有機會都用沙箱這個概念來「套「的。事實上,提出 Pod 沙箱概念就是爲 Kubernetes 兼容不一樣運行時環境(甚至包括虛擬機!)預留空間,讓運行時根據各自的實現來建立不一樣的 Pod Sandbox。對於基於 hypervisor
的運行時(KVM,kata 等),Pod Sandbox 就是虛擬機。對於 Linux 容器,Pod Sandbox 就是 Linux Namespace(Network Namespace 等)。安全
Pod Sandbox 與咱們今天要聊的「主角」pause 容器有着千絲萬縷的聯繫。在 Linux CRI 體系裏,Pod Sandbox 其實就是 pause 容器。Kubelet 代碼引用的 defaultSandboxImage 其實就是官方提供的 gcr.io/google_containers/pause-amd64
鏡像。bash
咱們知道 Kubernetes 的 Pod 抽象基於 Linux 的 namespace
和 cgroups
,爲一組容器共同提供了隔離的運行環境。從網絡的角度看,同一個 Pod 中的不一樣容器猶如在運行在同一個專有主機上,能夠經過 localhost 進行通訊。服務器
原則上,任何人均可以配置 Docker 來控制容器組之間的共享級別——你只需建立一個父容器,並建立與父容器共享資源的新容器,而後管理這些容器的生命週期。在 Kubernetes 中,pause 容器被看成 Pod 中全部容器的「父容器」併爲每一個業務容器提供如下功能:微信
Kubernetes 的 pause 容器沒有複雜的邏輯,裏面運行着一個很是簡單的進程,它不執行任何功能,基本上是永遠「睡覺」的,源代碼在 kubernetes 項目的 build/pause/
目錄中。由於它比較簡單,在這裏便寫下完整的源代碼,以下所示:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define STRINGIFY(x) #x #define VERSION_STRING(x) STRINGIFY(x) #ifndef VERSION #define VERSION HEAD #endif static void sigdown(int signo) { psignal(signo, "Shutting down, got signal"); exit(0); } static void sigreap(int signo) { while (waitpid(-1, NULL, WNOHANG) > 0) ; } int main(int argc, char **argv) { int i; for (i = 1; i < argc; ++i) { if (!strcasecmp(argv[i], "-v")) { printf("pause.c %s\n", VERSION_STRING(VERSION)); return 0; } } if (getpid() != 1) /* Not an error because pause sees use outside of infra containers. */ fprintf(stderr, "Warning: pause should be the first process\n"); if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 1; if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 2; if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap, .sa_flags = SA_NOCLDSTOP}, NULL) < 0) return 3; for (;;) pause(); fprintf(stderr, "Error: infinite loop terminated\n"); return 42; }
如上所示,這個「暫停」容器運行一個很是簡單的進程,它不執行任何功能,一啓動就永遠把本身阻塞住了(見 pause()
系統調用)。正如你看到的,它固然不會只知道睡覺。它執行另外一個重要的功能——即它扮演 PID 1 的角色,並在子進程成爲孤兒進程的時候經過調用 wait()
收割這些殭屍子進程。這樣咱們就不用擔憂咱們的 Pod 的 PID namespace 裏會堆滿殭屍進程了。這也是爲何 Kubernetes 不隨便找個容器(例如:Nginx)做爲父容器,而後讓用戶容器加入的緣由了。
咱們在第 1 章介紹過,在 Linux 系統中運行新進程時,該進程從父進程繼承了其 namespace。在 namespace 中運行進程的方法是經過取消與父進程的共享 namespace,從而建立一個新的 namespace。如下是使用 unshare 工具在新的 PID、UTS、IPC 和 mount namespace 中運行 shell 的示例。
🐳 → unshare --pid --uts --ipc --mount -f chroot rootfs /bin/sh
一旦進程運行,你能夠將其餘進程添加到該進程的 namespace 中以造成一個 Pod,Pod 中的容器在其中共享 namespace。讀者可使用第 1 章提到的 setns
系統調用將新進程添加到現有命名空間,Docker 也提供命令行功能讓你自動完成此過程。下面讓咱們來看一下如何使用 pause 容器和共享 namespace 從頭開始建立 Pod。
首先,咱們使用 Docker 啓動 pause 容器,以便咱們能夠將其餘容器添加到 Pod 中,以下所示:
🐳 → docker run -d --name pause gcr.io/google_containers/pause-amd64:3.0
而後,咱們在 Pod 中運行其餘容器,分別是 Nginx 代理和 ghost 博客應用。
Nginx 代理的後端配置成 http://127.0.0.1:2368
,也就是 ghost 進程監聽的地址,以下所示:
# cat <<EOF >> nginx.conf > error_log stderr; > events { worker_connections 1024; } > http { > access_log /dev/stdout combined; > server { > listen 80 default_server; > server_name example.com www.example.com; > location / { > proxy_pass http://127.0.0.1:2368; > } > } > } > EOF # docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 --net=container:pause --ipc=container:pause --pid=container:pause nginx
做爲應用服務器的 ghost 博客應用程序建立另外一個容器,以下所示:
🐳 → docker run -d --name ghost --net = container:pause --ipc = container:pause --pid = container:pause ghost
在咱們這個例子中,咱們將 pause 容器指定爲咱們要加入其 namespace 的容器。若是訪問http://localhost:8080/ ,那麼應該可以看到 ghost 經過 Nginx 代理運行,由於 pause、nginx 和 ghost 容器之間共享 Network namespace,以下圖所示。
經過 Pod,Kubernetes 爲你屏蔽了以上全部複雜度。
在 UNIX 系統中,PID 爲 1 的進程是 init 進程,即全部進程的父進程。init 進程比較特殊,它維護一張進程表而且不斷地檢查其餘進程的狀態。init 進程的其中一個做用是當某個子進程因爲父進程的錯誤退出而變成了「孤兒進程」,便會被 init 進程收養並在該進程退出時回收資源。
進程可使用 fork 和 exec 這兩個系統調用啓動其餘進程。當啓動了其餘進程後,新進程的父進程就是調用 fork 系統調用的進程。fork 用於啓動正在運行的進程的另外一個副本,而 exec 則用於啓動不一樣的進程。每一個進程在操做系統進程表中都有一個條目。這將記錄有關進程的狀態和退出代碼。當子進程運行完成後,它的進程表條目仍然將保留直到父進程使用 wait 系統調用得到其退出代碼後纔會清理進程條目。這被稱爲「收割」殭屍進程,而且殭屍進程沒法經過 kill
命令來清除。
殭屍進程是已中止運行但進程表條目仍然存在的進程,由於父進程還沒有經過 wait 系統調用進行檢索。從技術層面來講,終止的每一個進程都算是一個殭屍進程,儘管只是在很短的時間內發生的。當用戶程序寫得很差而且簡單地省略 wait 系統調用,或者當父進程在子進程以前異常退出而且新的父進程沒有調用 wait 去檢索子進程時,會出現較長時間的殭屍進程。系統中存在過多殭屍進程將佔用大量操做系統進程表資源。
當進程的父進程在子進程完成前退出時,OS 將子進程分配給 init 進程。init 進程「收養」子進程併成爲其父進程。這意味着當子進程此時退出時,新的父進程(init 進程)必須調用 wait 獲取其退出代碼,不然其進程表項將一直保留,而且它也將成爲一個殭屍進程。同時,init 進程必須擁有「信號屏蔽」功能,不能處理某個信號邏輯,從而防止 init 進程被誤殺。因此不是隨隨便便一個進程都能當 init 進程的。
容器使用 PID namespace
對 pid 進行隔離,所以每一個容器中都可以有獨立的 init 進程。當在主機上發送 SIGKILL
或者 SIGSTOP
(也就是 docker kill 或者 docker stop)強制終止容器的運行時,其實就是在終止容器內的 init 進程。一旦 init 進程被銷燬,同一 PID namespace 下的進程也隨之被銷燬。
在容器中,必需要有一個進程充當每一個 PID namespace 的 init 進程,使用 Docker 的話,ENTRYPOINT
進程是 init 進程。若是多個容器之間共享 PID namespace,那麼擁有 PID namespace 的那個進程須承擔 init 進程的角色,其餘容器則做爲 init 進程的子進程添加到 PID namespace 中。
爲了給讀者一個直觀的印象,下面給出一個例子來講明用戶容器和 pause 容器的 PID 關係。
先啓動一個 pause 容器:
🐳 → docker run -idt --name pause gcr.io/google_containers/pause-amd64:3.0 7f6e459df5644a1db4bc9ad2206a0f99e40312de1892695f8a09d52faa9c1073
再運行一個 busybox 容器,加入 pause 容器的 namespace(network,PID,IPC)中:
🐳 → docker run -idt --name busybox --net=container:pause --pid=container:pause --ipc=container:pause busybox ad3029c55476e431101473a34a71516949d1b7de3afe3d505b51d10c436b4b0f
上述這種加入 pause 容器的方式也是 Kubernetes 啓動 Pod 的原理。
接下來,讓咱們進入 busybox 容器查看裏面的進程,發現裏面 PID=1 的進程是/pause
:
🐳 → docker exec -it ad3029c55476 /bin/sh / # ps aux PID USER TIME COMMAND 1 root 0:00 /pause 5 root 0:00 sh 9 root 0:00 /bin/sh 13 root 0:00 ps aux
咱們徹底能夠在父容器中運行 Nginx,並將 ghost 添加到 Nginx 容器的 PID 命名空間。
🐳 → docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 nginx 🐳 → docker run -d --name ghost --net=container:nginx --ipc=container:nginx --pid=container:nginx ghost
在這種狀況下,Nginx 將承擔 PID 1 的做用,並將 ghost 添加爲 Nginx 的子進程。雖然這樣貌似不錯,但從技術上看,Nginx 如今須要負責 ghost 進程的全部子進程。例如,若是 ghost 在其子進程完成以前異常退出了,那麼這些子進程將被 Nginx 收養。可是,Nginx 並非設計用來做爲一個 init 進程運行並收割殭屍進程的。這意味着將會有不少這種殭屍進程,而且這種狀況將持續整個容器的生命週期。
最後總結一句,Pod 的 init 進程,pause 容器舍他其誰?
關於共享/隔離 Pod 內容器的 PID namespace,就是一個見仁見智的問題了,支持共享的人以爲方便了進程間通訊,例如能夠在容器中給另一個容器內的進程發送信號量,並且還不用擔憂殭屍進程回收問題。
在 Kubernetes 1.8 版本以前,默認是啓用 PID namespace 共享的,除非使用 kubelet 標誌 --docker-disable-shared-pid=true
禁用。然而在 Kubernetes 1.8 版本之後,狀況恰好相反,默認狀況下 kubelet 標誌 --docker-disable-shared-pid=true
,若是要開啓,還要設置成 false。下面就來看看 Kubernetes 提供的關因而否共享 PID namespace 的 downward API。
apiVersion: v1 kind: Pod metadata: name: nginx spec: shareProcessNamespace: true containers: - name: nginx image: nginx - name: shell image: busybox securityContext: capabilities: add: - SYS_PTRACE stdin: true tty: true
如上所示,podSpec.shareProcessNamespace
指示了是否啓用 PID namespace 共享。
經過前文的討論,咱們知道 Pod 內容器共享 PID namespace 是頗有意義的,那爲何還要開放這個禁止 PID namesapce 共享的開關呢?那是由於當應用程序不會產生其餘進程,並且殭屍進程帶來的問題就能夠忽略不計時,就用不到 PID namespace 的共享了。並且有些場景下,用戶但願 Pod 內容器可以與其餘容器隔離 PID namespace,例以下面兩個場景:
(1)PID namespace 共享時,因爲 pause 容器成了 PID =1,其餘用戶容器就沒有 PID 1 了。但像 systemd 這類鏡像要求得到 PID 1,不然沒法正常啓動。有些容器經過 kill -HUP 1
命令重啓進程,然而在由 pause 託管 init 進程的 Pod 裏,上面這條命令只會給 pause 發信號量。
(2)PID namespace 共享帶來 Pod 內不一樣容器的進程對其餘容器是可見的,這包括 /proc
中可見的全部信息,例如,做爲參數或環境變量傳遞的密碼,這將帶來必定的安全風險。
掃一掃下面的二維碼關注微信公衆號,在公衆號中回覆◉加羣◉便可加入咱們的雲原生交流羣,和孫宏亮、張館長、陽明等大佬一塊兒探討雲原生技術