靈魂一問:為什么你的 Docker 容器剛啟動就停了
很多docker初學(xué)者,在運行容器的時候,或者是寫第一個dockerfile的時候,問題最多的就是容器啟動后就停了,怎么看都覺得命令沒有問題,容器也沒有錯誤日志,dockerfile也就那么幾條……
其實你沒有錯,錯的是docker,它執(zhí)行的太快了
這話怎么說呢,我拿nginx官方的dockerfile給你解釋下。

上面是nginx官方的dockerfile文件,我把set部分刪掉了,其他沒啥,主要看下CMD
為什么這里不是systemctl nginx start,或者/etc/init.d/nginx start,再或者nginx直接啟動,而是用daemon off的方式啟動?
這是因為如果nginx用后臺模式運行,啟動的命令執(zhí)行完之后,這個啟動的命令就退出了,這個時候,容器也就跟著退出了
又為什么命令執(zhí)行完,容器就退出了?這個要從linux內(nèi)核說起。
在linux操作系統(tǒng)中,當(dāng)內(nèi)核初始化完畢之后,會啟動一個init進(jìn)程,這個進(jìn)程是整個操作系統(tǒng)的第一個用戶進(jìn)程,所以它的進(jìn)程ID為1,也就是我們常說的PID1進(jìn)程,然后所有的用戶態(tài)進(jìn)程,都是這個進(jìn)程的子進(jìn)程,所以,整個系統(tǒng)的用戶進(jìn)程,都是由init進(jìn)程作為根進(jìn)程的
要了解這個PID1進(jìn)程,要從以下幾個概念了解:
進(jìn)程表項
linux內(nèi)核程序通過進(jìn)程表對進(jìn)程進(jìn)行管理, 每個進(jìn)程在進(jìn)程表中占有一項,稱為進(jìn)程表項,它記錄了進(jìn)程的狀態(tài),打開的文件描述符等等一系統(tǒng)信息。當(dāng)一個進(jìn)程結(jié)束了運行或在半途中終止了運行,那么內(nèi)核就需要釋放該進(jìn)程所占用的系統(tǒng)資源。這包括進(jìn)程運行時打開的文件,申請的內(nèi)存等。
但是,這里要注意的是,進(jìn)程表項并沒有隨著進(jìn)程的退出而被清除,它會一直占用內(nèi)核的內(nèi)存。為什么會有這么奇怪的行為呢?這是因為在某些程序中,我們必須明確地知道進(jìn)程的退出狀態(tài)等信息,而這些信息的獲取是由父進(jìn)程調(diào)用wait/waitpid而獲取的。
設(shè)想這樣一種場景,如果子進(jìn)程在退出的時候直接清除文件表項的話,那么父進(jìn)程就很可能沒有地方獲取進(jìn)程的退出狀態(tài)了,因此操作系統(tǒng)就會將文件表項一直保留至wait/waitpid系統(tǒng)調(diào)用結(jié)束。
僵尸進(jìn)程
僵尸進(jìn)程指的是:進(jìn)程退出后,到其父進(jìn)程還未對其調(diào)用wait/waitpid之間的這段時間所處的狀態(tài)。一般來說,這種狀態(tài)持續(xù)的時間很短,所以我們一般很難在系統(tǒng)中捕捉到。但是,一些粗心的程序員可能會忘記調(diào)用wait/waitpid,或者由于某種原因未執(zhí)行該調(diào)用等等,那么這個時候就會出現(xiàn)長期駐留的僵尸進(jìn)程了。如果大量的產(chǎn)生僵尸進(jìn)程,其進(jìn)程號就會一直被占用,可能導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程。
然后還有我們經(jīng)常會見到的一種情況,就是父進(jìn)程先于子進(jìn)程結(jié)束,這種情況多見于手動kill某個父進(jìn)程的情況,這種情況就是下面要說到的
孤兒進(jìn)程
父進(jìn)程先于子進(jìn)程退出,那么子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號為1)接管,并由init進(jìn)程對它完成狀態(tài)收集(wait/waitpid)工作
PID1負(fù)責(zé)清理那些被拋棄的進(jìn)程所留下來的痕跡,有效的回收的系統(tǒng)資源,保證系統(tǒng)長時間穩(wěn)定的運行
了解了linux的PID1,接著來看下容器中的PID1進(jìn)程
熟悉docker都知道,docker容器并不是一個完整的linux的操作系統(tǒng),它也沒什么內(nèi)核初始化過程,更沒有像init(1)這樣的初始化過程。在docker容器中被標(biāo)志為PID1的進(jìn)程實際上就是一個普通的用戶進(jìn)程,我們還拿nginx官方的鏡像起的容器來看。
我用docker run -d nginx直接啟動的

