溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

經(jīng)典init系統(tǒng)如何處理孤立進(jìn)程?

發(fā)布時(shí)間:2020-05-23 13:44:18 來(lái)源:億速云 閱讀:353 作者:鴿子 欄目:大數(shù)據(jù)

進(jìn)程標(biāo)識(shí)符 (PID) 是Linux 內(nèi)核為每個(gè)進(jìn)程提供的唯一標(biāo)識(shí)符。熟悉docker的同學(xué)都知道, 所有的進(jìn)程 PID都屬于某一個(gè)PID namespaces, 也就是說(shuō)容器具有一組自己的 PID,這些 PID 映射到主機(jī)系統(tǒng)上的 PID。啟動(dòng)Linux內(nèi)核時(shí)啟動(dòng)的第一個(gè)進(jìn)程具有 PID 1,一般來(lái)說(shuō)該進(jìn)程就是 init 進(jìn)程,例如 systemd 或 SysV。同樣,在容器中啟動(dòng)的第一個(gè)進(jìn)程也會(huì)獲得該P(yáng)ID namespaces內(nèi)的 PID 1。Docker 和 Kubernetes 使用信號(hào)與容器內(nèi)的進(jìn)程通信,來(lái)終止容器的運(yùn)行, 只能向容器內(nèi) PID 1 的進(jìn)程發(fā)送信號(hào)。

在容器的環(huán)境中,PID 和 Linux 信號(hào)會(huì)產(chǎn)生兩個(gè)需要考慮的問(wèn)題。

問(wèn)題 1:Linux 內(nèi)核如何處理信號(hào)

對(duì)于具有 PID 1 的進(jìn)程,Linux 內(nèi)核處理信號(hào)的方式與其他進(jìn)程有所不同。系統(tǒng)不會(huì)自動(dòng)為此進(jìn)程注冊(cè)信號(hào)處理函數(shù),SIGTERM 或 SIGINT 等信號(hào)默認(rèn)被忽略,必須使用 SIGKILL 來(lái)終止進(jìn)程。使用 SIGKILL 可能會(huì)導(dǎo)致應(yīng)用程序無(wú)法平滑退出,例如正在寫入的數(shù)據(jù)出現(xiàn)不一致或正在處理的請(qǐng)求異常結(jié)束。

問(wèn)題 2:經(jīng)典 init 系統(tǒng)如何處理孤立進(jìn)程

宿主機(jī)上的init進(jìn)程(如 systemd)也用來(lái)回收孤兒進(jìn)程。孤兒進(jìn)程(其父級(jí)已結(jié)束的進(jìn)程)會(huì)重新附加到 PID 1 的進(jìn)程,PID 1進(jìn)程會(huì)在這些進(jìn)程結(jié)束時(shí)回收它們。但在容器中,這一職責(zé)由具有 PID 1 的進(jìn)程承擔(dān),如果該進(jìn)程無(wú)法正確處理回收,則可能會(huì)出現(xiàn)耗盡內(nèi)存或一些其他資源的風(fēng)險(xiǎn)。

常見(jiàn)的解決方案

上述問(wèn)題對(duì)于一些應(yīng)用程序可能無(wú)足輕重,并不需要關(guān)注,但是對(duì)于一些面向用戶或者處理數(shù)據(jù)的應(yīng)用程序卻極為關(guān)鍵。需要嚴(yán)格防止。 對(duì)此有以下幾種解決方案:

解決方案 1:作為 PID 1 運(yùn)行并注冊(cè)信號(hào)處理程序

最簡(jiǎn)單方法是使用 Dockerfile 中的 CMD 或 ENTRYPOINT 指令來(lái)啟動(dòng)進(jìn)程。例如,在以下 Dockerfile 中,nginx 是第一個(gè)也是唯一一個(gè)要啟動(dòng)的進(jìn)程。

FROM debian:9

RUN apt-get update && \

apt-get install -y nginx

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

nginx 進(jìn)程會(huì)注冊(cè)自己的信號(hào)處理程序。如果是我們自己寫的程序則需要自己在代碼中執(zhí)行相同操作。

