kubernetes pod爲什麼需要pause容器?

前言

【譯】The Almighty Pause Container

當我們檢查 kubernetes 集羣的 node 節點時,我們使用 docker ps 查看時會發現一些名爲 pause 的容器在節點上運行。

$ docker ps
CONTAINER ID IMAGE COMMAND ...
...
3b45e983c859 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
dbfc35b00062 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
c4e998ec4d5d gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
508102acf1e7 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
  • 這些 pause 容器是什麼?
  • 爲什麼會有這麼多 pause 容器?
  • 這是怎麼回事呢?

爲了回答這些問題,我們需要去回顧一下這些pods是如何在kubernetes下被創建的,特別是在docker/containerd運行環境。

Docker支持以containers的方式部署軟件,container也非常適合用來部署單個軟件。但是,當我們想一起運行一個軟件的多個模塊的時候,這種方式又會變得非常的笨重。我們會常常遇到這種情況,當開發人員創建了多個docker鏡像後,還需要使用監控模塊去啓動和管理多個進程。在生產環境下,會發現如果把這些應用部署爲一組容器,並將這些容器組彼此分隔,每個容器組共享一個環境,這種方式會更有效。

Kubernetes爲應對這種case,提出了pod的抽象概念。Pod的概念,隱藏了docker中複雜的標誌位以及管理docker容器、共享卷及其他docker資源的複雜性。同時也隱藏了不同容器運行環境的差異。

原則上,任何人只需要創建一個父容器就可以配置docker來管理容器組之間的共享問題。這個父容器需要能夠準確的知道如何去創建共享運行環境的容器,還能管理這些容器的生命週期。爲了實現這個父容器的構想,kubernetes中,用pause容器來作爲一個pod中所有容器的父容器。這個pause容器有兩個核心的功能,第一,它提供整個pod的Linux命名空間的基礎。第二,啓用PID命名空間,它在每個pod中都作爲PID爲1進程,並回收殭屍進程。

什麼是共享命名空間?

在Linux中,當我們運行一個新的進程時,這個進程會繼承父進程的命名空間。而運行一個進程在一個新的命名空間,是通過 unsharing 父進程的命名空間從而創建一個新的命名空間。這裏舉個例子,使用unshare工具來運行一個具有新PID,UTS,IPC以及mount命名空間的shell。

$ sudo unshare --pid --uts --ipc --mount -f chroot rootfs /bin/sh

一旦這個進程運行,我們就可以添加其他進程到這個進程的命名空間,從而組成一個pod。我們就可以使用setns來添加新的進程到一個已存在的命名空間中。同一個Pod中的容器共享命名空間,下面舉個例子,一起來看我們如何利用pause容器和共享空間來創建一個pod。

首先我們創建一個pause容器。

$ docker run -d --name pause -p 8080:80 gcr.io/google_containers/pause-amd64:3.0

然後我們可以運行其他容器來組成我們的pod。先運行nginx,爲localhost:2368創建一個代理。

注意,我們也將本機的8080端口代理到pause容器的80端口,而不是代理到nginx容器,這是因爲pause容器初始化了網絡命名空間,nginx容器將會加入這個命名空間。

$ 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 --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

通過以上的方法,我們就可以通過訪問 http://localhost:8080/ 來查看我們的博客了。

上面的例子中,我們用pause容器爲其他容器提供了共享的命名空間。

不難想到,這其中的過程是非常複雜的。而且我們還沒有深入探討如何去監控和管理這些容器的生命週期。但是不用擔心,我們不需要這麼複雜的去管理我們的容器,因爲kubernetes已經都爲我們做好了。

如何回收殭屍進程?

在Linux中,存在父進程的進程在同一個PID命名空間中會組成一個樹形結構。在這個熟悉結構中,位於根節點的進程沒有父進程,這個進程就是PID爲1的init進程。

進程可以通過fork和exec系統調用來創建其他進程,而這個使用fork系統調用的進程就成爲新建進程的父進程。Fork用來創建當前進程的另一個拷貝,exec用來運行新的進程以代替當前進程,此時新進程的PID和被替代進程的PID是一樣的(爲了運行一個完全獨立的應用,你需要執行fork以及exec系統調用,使用fork來爲當前進程創建一個擁有新PID的子進程,然後當子進程檢測他自己是否是子進程時,執行exec從而用你想要運行的進程來替代本身,大多是語言都提供了函數以實現這一方法)。每個進程在系統進程表裏有存在一條記錄。它記錄了關於進程狀態和退出碼的相關信息。當子進程已經結束運行時,它在進程表中的記錄仍然存在,只有當父進程通過使用wait系統調用取回了它的退出碼。這個過程就叫做回收殭屍進程。