可以看到,就是Dockerfile中指定的CMD那個進(jìn)程,注意:如果你啟動容器的時候,指定了命令,會覆蓋CMD,也就是CMD是條默認(rèn)啟動的命令參數(shù),如果啟動容器時指定了命令,會覆蓋,當(dāng)Dockerfile中有多條CMD時,執(zhí)行最后一條
這個進(jìn)程其實在宿主機(jī)上有一個普通的用戶進(jìn)程ID

之所以在容器中PID變成1,是因為linux內(nèi)核提供的PID namespaces功能,如果宿主機(jī)上所有用戶進(jìn)程構(gòu)成了一個完整的樹形結(jié)構(gòu),那么PID namespaces實際上就是將這個CMD或ENTRYPOINT進(jìn)程及其子進(jìn)程作為另外一個分支,很顯然這部分也是一個樹形結(jié)構(gòu)
當(dāng)我們在宿主機(jī)上kill掉這個進(jìn)程ID,那么整個容器便會處于退出狀態(tài)
這也就解釋了上面為什么命令執(zhí)行完之后,容器就退出了
認(rèn)真的小伙伴從上面圖中看到了,我上面說linux中PID1進(jìn)程為所有用戶進(jìn)程的父進(jìn)程,但是在容器里面,通過ps命令看到的進(jìn)程的父進(jìn)程都是“0”,這又是為什么呢?
前面提到,容器中的進(jìn)程樹實際上是宿主機(jī)進(jìn)程樹的一棵子樹,或者說分支,那么我們在宿主機(jī)上就可以找到這顆子樹的父進(jìn)程。

我們可以看到,這個docker容器中PID 0的進(jìn)程應(yīng)該就是這個containerd-shim
我們結(jié)合docker的結(jié)構(gòu)圖看一下

從架構(gòu)圖中,我們可以看到containerd-shim進(jìn)程下還有一個runC進(jìn)程,但是我們在上面過程中,并沒有發(fā)現(xiàn)runC這個進(jìn)程
runC是OCI標(biāo)準(zhǔn)的一個參考實現(xiàn),而OCI Open Container Initiative,是由多家公司共同成立的項目,并由linux基金會進(jìn)行管理,致力于container runtime的標(biāo)準(zhǔn)的制定和runc的開發(fā)等工作。runc,是對于OCI標(biāo)準(zhǔn)的一個參考實現(xiàn),是一個可以用于創(chuàng)建和運行容器的CLI(command-line interface)工具。
runc直接與容器所依賴的cgroup/linux kernel等進(jìn)行交互,負(fù)責(zé)為容器配置cgroup/namespace等啟動容器所需的環(huán)境,創(chuàng)建啟動容器的相關(guān)進(jìn)程
事實上,Docker容器的創(chuàng)建過程是這樣子的 docker-containerd-shim –> runC –> entrypoint,而我們看到的最終狀態(tài)是 docker-containerd-shim –> entrypoint,而runc進(jìn)程創(chuàng)建完容器之后,自己就先退出去了,所以我們上面的過程中一直沒有出現(xiàn)
看到這里你應(yīng)該了解,為什么你啟動容器或?qū)懞玫膁ockerfile,總是剛啟動就退出,而且沒有任何錯誤了吧!