因?yàn)槲覀兊倪M(jìn)程就是PID 1進(jìn)程,所以可以保證能夠正確的收到并處理信號(hào)。 這種方式可以輕松地解決了第一個(gè)問(wèn)題,但是對(duì)于第二個(gè)問(wèn)題卻無(wú)法解決。 如果你的應(yīng)用程序不會(huì)產(chǎn)生多余的子進(jìn)程,則第二個(gè)問(wèn)題也不存在。 可以直接采用這種相對(duì)簡(jiǎn)單的解決方案。

此處需要注意,有時(shí)候我們可能一不小心就讓我們的進(jìn)程不是容器內(nèi)首進(jìn)程了,例如如下Dockerfile:

FROM tagedcentos:7

ADD command /usr/bin/command

CMD cd /usr/bin/ && ./command

我們只是想執(zhí)行啟動(dòng)命令而已,卻發(fā)現(xiàn)此時(shí)首進(jìn)程變?yōu)榱藄hell:

[root@425523c23893 /]# ps -ef

UID        PID  PPID  C STIME TTY          TIME CMD

root        1    0  1 07:05 pts/0    00:00:00 /bin/sh -c cd /usr/bin/ && ./command

root        6    1  0 07:05 pts/0    00:00:00 ./command

docker會(huì)自動(dòng)地判斷你當(dāng)前啟動(dòng)命令是否由多個(gè)命令組成,如果是多個(gè)命令則會(huì)用shell來(lái)解釋。如果是單個(gè)命令則就算外面包了一層shell容器內(nèi)首進(jìn)程也直接是業(yè)務(wù)進(jìn)程。例如如果將dockerfile寫成CMD bash -c "/usr/bin/command",容器內(nèi)首進(jìn)程還是業(yè)務(wù)進(jìn)程,如下:

[root@c380600ce1c4 /]# ps -ef

UID        PID  PPID  C STIME TTY          TIME CMD

root        1    0  2 13:09 ?        00:00:00 /usr/bin/command

所以正確地書寫Dockerfile也可以讓我們避免掉很多問(wèn)題。

有時(shí),我們可能需要在容器中準(zhǔn)備環(huán)境,以便進(jìn)程能夠正常運(yùn)行。在此情況下,一般我們會(huì)讓容器在啟動(dòng)時(shí)執(zhí)行一個(gè) shell 腳本。此 shell 腳本的任務(wù)是準(zhǔn)備環(huán)境和啟動(dòng)主進(jìn)程。但是,如果采用此方法,shell腳本將是PID 1 而不是我們的進(jìn)程。因此必須使用內(nèi)置的 exec 命令從 shell 腳本啟動(dòng)進(jìn)程。exec 命令會(huì)將腳本替換為我們所需的程序, 這樣我們的業(yè)務(wù)進(jìn)程將成為 PID 1。

解決方案 2:使用專用 init 進(jìn)程

正如在傳統(tǒng)宿主機(jī)所做的那樣,還可以使用init進(jìn)程來(lái)處理這些問(wèn)題。但是, 傳統(tǒng)的init進(jìn)程(例如 systemd 或 SysV)太過(guò)復(fù)雜而龐大,建議使用專為容器創(chuàng)建的init進(jìn)程(例如 tini)。

如果使用專用 init 進(jìn)程,則 init 進(jìn)程具有 PID 1 并執(zhí)行以下操作:

注冊(cè)正確的信號(hào)處理程序。init進(jìn)程會(huì)將信號(hào)傳遞給業(yè)務(wù)進(jìn)程

回收僵尸進(jìn)程

可以通過(guò)使用 docker run 命令的 --init 選項(xiàng)在 Docker 中使用此解決方案。但是目前kubernetes還不支持直接使用該方案,需要在啟動(dòng)命令前手動(dòng)指定。

落地的難題

上面兩種解決方案看似美好,實(shí)則在實(shí)施的過(guò)程中還是存在很多弊端。

