容器內init進程方案

進程標識符 (PID) 是Linux 內核爲每一個進程提供的惟一標識符。熟悉docker的同窗都知道, 全部的進程 PID都屬於某一個PID namespaces, 也就是說容器具備一組本身的 PID,這些 PID 映射到主機系統上的 PID。啓動Linux內核時啓動的第一個進程具備 PID 1,通常來講該進程就是 init 進程,例如 systemd 或 SysV。一樣,在容器中啓動的第一個進程也會得到該PID namespaces內的 PID 1。Docker 和 Kubernetes 使用信號與容器內的進程通訊,來終止容器的運行, 只能向容器內 PID 1 的進程發送信號。nginx

在容器的環境中,PID 和 Linux 信號會產生兩個須要考慮的問題。docker

問題 1:Linux 內核如何處理信號shell

對於具備 PID 1 的進程,Linux 內核處理信號的方式與其餘進程有所不一樣。系統不會自動爲此進程註冊信號處理函數,SIGTERM 或 SIGINT 等信號默認被忽略,必須使用 SIGKILL 來終止進程。使用 SIGKILL 可能會致使應用程序沒法平滑退出,例如正在寫入的數據出現不一致或正在處理的請求異常結束。ubuntu

問題 2:經典 init 系統如何處理孤立進程centos

宿主機上的init進程(如 systemd)也用來回收孤兒進程。孤兒進程(其父級已結束的進程)會從新附加到 PID 1 的進程,PID 1進程會在這些進程結束時回收它們。但在容器中,這一職責由具備 PID 1 的進程承擔,若是該進程沒法正確處理回收,則可能會出現耗盡內存或一些其餘資源的風險。bash

常見的解決方案ide

上述問題對於一些應用程序可能無足輕重,並不須要關注,可是對於一些面向用戶或者處理數據的應用程序卻極爲關鍵。須要嚴格防止。 對此有如下幾種解決方案:函數

解決方案 1:做爲 PID 1 運行並註冊信號處理程序學習

最簡單方法是使用 Dockerfile 中的 CMD 或 ENTRYPOINT 指令來啓動進程。例如,在如下 Dockerfile 中,nginx 是第一個也是惟一一個要啓動的進程。ui

FROM debian:9

RUN apt-get update && \

apt-get install -y nginx

EXPOSE 80

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

nginx 進程會註冊本身的信號處理程序。若是是咱們本身寫的程序則須要本身在代碼中執行相同操做。

由於咱們的進程就是PID 1進程,因此能夠保證可以正確的收到並處理信號。 這種方式能夠輕鬆地解決了第一個問題,可是對於第二個問題卻沒法解決。 若是你的應用程序不會產生多餘的子進程,則第二個問題也不存在。 能夠直接採用這種相對簡單的解決方案。

此處須要注意,有時候咱們可能一不當心就讓咱們的進程不是容器內首進程了,例如以下Dockerfile:

FROM tagedcentos:7

ADD command /usr/bin/command

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

咱們只是想執行啓動命令而已,卻發現此時首進程變爲了shell:

