當構建Docker 容器時,需要注意PID 1 僵屍回收問題,那個問題會在你最不期望出現問題的時候,導致一些不期望的結果和看起來很困惑的問題。本文解釋了PID 1問題,解釋怎樣解決它,並且作為一個預先構建的方案--可以作為一個基本的Docker鏡像來使用。
當上面的問題解決了,你可能會想去讀 第二部分:Docker基礎鏡像,胖容器 和 “把容器當虛擬機”
介紹
大概一年前-回溯到Docker0.6 的時期-我第一次介紹Docker基礎鏡像。這是為了對Docker友好而修改的最小的Ubuntu 基礎鏡像。其他人可以 從Docker 登記下載Docker基礎鏡像並且把它作為他們自己鏡像的基礎鏡像。
我們是早期的Docker使用者,用Docker來持續集成並且在Docker達到1.0版本之前,用作搭建開發環境的方式。為了解決一些使用 Docker能夠解決的問題,我們開發了docker基礎鏡像。例如,Docker不會在某個恰當處理子進程的初始化進程下運行進程。以至於容器有可能結 束導致各種各樣的問題僵屍進程。Docker 也不會做任何事情,以至於讓重要的消息能夠正常的被處理等等。
然而,我們已經發現很多人對我們解決的問題理解上有問題。Granted,是Unix操作系統底層很少人知道和理解的系統級機制。所以在本文中我們將會詳細描述這個我們已經解決了的最重要的PID 1僵屍進程問題。
我們發現:
我們解決的問題適用於很多人
大多數人甚至沒有意識到這些問題,所以很多事情會以意想不到的方式被打斷(墨菲定律)
如果每個人都一遍又一遍的重復解決這些問題是低效率的。
所以我們在空閒時間,把解決方案提取為每個人可以復用的基礎鏡像:Baseimage-docker.這個鏡像也加入了一些有用的,相信大多數Docker鏡像開發者都需要的工具。我們把Baseimage-docker作為我們所有Docker鏡像的一個基礎鏡像。
社區看起來喜歡我們所做得事情:我們是Docker注冊處最流行的第三方鏡像。只是排在了官方的Ubuntu和CentOServer鏡像下面。
PID 1 問題: 進程僵屍
回想一下Unix的進程是一個有序的樹。每個進程可以派生子進程,每個進程具有一個除了最頂層以外的父進程。
這 個最頂層的進程是init進程。它是當你啟動系統時由內核啟動。這個init進程負責啟動系統的其余部分,如啟動SSH服務,從啟動Docker 守護進程,啟動Apache / Nginx的,啟動你的GUI桌面環境,等等。他們每個進程都可能會反過來派生出更多的子進程。
到目前為止還沒有什麼特別的。但考慮到如果一個進程終止會發生什麼。比方說,bash(PID 5)進程終止。它變成了一個所謂的“停止活動的進程”,也稱為“僵屍進程”。
為什麼會這樣?這是因為Unix被設計為這樣一種方式,父進程必須明確地“等待”子進程終止,以便收集它的退出狀態.。僵屍進程一直存在,直到父進程已經執行該操作,使用系統調用waitpid()函數。我從手冊頁引用
“一個子進程終止了,但一直被等待就變成了”僵屍“。內核維護了一組關於僵屍進程最小的信息列表(PID,終止狀態,資源使用信息),為了讓父進程以後進行等待時,能夠獲取有關子進程的信息。”
在日常的語言中,人們認為“僵屍進程”是會造成嚴重破壞的混亂進程。但正式的說 - 從Unix操作系統觀點 - 僵屍進程有一個非常明確的定義。他們是已經終止,但沒有(還)被他們的父進程等待的進程。
大多數時間這都不是問題,在子進程上調用waitpid()的動作是為了消除它的僵死進程,這就是所謂的“收割”。許多應用正確的收割它們的子進程。在上 面的例子中用的是sshd,如果bash終止了然後操作系統將會向sshd發送一個SIGCHLD信號把它喚醒。sshd注意到了這個信號後就收割子進 程。
但是有個特殊情況,假設父進程終結了,或者是故意的(因為程序邏輯決定該退出系統了)或者是用戶的操作導致的(例如用戶將這個進程殺死了)。這個父進程的子進程將會發生什麼?他們不再有父進程了,所以他們變成了“孤兒”(這是實際的專業術語)。
這就是init進程起作用的地方。init進程--PID 1--有一個特殊的任務。就是“接收”孤兒進程(注意,這是實際的技術術語)。這就意味著init進程變為了這些進程的父進程。盡管這些進程從來都沒有被init進程直接創建。
拿Nginx作為例子,默認是作為後台守護進程。它是這麼工作的。第一,Nginx創建一個子進程。第二,原始的Nginx進程退出了。第三,Nginx子進程被init進程給接收了。
你可能知道我將要表達什麼。操作系統內核自動的處理收容,所以這就意味著內核期望init進程要有一個專門的職責:操作系統也期望init進程收割被接收的孤兒進程。
這是Unix系統中一個非常重要的職責。它是如此基礎的職責以至於很多很多軟件的都利用了這一點。所有的守護軟件非常期望被守護的子進程都被init進程收容和收割。
盡管我用守護進程作為例子,但不限於守護進程。每當一個進程退出了,雖然它還有子進程存在。這是因為它們期待init進程稍後來清理。這些已經詳細的在這兩本書中描述了:操作系統概念 著 Silberschatz等和Unix環境中的高級編程 著 Stevens 等。
為什麼僵屍進程是有害的
即使他們終止了進程,為什麼僵屍進程是一件壞事 ? 原始應用程序的內存已經被釋放,對啊?這不僅僅是一個條目,你在ps中看到它了嗎?
你是對的,原始應用程序的內存已經被釋放。 但事實上,你還看到它在ps中,這意味著它仍然占用一些內核資源。 我參考Linux waitpid手冊:
與Docker的關系“只要一個僵屍進程通過等待沒有在系統中被移除, 它就會在內核進程表中消耗一個位置,並且要是這個表被填滿,那它就沒辦法創建一個新的進程。”
那麼這怎麼涉及到Docker?我們看到很多人在他們的容器裡只運行一個進程,他們認為運行單進程,他們的工作就結束了。但是,這個進程寫出來並不是為了 完全像init進程的行為。也就是說,非但沒有恰當的收割被收容的孤兒進程,反而沒准它還期望其他的init進程來正確地做那樣的工作。
讓我們來看看具體的例子.假設你的容器運行了一個web服務器,web服務器運行一個CGI,它是用bash寫的腳本。CGI腳本調用grep.然 後web服務器決定CGI腳本運行的時間太長了並且殺死了這個腳本,但是grep 沒有受影響並繼續運行。當grep結束了,它成為了僵屍並且被PID 1收容(web服務器)。web服務器不知道grep,所以web容器不收割它,然後grep僵屍進程停留在系統中了。
這個問題也適用於其他狀況。人們經常為第三方應用創建Docker容器--比如PostgreSQL--並且把這些應用當做靈魂進程在容器中運行。當你正 運行其他人的代碼,你能確保這些應用接下來不會大量產生僵屍進程嗎?如果你運行你自己的代碼,並且你審計了類庫。沒發現問題。但是通常情況下還是應該運行 一個適當的init系統進程來阻止問題發生。
但是運行一個全初始化系統不會讓container重量級並且像一個虛擬機嗎?
一個初始化系統沒有必要是重量級的,你可能很輕易地就想到了Upstart,Systemd,SysV等初始化系統。可能你認為完整的系統需要在容器中被啟動。其實不是這樣的。我們所說的“全初始化系統”,是沒有必要的也不是令人滿意的。
我所談論的初始化系統是小的,它的唯一職責就是啟動你的應用,並且收割收容的子進程。使用如此簡單的初始化系統是完全符合Docker的哲學的。
一個簡單的初始化系統
是否已經存在一個能夠運行其他應用並且能夠同時收割收容的子進程的軟件?
有一個幾近完美的解決方案,每個人都有--它是簡單陳舊的bash. Bash正確的收割收容的子進程。Bash能夠運行任何事。所以不是要把這些放到你的Dockerfile中...
CMD ["/path-to-your-app"]
…你可能有興趣用這個替代:
CMD ["/bin/bash", "-c", "set -e && /path-to-your-app"]
(-e 指令阻止bash把這個腳本當做簡單的命令直接執行exec())
這回導致如下處理層次結構:
但是不幸的是,這個程序有一個致命問題,它沒有正確處理信號!假設你用kill發送SIGTERM信號給bash.Bash終止了,但是沒有發送SIGTERM給它的子進程!
當bash結束了,內核結束整個容器中的所有進程。包擴通過SIGKILL信號沒有被干淨的終結的進程。SIGKILL不能被捕獲,所以進程是沒有辦法干 淨的終結。假設你運行的應用程序正忙於寫文件;在寫的過程中,應用被不干淨的終止了這個文件可能會崩潰。不干淨的終止是很壞的事情。很像把服務器的電源給 拔掉。
但是為什麼要關心init進程是否被SIGTERM給終結了呢?那是因為docker stop 發送 SIGTERM信號給init進程了。“docker stop” 應該干淨的停止容器,以至於稍後你能夠用“docker start”啟動它。
Bash專家現在可能會有興趣寫一個EIXT處理器,它簡單的發送信號給子進程,像這樣:
#!/bin/bash function cleanup() { local pids=`jobs -p` if [[ "$pids" != "" ]]; then kill $pids >/dev/null 2>/dev/null fi } trap cleanup EXIT /path-to-your-app不幸的是,這個不解決問題。僅僅是給子進程發送信號是不夠的:init進程在終結自己前必須等待子進程終結。如果init進程過早的結束了,所有的子進程又沒有干淨的被內核終結。
所以明顯的一個更加復雜的解決方案是需要的。但是一個全初始化系統像 Upstart,Systemd 和SysV init對於輕量級的Docker容器來說就是趕盡殺絕。幸運的是,Baseimage-docker有一個解決方案。我們已經寫了一個自定義的,輕量級 的初始化系統。特別是在Docker容器內。由於缺少一個好的名字,我們把這個程序叫做my init,一個350行最小資源使用率的Python程序。
my_init的一些關鍵特性:
收割收容的子進程
執行子進程
等待直到所有的子進程都終結了才結束自己,並且用最大超時時間。
記錄活動到“docker日志文件”。
你可能會認為,我從來沒有看到它出現,所以機會是很小的。但是墨菲定律說道,當事情可能會出錯,那麼它們就會出錯。
除了僵屍進程持有內核資源這個事實外,僵屍進程的不離開也會干擾那些檢測進程是否存在的軟件。例如 Phusion 乘客應用服務器 管理進程。 它重啟那些崩潰的進程。崩潰監測通過分析 ps 的輸出實現和發送0信號到進程ID實現的。僵屍進程是通過ps 和對0信號的響應來顯示的,所以Phusion 乘客認為這個進程依然存活,盡管他已經終止了。
再想想取捨。阻止僵屍進程的發生這個問題的發生,你所需要做的就是花5分鐘,或者使用docker基礎鏡像,或者導入 our 350 lines my_init init system 到你的容器中。內存和磁盤占用很小:只占用內存和硬盤幾MB空間就能夠阻止墨菲定律發生。
總結
所以PID 1 問題是需要注意的的問題。一個方法就是使用Dock基礎鏡像。
是否Dock基礎鏡像是唯一的解決方式?當然不是,Dock基礎鏡像的主旨是:
1.使人們書一道一些重要Docker容器的警告和缺陷。
2.提供一些預先解決方案方便其他人使用,以至於其他人不至於針對此問題重新發明解決方案。
這也意味著多種解決方案的存在,一旦他們解決了我們描述的這個問題。你可以自由的重新用C,Go,Ruby 或者其他什麼語言來實現該解決方案。但是我們已經提供了一個很好的解決方案了,你為什麼還要這樣呢?
可能你不想使用Ubuntu作為基礎鏡像。可能你會使用CentOS。 但是不要停止使用image-docker所給你帶來的好處。 舉例來說,我們的 passenger_rpm_automation 項目使用CentOS容器。 我們簡單地提取了基礎的image-docker的my_init並且將其引入。
因此,即使你不使用, 或者不想要使用Baseimage-docker,好好看看我們描述的問題,考慮你能做什麼來解決這些問題。
快樂的Dockering.
第二部分: 我們將討論這一現象,很多人將Baseimage-docker與"胖容器"聯系起來。 Baseimage-docker根本不是關於胖容器的, 那麼他是什麼? 參看 Baseimage-docker,胖容器和 “將容器作為VM”
原文:http://www.oschina.net/translate/docker-and-the-pid-1-zombie-reaping-problem