容器中的一号进程
微信公眾號(hào):運(yùn)維開發(fā)故事,作者:夏老師
如何理解 init 進(jìn)程?
linux 進(jìn)程在樹中排序。每個(gè)進(jìn)程都可以產(chǎn)生子進(jìn)程,并且除了最頂層的進(jìn)程之外,每個(gè)進(jìn)程都有一個(gè)父進(jìn)程。
一旦我們啟動(dòng)了多個(gè)進(jìn)程,那么容器里就會(huì)出現(xiàn)一個(gè) pid 1,也就是我們常說的 1 號(hào)進(jìn)程或者 init 進(jìn)程,然后由這個(gè)進(jìn)程創(chuàng)建出其他的子進(jìn)程。接下來,我?guī)闶崂硪幌?init 進(jìn)程是怎么來的。
一個(gè) Linux 操作系統(tǒng),在系統(tǒng)打開電源,執(zhí)行 BIOS/boot-loader 之后,就會(huì)由 boot-loader 負(fù)責(zé)加載 Linux 內(nèi)核。Linux 內(nèi)核執(zhí)行文件一般會(huì)放在 /boot 目錄下,文件名類似 vmlinuz*。在內(nèi)核完成了操作系統(tǒng)的各種初始化之后,這個(gè)程序需要執(zhí)行的第一個(gè)用戶態(tài)程就是 init 進(jìn)程。
內(nèi)核代碼啟動(dòng) 1 號(hào)進(jìn)程的時(shí)候,在沒有外面參數(shù)指定程序路徑的情況下,一般會(huì)從幾個(gè)缺省路徑嘗試執(zhí)行 1 號(hào)進(jìn)程的代碼。這幾個(gè)路徑都是 Unix 常用的可執(zhí)行代碼路徑。
系統(tǒng)啟動(dòng)的時(shí)候先是執(zhí)行內(nèi)核態(tài)的代碼,然后在內(nèi)核中調(diào)用 1 號(hào)進(jìn)程的代碼,從內(nèi)核態(tài)切換到用戶態(tài)。
目前主流的 Linux 發(fā)行版,無論是 RedHat 系的還是 Debian 系的,都會(huì)把 /sbin/init 作為符號(hào)鏈接指向 Systemd。Systemd 是目前最流行的 Linux init 進(jìn)程,在它之前還有 SysVinit、UpStart 等 Linux init 進(jìn)程。
docker中的init
在 Linux 上有了容器的概念之后,一旦容器建立了自己的 Pid Namespace(進(jìn)程命名空間),這個(gè) Namespace 里的進(jìn)程號(hào)也是從 1 開始標(biāo)記的。所以,容器的 init 進(jìn)程也被稱為 1 號(hào)進(jìn)程。你只需要記住:1 號(hào)進(jìn)程是第一個(gè)用戶態(tài)的進(jìn)程,由它直接或者間接創(chuàng)建了 Namespace 中的其他進(jìn)程。
每個(gè)Docker容器都是一個(gè)PID命名空間,這意味著容器中的進(jìn)程與主機(jī)上的其他進(jìn)程是隔離的。PID命名空間是一棵樹,從PID 1開始,通常稱為init。
注意:當(dāng)你運(yùn)行一個(gè)Docker容器時(shí),鏡像的ENTRYPOINT就是你的根進(jìn)程,即PID 1(如果你沒有ENTRYPOINT,那么CMD就會(huì)作為根進(jìn)程,你可能配置了一個(gè)shell腳本,或其他的可執(zhí)行程序,容器的根進(jìn)程具體是什么,完全取決于你的配置)。
PID 1在處理kill信號(hào)的特別之處
與其他進(jìn)程不同的是:
-
PID 1它會(huì)忽略具有默認(rèn)操作的任何信號(hào)。因此除非經(jīng)過編碼,否則應(yīng)用沒有監(jiān)聽 SIGTERM 信號(hào),或者應(yīng)用中沒有實(shí)現(xiàn)處理 SIGTERM 信號(hào)的邏輯,應(yīng)用就不會(huì)停止。比如默認(rèn)的Bash與C語言的程序,是沒有注冊(cè)SIGTERM 信號(hào)的handler;
-
PID 1永遠(yuǎn)不會(huì)響應(yīng) SIGKILL 和 SIGSTOP 這兩個(gè)特權(quán)信號(hào);
-
對(duì)于其他的信號(hào),如果用戶自己注冊(cè)了 handler,1 號(hào)進(jìn)程可以響應(yīng)。
把Bash當(dāng)作PID 1呢?
每個(gè)基礎(chǔ)鏡像都有這個(gè)是Bash 。Bash 正確地收割了采用的子進(jìn)程。Bash 可以運(yùn)行任何東西。所以在你的Dockerfile中,你肯定會(huì)用這個(gè):
CMD ["/bin/bash", "-c", "/path-to-your-app"]Bash默認(rèn)不會(huì)處理SIGTERM信號(hào),因此這將會(huì)導(dǎo)致如下的問題:第一個(gè)問題是:如果將Bash作為PID 1運(yùn)行,那么發(fā)送到Docker容器docker stop的信號(hào),最終都是將 SIGTERM信號(hào)發(fā)送到Bash,但是Bash默認(rèn)不會(huì)處理SIGTERM信號(hào),也不會(huì)將它們轉(zhuǎn)發(fā)到任何地方(除非您自己編寫代碼實(shí)現(xiàn))。docker stop命令執(zhí)行后,容器會(huì)有一個(gè)關(guān)閉的時(shí)限,默認(rèn)為10秒,超過十秒則用kill強(qiáng)制關(guān)閉。換句話說,給 Bash發(fā)送SIGTERM信號(hào)終止時(shí),會(huì)等待十秒鐘,然后被內(nèi)核強(qiáng)制終止包含所有進(jìn)程的整個(gè)容器。這些進(jìn)程通過 SIGKILL 信號(hào)不正常地終止。SIGKILL是特權(quán)信號(hào),無法被捕獲,因此進(jìn)程無法干凈地終止。假設(shè)服務(wù)正在運(yùn)行的應(yīng)用程序正忙于寫入文件;如果應(yīng)用程序在寫入過程中不干凈地終止,文件可能會(huì)損壞。不干凈的終止是不好的。這幾乎就像從服務(wù)器上拔下電源插頭一樣。
第二個(gè)問題是:一旦進(jìn)程退出,Bash也會(huì)繼續(xù)退出。如果程序出了bug退出了,Bash會(huì)退出,退出代碼為0,而進(jìn)程實(shí)際上崩潰了(但0表示“一切正常”;這將導(dǎo)致Docker或者k8s上重啟策略不符合預(yù)期)。因?yàn)檎嬲胍目赡苁荁ash返回與的進(jìn)程相同的退出代碼。
請(qǐng)注意,我們對(duì)bash進(jìn)行修改,編寫一個(gè) EXIT 處理程序,它只是向子進(jìn)程發(fā)送信號(hào):
#!/bin/bash function cleanup() {local pids=`jobs -p`if [[ "$pids" != "" ]]; thenkill $pids >/dev/null 2>/dev/nullfi }trap cleanup EXIT /path-to-your-app不幸的是,這并不能解決問題。向子進(jìn)程發(fā)送信號(hào)是不夠的:init 進(jìn)程還必須等待子進(jìn)程終止,然后才能終止自己。如果 init 進(jìn)程過早終止,那么所有子進(jìn)程都會(huì)被內(nèi)核不干凈地終止。
很明顯,需要一個(gè)更復(fù)雜的解決方案,但是像 Upstart、Systemd 和 SysV init 這樣的完整 init 系統(tǒng)對(duì)于輕量級(jí) Docker 容器來說太過分了。幸運(yùn)的是,我們有很多在容器中使用的init程序。我們這里推薦使用簡(jiǎn)單的tini。
tini當(dāng)作PID 1
我們?cè)谌萜髦袉?dòng)一個(gè)init 系統(tǒng)有很多種,這里推薦使用 tini,它是專用于容器的輕量級(jí) init 系統(tǒng),使用方法也很簡(jiǎn)單:
FROM openjdk8:8u201-jdk-alpine3.9 RUN apk add --no-cache tini wget \&& mkdir -p /opt/arthas \&& cd /opt/arthas \&& wget https://arthas.aliyun.com/arthas-boot.jar ENTRYPOINT ["/sbin/tini", "--"]請(qǐng)注意,Tini中還有一些額外的功能,在Bash或Java中很難實(shí)現(xiàn)(例如,Tini可以注冊(cè)為“子收割者”,因此它實(shí)際上不需要作為PID 1運(yùn)行來完成“僵尸進(jìn)程”收割工作),但是這些功能對(duì)于一些高級(jí)應(yīng)用場(chǎng)景來說非常有用。
為什么docker中會(huì)有僵尸進(jìn)程?
使用容器的理想境界是一個(gè)容器只啟動(dòng)一個(gè)進(jìn)程,但這在現(xiàn)實(shí)應(yīng)用中有時(shí)是做不到的。
比如說,在一個(gè)容器中除了主進(jìn)程之外,我們可能還會(huì)啟動(dòng)輔助進(jìn)程,做監(jiān)控或者 rotate logs;再比如說,我們需要把原來運(yùn)行在虛擬機(jī)(VM)的程序移到容器里,這些原來跑在虛擬機(jī)上的程序本身就是多進(jìn)程的。
一旦我們啟動(dòng)了多個(gè)進(jìn)程,那么容器里就會(huì)出現(xiàn)一個(gè) pid 1,也就是我們常說的 1 號(hào)進(jìn)程或者 init 進(jìn)程,然后由這個(gè)進(jìn)程創(chuàng)建出其他的子進(jìn)程。比如我們?cè)诓渴餵ava服務(wù)的時(shí)候,我們需要部署一個(gè)Arthas(阿爾薩斯),來做為java程序的診斷工具。
總結(jié)
第一個(gè)概念是 Linux 1 號(hào)進(jìn)程。它是第一個(gè)用戶態(tài)的進(jìn)程。它直接或者間接創(chuàng)建了 Namespace 中的其他進(jìn)程。第二個(gè)概念是容器里 1 號(hào)進(jìn)程對(duì)信號(hào)處理的三個(gè)要點(diǎn):
-
PID 1沒有默認(rèn)的信號(hào)處理程序。如果應(yīng)用沒有監(jiān)聽 SIGTERM 信號(hào),或者應(yīng)用中沒有實(shí)現(xiàn)處理 SIGTERM 信號(hào)的邏輯,應(yīng)用就不會(huì)停止,容器也不會(huì)終止。
-
在容器中,1 號(hào)進(jìn)程永遠(yuǎn)不會(huì)響應(yīng) SIGKILL 和 SIGSTOP 這兩個(gè)特權(quán)信號(hào);
-
對(duì)于其他的信號(hào),如果用戶自己注冊(cè)了 handler,1 號(hào)進(jìn)程可以響應(yīng)。
第三個(gè)概念是tini作為1號(hào)進(jìn)程可以給子進(jìn)程傳遞SIGTERM信號(hào)和收割僵尸進(jìn)程。
總結(jié)
- 上一篇: 2021年 最新编程语言排行
- 下一篇: 哈哈!一个 print 函数,还挺会玩啊