[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會自動地判斷你當前啓動命令是否由多個命令組成,若是是多個命令則會用shell來解釋。若是是單個命令則就算外面包了一層shell容器內首進程也直接是業務進程。例如若是將dockerfile寫成CMD bash -c "/usr/bin/command",容器內首進程仍是業務進程,以下:

[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也可讓咱們避免掉不少問題。

有時,咱們可能須要在容器中準備環境,以便進程可以正常運行。在此狀況下,通常咱們會讓容器在啓動時執行一個 shell 腳本。此 shell 腳本的任務是準備環境和啓動主進程。可是,若是採用此方法,shell腳本將是PID 1 而不是咱們的進程。所以必須使用內置的 exec 命令從 shell 腳本啓動進程。exec 命令會將腳本替換爲咱們所需的程序, 這樣咱們的業務進程將成爲 PID 1。

解決方案 2:使用專用 init 進程

正如在傳統宿主機所作的那樣,還可使用init進程來處理這些問題。可是, 傳統的init進程(例如 systemd 或 SysV)太過複雜而龐大,建議使用專爲容器建立的init進程(例如 tini)。

若是使用專用 init 進程,則 init 進程具備 PID 1 並執行如下操做:

註冊正確的信號處理程序。init進程會將信號傳遞給業務進程

回收殭屍進程

能夠經過使用 docker run 命令的 --init 選項在 Docker 中使用此解決方案。可是目前kubernetes還不支持直接使用該方案,須要在啓動命令前手動指定。

落地的難題

上面兩種解決方案看似美好,實則在實施的過程當中仍是存在不少弊端。

方案一須要嚴格保證用戶進程是首進程而且不能fork出多餘的其餘進程。 有時候咱們在啓動的時候須要執行一個shell腳本準備環境, 或者須要運行多個命令,例如'sleep 10 && cmd', 此時容器內首進程便爲shell,就會碰到問題一, 沒法轉發信號。 若是咱們限制用戶的啓動命令不能包含shell語法, 對用戶體驗也不太好。 而且做爲PASS平臺,咱們須要爲用戶提供一個簡單友好的接入環境,幫用戶處理好相關的問題。 從另一方面考慮, 在容器環境下多進程在所不免,即便咱們在啓動時確保只運行一個進程,有時候在運行時過程當中也會fork出進程。 咱們沒法確保咱們所使用的第三方組件或者開源的方案不會產生子進程, 咱們稍不注意就會碰到第二個問題,殭屍進程沒法回收的囧境。

方案二中須要在容器中有一個init進程負責完成全部的這些任務, 當前業務廣泛的作法是, 在構建鏡像的時候裏面自帶init進程,負責處理上面全部的問題。 這種方案當然可行,可是須要讓全部人都使用這種方式彷佛有點難以接受。首先對用戶鏡像有侵入,用戶必須修改已有的Dockerfile, 專門增長init進程 或者 只能在包含有該init進程的基礎鏡像上面進行構建。 其次管理起來比較麻煩,若是init進程升級,意味着所有鏡像都得從新build,這彷佛沒法接受。即便使用docker默認支持的tini,也有一些其餘問題,咱們後面會談到。

歸根結底, 做爲PASS平臺,咱們想給用戶提供一個便捷的接入環境,幫助用戶解決這些問題:

用戶進程可以收到信號, 進行一些優雅的退出

容許用戶產生多進程,而且在多進程的狀況下幫助用戶回收殭屍進程。

不對用戶的運行命令作約束,容許用戶填寫各類shell格式的命令,都可以解決上述1和2問題

解決方案

若是咱們想要對用戶無侵入,則最好使用docker或kubernetes原生支持的方案。

上面已經介紹過了docker run --init選項, docker原生提供的init進程實則爲tini。tini支持給進程組傳遞信號, 經過-g參數或者TINI_KILL_PROCESS_GROUP來進行開啓該功能。 開啓該功能後咱們就能夠將tini做爲首進程,而後讓它傳遞信號給全部的子進程。問題一就能夠輕鬆解決。 例如咱們執行 docker run -d --init ubuntu:14.04 bash -c "cd /home/ && sleep 100" 就會發現容器內的進程視圖以下:

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

此時1號docker-init進程,也就是tini進程, 負責轉發信號到全部的子進程,而且回收殭屍進程, tini的子進程爲6號bash進程, 它負責執行shell命令,能夠執行多個命令。這裏有一個問題就是: tini進程只會監聽他的直接子進程,若是直接子進程退出則整個容器就視爲退出了, 也就是本例中的6號bash進程。 若是咱們往容器中發送SIGTERM,可能用戶進程註冊了信號處理函數, 收到信號後處理須要必定的時間完成,可是因爲bash沒有註冊SIGTERM信號處理函數,會直接退出,進而致使tini退出,整個容器退出。用戶進程的信號處理函數尚未執行完畢就被強制退出了。咱們須要想辦法讓bash忽略掉這個信號,同事提到bash在交互模式下不會處理SIGTERM信號, 能夠一試。 在啓動命令前面加上bash -ci便可。發現使用bash交互模式啓動用戶進程就可使bash忽略掉SIGTERM,而後等待業務的信號處理函數執行完畢整個容器再退出。

如此便完美解決了上述相關問題。 同時還收穫了另一個微不足道的好處:容器退出時更加快速。咱們知道kubernetes中容器退出的邏輯和docker同樣,先發送SIGTEMR 而後再發送SIGKILL, 對於大部分用戶來講,都不會處理SIGTERM信號,容器內1號進程收到該信號後默認的行爲是忽略該信號, 因而SIGTERM信號白白地被浪費掉,須要等待terminationGracePeriodSeconds以後才被刪除。既然用戶不處理SIGTERM,爲何不直接在收到SIGTERM以後就退出吶? 在當前咱們的解決方案下若是用戶有註冊該信號處理函數,則能正常處理。 若是沒有註冊則容器在收到SIGTERM以後就立刻退出,能夠加快退出速度。

目前因爲kubernetes中CRI並無直接提供能夠設置docker tini的方法,因此要想在kubernetes中使用tini就只能改代碼了,筆者的集羣中就是經過改代碼來實現的。爲了解決用戶的痛點,咱們有能力也有義務爲合理的需求改代碼,何況這個改動足夠小,很是簡單。

後記

在容器落地的過程當中會碰到各類實際的問題,開源的方案可能沒法覆蓋到咱們全部的需求,須要咱們在精通社區的實現基礎上進行輕微的變形便可完美適應企業內部的場景。

日記本相關推薦此次疫情教會個人道理閱讀 21596學習Python能夠作哪些副業,你是否是感受本身錯過了一個億?閱讀 1263吐槽一下我12歲的小叔子,真讓我大開眼界啊!閱讀 940武漢封城第四十三天:是誰模糊了咱們的眼睛閱讀 1688懷孕了閱讀 7095

相關文章
相關標籤/搜索