殭屍進程意爲那些已經停止運行但因爲父進程沒有釋放導致他們在進程表中的記錄仍然存在的一類進程。父進程沒有被釋放主要是因爲沒有通過調用wait系統調用。嚴格的來說,每個進程在結束時,都會在一小段時間內成爲“殭屍”,但這些殭屍進程則會存活的更久。

當父進程在子進程完成後不調用wait的syscall時,就會出現生存時間較長的僵死進程。一種情況是,父進程編寫得很差,並且簡單地忽略了wait調用,或者父進程在子進程之前死亡,而新的父進程沒有調用wait。當進程的父進程在子進程之前死亡時,操作系統將子進程分配給“init”進程或PID 1。例如,init進程“採用”子進程併成爲它的父進程。這意味着,現在當子進程退出時,新的父進程(init)必須調用wait獲取它的退出代碼,否則它的進程表項將永遠保持不變,變成殭屍。

在容器中,每個PID命名空間必須有一個進程作爲init進程。Docker中每個容器通常有自己的PID命名空間,入口點進程是init進程。但是,在kubernetes pod中,我們可以使容器在另一個容器的命名空間中運行。在這種情況下,一個容器必須承擔init進程的角色,而其他容器則作爲init進程的子元素添加到命名空間中。

下面的例子是,我將在nginx容器的PID命名空間中添加ghost容器。

$ 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 fork了子進程或使用exec運行子進程,但ghost又在子進程完成之前崩潰,那麼nginx將採用這些ghost的子進程作爲自己的子進程。然而,nginx的設計初衷並不是爲了能夠以init進程的形式運行並獲取殭屍。針對這一現象,在Kubernetes pods中,容器的運行方式與上面基本相同,但是爲每個pod創建了一個特殊的暫停容器。這個pause容器運行一個非常簡單的進程,它不執行任何函數,但本質上是永久休眠的(請參閱下面的pause()調用)。它是如此簡單,我可以包括完整的源代碼,在這裏寫:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static void sigdown(int signo) {
 psignal(signo, "Shutting down, got signal");
 exit(0);
}

static void sigreap(int signo) {
 while (waitpid(-1NULL, WNOHANG) > 0);
}

int main() {
 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;
}

如上所見,它不僅僅是休眠。它的一個重要功能是作爲pod中PID 1的角色,當殭屍被父進程孤立時,通過調用wait 來捕獲殭屍進程(參見sigreap)。這樣我們就不會讓殭屍在Kubernetes pod的PID命名空間中堆積。

一些關於PID命名空間共享的思考

值得注意的是,kubernetes在PID命名空間共享方面已經經歷了多次反覆修改。如果啓用了PID命名空間共享,那麼pause容器就可以幫你回收喪屍進程,這一配置目前只在Kubernetes 1.7+中可用。在Docker 1.13.1+及Kubernetes 1.7運行環境下,這一選項是默認開啓的。你也可以使用kubelet標誌禁用它(--docker-disable-share-pid=true)。但是這一配置又在Kubernetes 1.8中被修改,現在它在默認情況下是禁用的,除非使用kubelet標誌(--docker-disable-share-pid=false)啓用它。請參閱關於在這個GitHub問題中添加對PID命名空間共享的討論。

https://github.com/kubernetes/kubernetes/issues/1615

如果沒有啓用PID命名空間共享,那麼Kubernetes pod中的每個容器都有自己的PID 1,並且每個容器都需要自己捕獲殭屍進程。很多時候這不是一個問題,因爲應用程序不會生成其他進程,但是殭屍進程佔用內存是一個經常被忽略的問題。因此,由於PID命名空間共享使我們能夠在相同pod中的容器之間發送信號,本人非常希望PID命名空間共享成爲Kubernetes中的默認值。

  • 原文出處:https://zhuanlan.zhihu.com/p/81666226

熱門文章推薦

最後

  • 後臺回覆 【 列表】,可獲取本公衆號所有文章列表
  • 歡迎您加我微信【 ypxiaozhan01 】,拉您進技術羣,一起交流學習
  • 歡迎您關注【 YP小站 】,學習互聯網最流行的技術,做個專業的技術人

  【文章讓您有收穫,👇  或者 在看 支持我吧】

本文分享自微信公衆號 - YP小站(ypxiaozhan)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章