docker 殭屍進程

其他參考:

https://www.lagou.com/lgeduarticle/85136.html

| Docker容器生成殭屍進程

現象

公司開發服務器上使用Docker跑了幾個容器,這些容器都是長時間運行的。偶然發現服務器上有大量殭屍進程,大約有兩三千個。簡單跟蹤了下,發現這些殭屍進程均是在容器的進程命名空間的。

12

ps aux | grep 'Z' | grep -v grepll /proc/${any_zombie_pid}

在容器裏運行的程序是很正常的web server,怎麼會這樣呢?

Docker 和子進程“殭屍化”問題

初始進程的責任:“收割”“殭屍進程”

Unix 的進程之間是樹狀結構的關係。每個進程都可以派生出子進程,而除了最頂端的進程之外,也都會有一個父進程。

這個最頂端的進程就是初始進程,其在啓動系統時被內核啓動,並負責啓動系統的其餘功能部分。如:SSH 後臺程序、Docker 後臺程序、Apache/Nginx 和 GUI 桌面環境等等。這些程序又可能會派生出它們自己的子進程。

這一部分並沒有什麼問題。但問題在於當一個進程終止時,會發生什麼?假設上圖中的 bash (5) 進程結束了,那麼其會轉變爲「廢棄進程」(defunct process),也被稱作爲“殭屍進程”(zombie process)。

爲什麼會這樣?因爲 Unix 這樣設計地目的,在於讓父進程能夠耐心“等待”子進程結束,從而獲得其結束狀態(exit status)。只有當父進程調用 waitpid() 之後,“殭屍進程”纔會真正結束。手冊裏是這樣描述地:

"一個已經終止但並未被“等待”的進程,就成爲了一個“殭屍”。內核會記錄這些“殭屍進程”的基本信息(PID、終止狀態、資源佔用信息),以確保其父進程在之後的時間裏可以通過“等待”來獲取這個子進程的信息。"

通常來說,人們會簡單地認爲“殭屍進程”就是那些會造成破壞的失控進程。但從 Unix 系統角度來分析,“殭屍進程”有着非常清晰地定義:進程已經終止,但尚未被其父進程“等待”。

絕大多數情況下,這都不會產生什麼問題。在一個子進程上調用 waitpid() 以消除其“殭屍”狀態,被稱爲“收割”。多數應用程序都能夠正確地“收割”其子進程。在上例中,操作系統會在 bash 進程終止時發送 SIGCHLD 信號以喚醒 sshd 進程,其在接收到信號後就“收割”掉了此子進程。

但還有一種特殊情況——如果父進程終止了,無論是正常的(程序邏輯正常終止),還是用戶操作導致的(比如用戶殺死了該進程)——子進程會如何處理?它們不再擁有父進程,變成了「孤兒進程」(orphaned)(這是確切的技術術語)。

此時初始進程(PID 1)就會因其被賦予地特殊任務而介入——「領養」(adopt)(同樣的,這是確切的技術術語)「孤兒進程」。這就意味着初始進程會成爲這些子進程的父進程,而無論其是否由初始進程創建。

以 Nginx 爲例,其默認就會作爲後臺程序運行。工作流程如下:Nginx 創建一個子進程後,自身進程結束,然後該子進程就被初始進程「領養」了。

其中的要點是什麼?操作系統內核自動處理了「領養」邏輯,因此內核其實是希望初始進程也自動完成對這些「孤兒進程」的“收割”邏輯

這在 Unix 操作系統中是一個非常重要的機制,大量的軟件都是因而設計和實現。幾乎所有的服務(daemon)程序都預期初始進程會「領養」和“收割”其守護子進程。

儘管我們是以服務程序做例子,但系統並沒有什麼機制對此進行規約。任何一個進程在結束時,都會預期初始進程能夠清理(「領養」和“收割”)其子進程。這一點,在《操作系統概述》和《Unix 系統高級編程》兩書中描述地非常詳細。

“殭屍進程”的危害

“殭屍進程”都已經終止了,它們危害在哪裏?它們原本佔用的內存已經釋放了嗎?在 ps 中除了多了些條目,還有什麼別的嗎?

是的,內存確實已經釋放,但能夠在 ps 中看到,說明它們還仍然佔用着一些內核資源。對 Linux waitpid 的文檔引用如下:

"在“殭屍進程”在被父進程“等待”以徹底消除之前,其仍然會被記錄在內核進程表中。而當該表被寫滿後,新的進程將無法被創建。"

對 Docker 的影響

這個問題會如何對 Docker 產生怎樣的影響?我們可以看到很多人只在他們的容器中跑一個進程,而且也認爲只需要跑這麼一個進程就足夠了。但顯而易見地,這些進程無法承擔初始進程在前文中所述的任務邏輯。因此,爲了能夠正確地“收割”被「領養」的進程,我們需要另外的初始進程來完成這些工作。