方案一需要嚴(yán)格保證用戶進(jìn)程是首進(jìn)程并且不能fork出多余的其他進(jìn)程。 有時(shí)候我們?cè)趩?dòng)的時(shí)候需要執(zhí)行一個(gè)shell腳本準(zhǔn)備環(huán)境, 或者需要運(yùn)行多個(gè)命令,例如'sleep 10 && cmd', 此時(shí)容器內(nèi)首進(jìn)程便為shell,就會(huì)碰到問(wèn)題一, 無(wú)法轉(zhuǎn)發(fā)信號(hào)。 如果我們限制用戶的啟動(dòng)命令不能包含shell語(yǔ)法, 對(duì)用戶體驗(yàn)也不太好。 并且作為PASS平臺(tái),我們需要為用戶提供一個(gè)簡(jiǎn)單友好的接入環(huán)境,幫用戶處理好相關(guān)的問(wèn)題。 從另外一方面考慮, 在容器環(huán)境下多進(jìn)程在所難免,即使我們?cè)趩?dòng)時(shí)確保只運(yùn)行一個(gè)進(jìn)程,有時(shí)候在運(yùn)行時(shí)過(guò)程中也會(huì)fork出進(jìn)程。 我們無(wú)法確保我們所使用的第三方組件或者開(kāi)源的方案不會(huì)產(chǎn)生子進(jìn)程, 我們稍不注意就會(huì)碰到第二個(gè)問(wèn)題,僵尸進(jìn)程無(wú)法回收的囧境。

方案二中需要在容器中有一個(gè)init進(jìn)程負(fù)責(zé)完成所有的這些任務(wù), 當(dāng)前業(yè)務(wù)普遍的做法是, 在構(gòu)建鏡像的時(shí)候里面自帶init進(jìn)程,負(fù)責(zé)處理上面所有的問(wèn)題。 這種方案固然可行,但是需要讓所有人都使用這種方式似乎有點(diǎn)難以接受。首先對(duì)用戶鏡像有侵入,用戶必須修改已有的Dockerfile, 專門增加init進(jìn)程 或者 只能在包含有該init進(jìn)程的基礎(chǔ)鏡像上面進(jìn)行構(gòu)建。 其次管理起來(lái)比較麻煩,如果init進(jìn)程升級(jí),意味著全部鏡像都得重新build,這似乎無(wú)法接受。即使使用docker默認(rèn)支持的tini,也有一些其他問(wèn)題,我們后面會(huì)談到。

歸根結(jié)底, 作為PASS平臺(tái),我們想給用戶提供一個(gè)便捷的接入環(huán)境,幫助用戶解決這些問(wèn)題:

用戶進(jìn)程能夠收到信號(hào), 進(jìn)行一些優(yōu)雅的退出

允許用戶產(chǎn)生多進(jìn)程,并且在多進(jìn)程的情況下幫助用戶回收僵尸進(jìn)程。

不對(duì)用戶的運(yùn)行命令做約束,允許用戶填寫各種shell格式的命令,都能夠解決上述1和2問(wèn)題

解決方案

如果我們想要對(duì)用戶無(wú)侵入,則最好使用docker或kubernetes原生支持的方案。

上面已經(jīng)介紹過(guò)了docker run --init選項(xiàng), docker原生提供的init進(jìn)程實(shí)則為tini。tini支持給進(jìn)程組傳遞信號(hào), 通過(guò)-g參數(shù)或者TINI_KILL_PROCESS_GROUP來(lái)進(jìn)行開(kāi)啟該功能。 開(kāi)啟該功能后我們就可以將tini作為首進(jìn)程,然后讓它傳遞信號(hào)給所有的子進(jìn)程。問(wèn)題一就可以輕松解決。 例如我們執(zhí)行 docker run -d --init ubuntu:14.04 bash -c "cd /home/ && sleep 100" 就會(huì)發(fā)現(xiàn)容器內(nèi)的進(jìn)程視圖如下:

root@24cc26039c4d:/# ps -ef

