您好,登錄后才能下訂單哦!
進程標識符 (PID) 是Linux 內核為每個進程提供的唯一標識符。熟悉docker的同學都知道, 所有的進程 PID都屬于某一個PID namespaces, 也就是說容器具有一組自己的 PID,這些 PID 映射到主機系統上的 PID。啟動Linux內核時啟動的第一個進程具有 PID 1,一般來說該進程就是 init 進程,例如 systemd 或 SysV。同樣,在容器中啟動的第一個進程也會獲得該PID namespaces內的 PID 1。Docker 和 Kubernetes 使用信號與容器內的進程通信,來終止容器的運行, 只能向容器內 PID 1 的進程發送信號。
在容器的環境中,PID 和 Linux 信號會產生兩個需要考慮的問題。
問題 1:Linux 內核如何處理信號
對于具有 PID 1 的進程,Linux 內核處理信號的方式與其他進程有所不同。系統不會自動為此進程注冊信號處理函數,SIGTERM 或 SIGINT 等信號默認被忽略,必須使用 SIGKILL 來終止進程。使用 SIGKILL 可能會導致應用程序無法平滑退出,例如正在寫入的數據出現不一致或正在處理的請求異常結束。
問題 2:經典 init 系統如何處理孤立進程
宿主機上的init進程(如 systemd)也用來回收孤兒進程。孤兒進程(其父級已結束的進程)會重新附加到 PID 1 的進程,PID 1進程會在這些進程結束時回收它們。但在容器中,這一職責由具有 PID 1 的進程承擔,如果該進程無法正確處理回收,則可能會出現耗盡內存或一些其他資源的風險。
常見的解決方案
上述問題對于一些應用程序可能無足輕重,并不需要關注,但是對于一些面向用戶或者處理數據的應用程序卻極為關鍵。需要嚴格防止。 對此有以下幾種解決方案:
解決方案 1:作為 PID 1 運行并注冊信號處理程序
最簡單方法是使用 Dockerfile 中的 CMD 或 ENTRYPOINT 指令來啟動進程。例如,在以下 Dockerfile 中,nginx 是第一個也是唯一一個要啟動的進程。
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就只能改代碼了,筆者的集群中就是通過改代碼來實現的。為了解決用戶的痛點,我們有能力也有義務為合理的需求改代碼,況且這個改動足夠小,非常簡單。
后記
在容器落地的過程中會碰到各種實際的問題,開源的方案可能無法覆蓋到我們所有的需求,需要我們在精通社區的實現基礎上進行輕微的變形即可完美適應企業內部的場景。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。