舉一個相對複雜地例子,我們的容器是一個 web 服務器,需要去跑一段基於 bash 的 CGI 腳本,而該腳本又會去調用 grep 程序。假定 web 服務器發現了 CGI 腳本執行超時,也中止了其繼續執行。但此時 grep 程序並不會受到影響仍然繼續執行,當其執行結束時,就變成了一個“殭屍進程”並由初始進程(即 web 服務器)「收養」。但 web 服務器無法正確地“收割”這個 grep 進程,所以該“殭屍進程”就在系統中常駐了。

這個問題同樣也存在於其它場景中。我們能看到人們嚐嚐爲第三方程序創建 Docker 容器——又如 PostgreSQL ——並將其作爲容器中的主進程運行。當我們運行別人的代碼時,我們如何確保這些程序*並不會*派生出子進程並因而堆積大量的“殭屍進程”?唯獨僅有我們運行着自己的代碼,同時還對所有的依賴包和依賴包的依賴包做嚴格地審查,才能杜絕這種問題。因此,通常來說,我們很有必要來執行一個合適的初始化系統(init system)來避免這些問題地發生。

解決方案

1. 重新編譯容器鏡像,像baseimage-docker一樣,往鏡像中引入一套輕量的初始化系統my_init,並將這個my_init程序作爲容器運行的初始進程。

2. 將原來的CMD ["/path-to-your-app"]修改爲CMD ["/bin/bash", "-c", "set -e && /path-to-your-app"] && true,這是一個不完善方案,因爲沒有乾淨地終止應用進程,可能會造成文件損壞,有風險。

| 容器的目錄被其它的進程使用

現象

在正常停止Docker容器後,刪除容器報錯:

1234

Error response from daemon: Driver devicemapper failed to remove root filesystem a5144c558eabbe647ee9a25072746935e03bb797f4dcaf44c275e0ea4ada463a: remove /var/lib/docker/devicemapper/mnt/25cb26493fd3c804d96e802a95d6c74d7cae68032bf50fc640f40ffe40cc4188: device or resource busyError response from daemon: Driver devicemapper failed to remove root filesystem bdd60d5104076351611efb4cdb34c50c9d3f2136fdaea74c9752e2df9fd6f40f: remove /var/lib/docker/devicemapper/mnt/d2b5b784495ece1c9365bdea78b95076f035426356e6654c65ee1db87d8c03e7: device or resource busyError response from daemon: Driver devicemapper failed to remove root filesystem 847b5bb74762a7356457cc331d948e5c47335bbd2e0d9d3847361c6f69e9c369: remove /var/lib/docker/devicemapper/mnt/71e7b20dca8fd9e163c3dfe90a3b31577ee202a03cd1bd5620786ebabdc4e52a: device or resource busyError response from daemon: Driver devicemapper failed to remove root filesystem a85e44dfa07c060244163e19a545c76fd25282f2474faa205d462712866aac51: remove /var/lib/docker/devicemapper/mnt/8bcd524cc8bfb1b36506bf100090c52d7fbbf48ea00b87a53d69f32e537737b7: device or resource busy

快速解決方案

12345

# 找到使用容器目錄的進程$ find /proc/*/mounts | xargs grep -E "526c823031c2065c6fb3b92f9aaded4477eccceb65f245391a1d8a6acae13d0e"/proc/27837/mounts:shm /var/lib/docker/containers/526c823031c2065c6fb3b92f9aaded4477eccceb65f245391a1d8a6acae13d0e/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0$ ps aux|grep 27837# 先停掉這些進程後,再就可以成功刪除容器了

問題根源

https://github.com/moby/moby/issues/27381

Core of the issue here is that container is either still running or some of its mount points have leaked into other some mount namespace. You docker-pid and host both seem to be sharing same mount namespace. And that means docker daemon is running in host mount namespace. And that probably means that nginx started at some point after container start and it seems to be running in its own mount namespace. And at that time mount points leaked into nginx mount namespace and that’s preventing deletion of container.

原來是老的內核存在bug,Docker進程共享宿主機的mount命名空間,這樣容器的掛載點被泄漏給其它進程的命名空間了。

解決方案

升級內核至3.10.0-693.5.2.el7.x86_64以後,另外安裝Docker倉庫裏最新的docker-ce

1234567891011121314151617

sudo yum remove docker \                  docker-client \                  docker-client-latest \                  docker-common \                  docker-latest \                  docker-latest-logrotate \                  docker-logrotate \                  docker-selinux \                  docker-engine-selinux \                  docker-enginesudo yum install -y yum-utils \  device-mapper-persistent-data \  lvm2sudo yum-config-manager \    --add-repo \    https://download.docker.com/linux/centos/docker-ce.reposudo yum install docker-ce

| 參考

1.https://gist.github.com/snakevil/0b47072fcb626b87f4bd4ab80f7d8946

2.https://www.lijiaocn.com/%E9%97%AE%E9%A2%98/2017/07/14/docker-unable-to-rm-filesystem.html

3.https://github.com/moby/moby/issues/27381

4.https://docs.docker.com/install/linux/docker-ce/centos/#install-using-the-repository

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章