UID        PID  PPID  C STIME TTY          TIME CMD

root        1    0  2 14:50 ?        00:00:00 /sbin/docker-init -- bash -c cd /home/ && sleep 100

root        6    1  0 14:50 ?        00:00:00 bash -c cd /home/ && sleep 100

root        7    6  0 14:50 ?        00:00:00 sleep 100

此時(shí)1號(hào)docker-init進(jìn)程,也就是tini進(jìn)程, 負(fù)責(zé)轉(zhuǎn)發(fā)信號(hào)到所有的子進(jìn)程,并且回收僵尸進(jìn)程, tini的子進(jìn)程為6號(hào)bash進(jìn)程, 它負(fù)責(zé)執(zhí)行shell命令,可以執(zhí)行多個(gè)命令。這里有一個(gè)問(wèn)題就是: tini進(jìn)程只會(huì)監(jiān)聽(tīng)他的直接子進(jìn)程,如果直接子進(jìn)程退出則整個(gè)容器就視為退出了, 也就是本例中的6號(hào)bash進(jìn)程。 如果我們往容器中發(fā)送SIGTERM,可能用戶進(jìn)程注冊(cè)了信號(hào)處理函數(shù), 收到信號(hào)后處理需要一定的時(shí)間完成,但是由于bash沒(méi)有注冊(cè)SIGTERM信號(hào)處理函數(shù),會(huì)直接退出,進(jìn)而導(dǎo)致tini退出,整個(gè)容器退出。用戶進(jìn)程的信號(hào)處理函數(shù)還沒(méi)有執(zhí)行完畢就被強(qiáng)制退出了。我們需要想辦法讓bash忽略掉這個(gè)信號(hào),同事提到bash在交互模式下不會(huì)處理SIGTERM信號(hào), 可以一試。 在啟動(dòng)命令前面加上bash -ci即可。發(fā)現(xiàn)使用bash交互模式啟動(dòng)用戶進(jìn)程就可以使bash忽略掉SIGTERM,然后等待業(yè)務(wù)的信號(hào)處理函數(shù)執(zhí)行完畢整個(gè)容器再退出。

如此便完美解決了上述相關(guān)問(wèn)題。 同時(shí)還收獲了另外一個(gè)微不足道的好處:容器退出時(shí)更加快速。我們知道kubernetes中容器退出的邏輯和docker一樣,先發(fā)送SIGTEMR 然后再發(fā)送SIGKILL, 對(duì)于大部分用戶來(lái)說(shuō),都不會(huì)處理SIGTERM信號(hào),容器內(nèi)1號(hào)進(jìn)程收到該信號(hào)后默認(rèn)的行為是忽略該信號(hào), 于是SIGTERM信號(hào)白白地被浪費(fèi)掉,需要等待terminationGracePeriodSeconds之后才被刪除。既然用戶不處理SIGTERM,為什么不直接在收到SIGTERM之后就退出吶? 在當(dāng)前我們的解決方案下如果用戶有注冊(cè)該信號(hào)處理函數(shù),則能正常處理。 如果沒(méi)有注冊(cè)則容器在收到SIGTERM之后就馬上退出,可以加快退出速度。

目前由于kubernetes中CRI并沒(méi)有直接提供可以設(shè)置docker tini的方法,所以要想在kubernetes中使用tini就只能改代碼了,筆者的集群中就是通過(guò)改代碼來(lái)實(shí)現(xiàn)的。為了解決用戶的痛點(diǎn),我們有能力也有義務(wù)為合理的需求改代碼,況且這個(gè)改動(dòng)足夠小,非常簡(jiǎn)單。

后記

在容器落地的過(guò)程中會(huì)碰到各種實(shí)際的問(wèn)題,開(kāi)源的方案可能無(wú)法覆蓋到我們所有的需求,需要我們?cè)诰ㄉ鐓^(qū)的實(shí)現(xiàn)基礎(chǔ)上進(jìn)行輕微的變形即可完美適應(yīng)企業(yè)內(nèi)部的場(chǎng)景。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI