摘要: Docker在進程管理上有一些特殊之處,如果不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵“一個容器一個進程(one process per container)”的方式。這種方式非常適合以單進程為主的微服務架構的應用。然而由於一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以
Docker在進程管理上有一些特殊之處,如果不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵“一個容器一個進程(one process per container)”的方式。這種方式非常適合以單進程為主的微服務架構的應用。然而由於一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以拆分到不同的容器中,所以在單個容器內運行多個進程便成了一種折衷方案;此外在一些場景中,用戶期望利用Docker容器來作為輕量級的虛擬化方案,動態的安裝配置應用,這也需要在容器中運行多個進程。而在Docker容器中的正確運行多進程應用將給開發者帶來更多的挑戰。
今天我們會分析Docker中進程管理的一些細節,並介紹一些常見問題的解決方法和注意事項。
容器的PID namespace(名空間)
在Docker中,進程管理的基礎就是Linux內核中的PID名空間技術。在不同PID名空間中,進程ID是獨立的;即在兩個不同名空間下的進程可以有相同的PID。
Linux內核為所有的PID名空間維護了一個樹狀結構:最頂層的是系統初始化時創建的root namespace(根名空間),再創建的新PID namespace就稱之為child namespace(子名空間),而原先的PID名空間就是新創建的PID名空間的parent namespace(父名空間)。通過這種方式,系統中的PID名空間會形成一個層級體系。父節點可以看到子節點中的進程,並可以通過信號等方式對子節點中的進程產生影響。反過來,子節點不能看到父節點名空間中的任何內容,也不可能通過kill或ptrace影響父節點或其他名空間中的進程。
在Docker中,每個Container都是Docker Daemon的子進程,每個Container進程缺省都具有不同的PID名空間。通過名空間技術,Docker實現容器間的進程隔離。另外Docker Daemon也會利用PID名空間的樹狀結構,實現了對容器中的進程交互、監控和回收。注:Docker還利用了其他名空間(UTS,IPC,USER)等實現了各種系統資源的隔離,由於這些內容和進程管理關聯不多,本文不會涉及。
當創建一個Docker容器的時候,就會新建一個PID名空間。容器啟動進程在該名空間內PID為1。當PID1進程結束之后,Docker會銷毀對應的PID名空間,並向容器內所有其它的子進程發送SIGKILL。
下面我們來做一些試驗,下面我們會利用官方的Redis鏡像創建兩個容器,並觀察里面的進程。
如果你在Windows或Mac上利用"docker-machine",請利用docker-machine ssh default
進入Boot2docker虛擬機
創建名為"redis"的容器,並在容器內部和宿主機中查看容器中的進程信息
docker@default:~$ docker run -d --name redis redis f6bc57cc1b464b05b07b567211cb693ee2a682546ed86c611b5d866f6acc531c docker@default:~$ docker exec redis ps -ef UID PID PPID C STIME TTY TIME CMD redis 1 0 0 01:49 ? 00:00:00 redis-server *:6379 root 11 0 0 01:49 ? 00:00:00 ps -ef docker@default:~$ docker top redis UID PID PPID C STIME TTY TIME CMD 999 9302 1264 0 01:49 ? 00:00:00 redis-server *:6379
創建名為"redis2"的容器,並在容器內部和宿主機中查看容器中的進程信息
docker@default:~$ docker run -d --name redis2 redis 356eca186321ab6ef4c4337aa0c7de2af1e01430587d6b0e1add2e028ed05f60 docker@default:~$ docker exec redis2 ps -ef UID PID PPID C STIME TTY TIME CMD redis 1 0 0 01:50 ? 00:00:00 redis-server *:6379 root 10 0 4 01:50 ? 00:00:00 ps -ef docker@default:~$ docker top redis2 UID PID PPID C STIME TTY TIME CMD 999 9342 1264 0 01:50 ? 00:00:00 redis-server *:6379
我們可以使用docker exec
命令進入容器PID名空間,並執行應用。通過ps -ef
命令,可以看到每個Redis容器都包含一個PID為1的進程,"redis-server",它是容器的啟動進程,具有特殊意義。
利用docker top
命令,可以讓我們從宿主機操作系統中看到容器的進程信息。在兩個容器中的"redis-server"是兩個獨立的進程,但是他們擁有相同的父進程 Docker Daemon。所以Docker可以父子進程的方式在Docker Daemon和Redis容器之間進行交互。
另一個值得注意的方面是,docker exec
命令可以進入指定的容器內部執行命令。由它啟動的進程屬於容器的namespace和相應的cgroup。但是這些進程的父進程是Docker Daemon而非容器的PID1進程。
我們下面會在Redis容器中,利用docker exec
命令啟動一個"sleep"進程
docker@default:~$ docker exec -d redis sleep 2000
docker@default:~$ docker exec redis ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 02:26 ? 00:00:00 redis-server *:6379 root 11 0 0 02:26 ? 00:00:00 sleep 2000 root 21 0 0 02:29 ? 00:00:00 ps -ef docker@default:~$ docker top redis UID PID PPID C STIME TTY TIME CMD 999 9955 1264 0 02:12 ? 00:00:00 redis-server *:6379 root 9984 1264 0 02:13 ? 00:00:00 sleep 2000
我們可以清楚的看到exec命令創建的sleep進程屬Redis容器的名空間,但是它的父進程是Docker Daemon。
如果我們在宿主機操作系統中手動殺掉容器的啟動進程(在上文示例中是redis-server),容器會自動結束,而容器名空間中所有進程也會退出。
docker@default:~$ PID=$(docker inspect --format="{{.State.Pid}}" redis) docker@default:~$ sudo kill $PID docker@default:~$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 356eca186321 redis "/entrypoint.sh redis" 23 minutes ago Up 4 minutes 6379/tcp redis2 f6bc57cc1b46 redis "/entrypoint.sh redis" 23 minutes ago Exited (0) 4 seconds ago redis
通過以上示例:
- 每個容器有獨立的PID名空間,
- 容器的生命周期和其PID1進程一致
- 利用
docker exec
可以進入到容器的名空間中啟動進程
此外,自從Docker 1.5之后,docker run
命令引入了--pid=host
參數來支持使用宿主機PID名空間來啟動容器進程,這樣可以方便的實現容器內應用和宿主機應用之間的交互:比如利用容器中的工具監控和調試宿主機進程。
如何指明容器PID1進程
在Docker容器中的初始化進程(PID1進程)在容器進程管理上具有特殊意義。它可以被Dockerfile中的ENTRYPOINT
或CMD
指令所指明;也可以被docker run
命令的啟動參數所覆蓋。了解這些細節可以幫助我們更好地了解PID1的進程的行為。
關於ENTRYPOINT和CMD指令的不同,我們可以參見官方的Dockerfile說明和最佳實踐
- https://docs.docker.com/engine/reference/builder/#entrypoint
- https://docs.docker.com/engine/reference/builder/#cmd
值得注意的一點是:在ENTRYPOINT和CMD指令中,提供兩種不同的進程執行方式 shell 和 exec
在 shell 方式中,CMD/ENTRYPOINT指令以如下方式定義
CMD executable param1 param2
這種方式中的PID1進程是以/bin/sh -c ”executable param1 param2”
方式啟動的
而在 exec 方式中,CMD/ENTRYPOINT指令以如下方式定義
CMD ["executable","param1","param2"]
注意這里的可執行命令和參數是利用JSON字符串數組的格式定義的,這樣PID1進程會以 executable param1 param2
方式啟動的。另外,在docker run
命令中指明的命令行參數也是以 exec 方式啟動的。
為了解釋兩種不同運行方式的區別,我們利用不同的Dockerfile分別創建兩個Redis鏡像
"Dockerfile_shell"文件內容如下,會利用shell方式啟動redis服務
FROM ubuntu:14.04 RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/* EXPOSE 6379 CMD "/usr/bin/redis-server"
"Dockerfile_exec"文件內容如下,會利用exec方式啟動redis服務
FROM ubuntu:14.04 RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/* EXPOSE 6379 CMD ["/usr/bin/redis-server"]
然后基於它們構建兩個鏡像"myredis:shell"和"myredis:exec"
docker build -t myredis:shell -f Dockerfile_shell . docker build -t myredis:exec -f Dockerfile_exec .
運行"myredis:shell"鏡像,我們可以發現它的啟動進程(PID1)是/bin/sh -c "/usr/bin/redis-server"
,並且它創建了一個子進程/usr/bin/redis-server *:6379
。
docker@default:~$ docker run -d --name myredis myredis:shell 49f7fc37f4b7cf1ed7f5296537a93b2ad23b1b6686a05e5c7e40e9a2b2d3665e docker@default:~$ docker exec myredis ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 08:12 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server" root 5 1 0 08:12 ? 00:00:00 /usr/bin/redis-server *:6379 root 8 0 0 08:12 ? 00:00:00 ps -ef
下面運行"myredis:exec"鏡像,我們可以發現它的啟動進程是/usr/bin/redis-server *:6379
,並沒有其他子進程存在。
docker@default:~$ docker run -d --name myredis2 myredis:exec d1df0e4f4e3bbe36fca94f08df9ad3306fa1dee86415c853ddc5593fb9fa5673 docker@default:~$ docker exec myredis2 ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 08:13 ? 00:00:00 /usr/bin/redis-server *:6379 root 8 0 0 08:13 ? 00:00:00 ps -ef
由此我們可以清楚的看到,以exec和shell方式執行命令可能會導致容器的PID1進程不同。然而這又有什么問題呢?
原因在於:PID1進程對於操作系統而言具有特殊意義。操作系統的PID1進程是init進程,以守護進程方式運行,是所有其他進程的祖先,具有完整的進程生命周期管理能力。在Docker容器中,PID1進程是啟動進程,它也會負責容器內部進程管理的工作。而這也將導致進程管理在Docker容器內部和完整操作系統上的不同。
進程信號處理
信號是Unix/Linux中進程間異步通信機制。Docker提供了兩個命令docker stop
和docker kill
來向容器中的PID1進程發送信號。
當執行docker stop
命令時,docker會首先向容器的PID1進程發送一個SIGTERM信號,用於容器內程序的退出。如果容器在收到SIGTERM后沒有結束, 那么Docker Daemon會在等待一段時間(默認是10s)后,再向容器發送SIGKILL信號,將容器殺死變為退出狀態。這種方式給Docker應用提供了一個優雅的退出(graceful stop)機制,允許應用在收到stop命令時清理和釋放使用中的資源。而docker kill
可以向容器內PID1進程發送任何信號,缺省是發送SIGKILL信號來強制退出應用。
注:從Docker 1.9開始,Docker支持停止容器時向其發送自定義信號,開發者可以在Dockerfile使用STOPSIGNAL
指令,或docker run
命令中使用--stop-signal
參數中指明。缺省是SIGTERM
我們來看看不同的PID1進程,對進程信號處理的不同之處。首先,我們使用docker stop
命令停止由 exec 模式啟動的“myredis2”容器,並檢查其日志
docker@default:~$ docker stop myredis2 myredis2 docker@default:~$ docker logs myredis2 [1] 11 Feb 08:13:01.631 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in stand alone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 1 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' [1] 11 Feb 08:13:01.632 # Server started, Redis version 2.8.4 [1] 11 Feb 08:13:01.633 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [1] 11 Feb 08:13:01.633 * The server is now ready to accept connections on port 6379 [1 | signal handler] (1455179074) Received SIGTERM, scheduling shutdown... [1] 11 Feb 08:24:34.259 # User requested shutdown... [1] 11 Feb 08:24:34.259 * Saving the final RDB snapshot before exiting. [1] 11 Feb 08:24:34.262 * DB saved on disk [1] 11 Feb 08:24:34.262 # Redis is now ready to exit, bye bye... docker@default:~$
我們發現對“myredis2”容器的stop命令幾乎立刻生效;而且在容器日志中,我們看到了“Received SIGTERM, scheduling shutdown...”的內容,說明“redis-server”進程接收到了SIGTERM消息,並優雅地退出。
我們再對利用 shell 模式啟動的“myredis”容器發出停止操作,並檢查其日志
docker@default:~$ docker stop myredis myredis docker@default:~$ docker logs myredis [5] 11 Feb 08:12:40.108 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in stand alone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 5 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' [5] 11 Feb 08:12:40.109 # Server started, Redis version 2.8.4 [5] 11 Feb 08:12:40.109 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [5] 11 Feb 08:12:40.109 * The server is now ready to accept connections on port 6379 docker@default:~$
我們發現對”myredis”容器的stop命令暫停了一會兒才結束,而且在日志中我們沒有看到任何收到SIGTERM信號的內容。原因其PID1進程sh沒有對SIGTERM信號的處理邏輯,所以它忽略了所接收到的SIGTERM信號。當Docker等待stop命令執行10秒鍾超時之后,Docker Daemon發送SIGKILL強制殺死sh進程,並銷毀了它的PID名空間,其子進程redis-server也在收到SIGKILL信號后被強制終止。如果此時應用還有正在執行的事務或未持久化的數據,強制進程退出可能導致數據丟失或狀態不一致。
通過這個示例我們可以清楚的理解PID1進程在信號管理的重要作用。所以,
- 容器的PID1進程需要能夠正確的處理SIGTERM信號來支持優雅退出。
- 如果容器中包含多個進程,需要PID1進程能夠正確的傳播SIGTERM信號來結束所有的子進程之后再退出。
- 確保PID1進程是期望的進程。缺省sh/bash進程沒有提供SIGTERM的處理,需要通過shell腳本來設置正確的PID1進程,或捕獲SIGTERM信號。
另外需要注意的是:由於PID1進程的特殊性,Linux內核為他做了特殊處理。如果它沒有提供某個信號的處理邏輯,那么與其在同一個PID名空間下的進程發送給它的該信號都會被屏蔽。這個功能的主要作用是防止init進程被誤殺。我們可以驗證在容器內部發出的SIGKILL信號無法殺死PID1進程
docker@default:~$ docker start myredis myredis docker@default:~$ docker exec myredis kill -9 1 docker@default:~$ docker top myredis UID PID PPID C STIME TTY TIME CMD root 3586 1290 0 08:45 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server" root 3591 3586 0 08:45 ? 00:00:00 /usr/bin/redis-server *:6379
孤兒進程與僵屍進程管理
熟悉Unix/Linux進程管理的同學對多進程應用並不陌生。
當一個子進程終止后,它首先會變成一個“失效(defunct)”的進程,也稱為“僵屍(zombie)”進程,等待父進程或系統收回(reap)。在Linux內核中維護了關於“僵屍”進程的一組信息(PID,終止狀態,資源使用信息),從而允許父進程能夠獲取有關子進程的信息。如果不能正確回收“僵屍”進程,那么他們的進程描述符仍然保存在系統中,系統資源會緩慢泄露。
大多數設計良好的多進程應用可以正確的收回僵屍子進程,比如NGINX master進程可以收回已終止的worker子進程。如果需要自己實現,則可利用如下方法:
1. 利用操作系統的waitpid()函數等待子進程結束並請除它的僵死進程,
2. 由於當子進程成為“defunct”進程時,父進程會收到一個SIGCHLD信號,所以我們可以在父進程中指定信號處理的函數來忽略SIGCHLD信號,或者自定義收回處理邏輯。
下面這些文章詳細介紹了對僵屍進程的處理方法
- http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html
- http://lbolla.info/blog/2014/01/23/die-zombie-die
如果父進程已經結束了,那些依然在運行中的子進程會成為“孤兒(orphaned)”進程。在Linux中Init進程(PID1)作為所有進程的父進程,會維護進程樹的狀態,一旦有某個子進程成為了“孤兒”進程后,init就會負責接管這個子進程。當一個子進程成為“僵屍”進程之后,如果其父進程已經結束,init會收割這些“僵屍”,釋放PID資源。
然而由於Docker容器的PID1進程是容器啟動進程,它們會如何處理那些“孤兒”進程和“僵屍”進程?
下面我們做幾個試驗來驗證不同的PID1進程對僵屍進程不同的處理能力
首先在myredis2容器中啟動一個bash進程,並創建子進程“sleep 1000”
docker@default:~$ docker restart myredis2 myredis2 docker@default:~$ docker exec -ti myredis2 bash root@d1df0e4f4e3b:/# sleep 1000
在另一個終端窗口,查看當前進程,我們可以發現一個sleep進程是bash進程的子進程。
docker@default:~$ docker exec myredis2 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 12:21 ? 00:00:00 /usr/bin/redis-server *:6379 root 8 0 0 12:21 ? 00:00:00 bash root 21 8 0 12:21 ? 00:00:00 sleep 1000 root 22 0 3 12:21 ? 00:00:00 ps -ef
我們殺死bash進程之后查看進程列表,這時候bash進程已經被殺死。這時候sleep進程(PID為21),雖然已經結束,而且被PID1進程(redis-server)接管,但是其沒有被父進程回收,成為僵屍狀態。
docker@default:~$ docker exec myredis2 kill -9 8 docker@default:~$ docker exec myredis2 ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 12:09 ? 00:00:00 /usr/bin/redis-server *:6379 root 21 1 0 12:10 ? 00:00:00 [sleep] <defunct> root 32 0 0 12:10 ? 00:00:00 ps -ef docker@default:~$
這是因為PID1進程“redis-server”沒有考慮過作為init對僵屍子進程的回收的場景。
我們來做另一個試驗,在用/bin/sh作為PID1進程的myredis容器中,再啟動一個bash進程,並創建子進程“sleep 1000”
docker@default:~$ docker start myredis myredis docker@default:~$ docker exec -ti myredis bash root@49f7fc37f4b7:/# sleep 1000
查看容器中進程情況,
docker@default:~$ docker exec myredis ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:29 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server" root 5 1 0 01:29 ? 00:00:00 /usr/bin/redis-server *:6379 root 8 0 0 01:30 ? 00:00:00 bash root 22 8 0 01:30 ? 00:00:00 sleep 1000 root 36 0 0 01:30 ? 00:00:00 ps -ef
我們殺死bash進程之后查看進程列表,發現“bash”和“sleep 1000”進程都已經被殺死和回收
docker@default:~$ docker exec myredis kill -9 8 docker@default:~$ docker exec myredis ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 01:29 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server" root 5 1 0 01:29 ? 00:00:00 /usr/bin/redis-server *:6379 root 45 0 0 01:31 ? 00:00:00 ps -ef docker@default:~$
這是因為sh/bash等應用可以自動清理僵屍進程。
關於僵屍進程在Docker中init處理所需注意細節的詳細描述,可以在如下文章得到
簡單而言,如果在容器中運行多個進程,PID1進程需要有能力接管“孤兒”進程並回收“僵屍”進程。我們可以
1. 利用自定義的init進程來進行進程管理,比如 S6 , phusion myinit,dumb-init, tini 等
2. Bash/sh等缺省提供了進程管理能力,如果需要可以作為PID1進程來實現正確的進程回收。
進程監控
在Docker中,如果docker run
命令中指明了restart policy,Docker Daemon會監控PID1進程,並根據策略自動重啟已結束的容器。
restart 策略 | 結果 |
---|---|
no | 不自動重啟,缺省值 |
on-failure[:max-retries] | 當PID1進程退出值非0時,自動重啟容器;可以指定最大重試次數 |
always | 永遠自動重啟容器;當Docker Daemon啟動時,會自動啟動容器 |
unless-stopped | 永遠自動重啟容器;當Docker Daemon啟動時,如果之前容器不為stoped狀態就自動啟動容器 |
注意:為防止頻繁重啟故障應用導致系統過載,Docker會在每次重啟過程中會延遲一段時間。Docker重啟進程的延遲時間從100ms開始並每次加倍,如100ms,200ms,400ms等等。
利用Docker內置的restart策略可以大大簡化應用進程監控的負擔。但是Docker Daemon只是監控PID1進程,如果容器在內包含多個進程,仍然需要開發人員來處理進程監控。
大家一定非常熟悉Supervisor,Monit等進程監控工具,他們可以方便的在容器內部中實現進程監控。Docker提供了相應的文檔來介紹,互聯網上也有很多資料,我們今天就不再贅述了。
另外利用Supervisor等工具作為PID1進程是在容器中支持多進程管理的主要實現方式;和簡單利用shell腳本fork子進程相比,采用Supervisor等工具有很多好處:
- 一些傳統的服務不能以PID1進程的方式執行,利用Supervisor可以方便的適配
- Supervisor這些監控工具大多提供了對SIGTERM的信號傳播支持,可以支持子進程優雅的退出
然而值得注意的是:Supervisor這些監控工具大多沒有完全提供Init支持的進程管理能力,如果需要支持子進程回收的場景需要配合正確的PID1進程來完成
總結
進程管理在Docker容器中和在完整的操作系統有一些不同之處。在每個容器的PID1進程,需要能夠正確的處理SIGTERM信號來支持容器應用的優雅退出,同時要能正確的處理孤兒進程和僵屍進程。
在Dockerfile中要注意shell模式和exec模式的不同。通常而言我們鼓勵使用exec模式,這樣可以避免由無意中選擇錯誤PID1進程所引入的問題。
在Docker中“一個容器一個進程的方式”並非絕對化的要求,然而在一個容器中實現對於多個進程的管理必須考慮更多的細節,比如子進程管理,進程監控等等。所以對於常見的需求,比如日志收集,性能監控,調試程序,我們依然建議采用多個容器組裝的方式來實現。
[在此處輸入文章標題]
摘要: Docker在進程管理上有一些特殊之處,如果不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵“一個容器一個進程(one process per container)”的方式。這種方式非常適合以單進程為主的微服務架構的應用。然而由於一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以
Docker在進程管理上有一些特殊之處,如果不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵“一個容器一個進程(one process per container)”的方式。這種方式非常適合以單進程為主的微服務架構的應用。然而由於一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以拆分到不同的容器中,所以在單個容器內運行多個進程便成了一種折衷方案;此外在一些場景中,用戶期望利用Docker容器來作為輕量級的虛擬化方案,動態的安裝配置應用,這也需要在容器中運行多個進程。而在Docker容器中的正確運行多進程應用將給開發者帶來更多的挑戰。
今天我們會分析Docker中進程管理的一些細節,並介紹一些常見問題的解決方法和注意事項。
容器的PID namespace(名空間)
在Docker中,進程管理的基礎就是Linux內核中的PID名空間技術。在不同PID名空間中,進程ID是獨立的;即在兩個不同名空間下的進程可以有相同的PID。
Linux內核為所有的PID名空間維護了一個樹狀結構:最頂層的是系統初始化時創建的root namespace(根名空間),再創建的新PID namespace就稱之為child namespace(子名空間),而原先的PID名空間就是新創建的PID名空間的parent namespace(父名空間)。通過這種方式,系統中的PID名空間會形成一個層級體系。父節點可以看到子節點中的進程,並可以通過信號等方式對子節點中的進程產生影響。反過來,子節點不能看到父節點名空間中的任何內容,也不可能通過kill或ptrace影響父節點或其他名空間中的進程。
在Docker中,每個Container都是Docker Daemon的子進程,每個Container進程缺省都具有不同的PID名空間。通過名空間技術,Docker實現容器間的進程隔離。另外Docker Daemon也會利用PID名空間的樹狀結構,實現了對容器中的進程交互、監控和回收。注:Docker還利用了其他名空間(UTS,IPC,USER)等實現了各種系統資源的隔離,由於這些內容和進程管理關聯不多,本文不會涉及。
當創建一個Docker容器的時候,就會新建一個PID名空間。容器啟動進程在該名空間內PID為1。當PID1進程結束之后,Docker會銷毀對應的PID名空間,並向容器內所有其它的子進程發送SIGKILL。
下面我們來做一些試驗,下面我們會利用官方的Redis鏡像創建兩個容器,並觀察里面的進程。
如果你在Windows或Mac上利用"docker-machine",請利用docker-machine ssh default進入Boot2docker虛擬機
創建名為"redis"的容器,並在容器內部和宿主機中查看容器中的進程信息
docker@default:~$ docker run -d --name redis redis
f6bc57cc1b464b05b07b567211cb693ee2a682546ed86c611b5d866f6acc531c
docker@default:~$ docker exec redis ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 01:49 ? 00:00:00 redis-server *:6379
root 11 0 001:49 ? 00:00:00 ps -ef
docker@default:~$ docker top redis
UID PID PPID C STIME TTY TIME CMD
999 9302 1264 0 01:49 ? 00:00:00 redis-server *:6379
創建名為"redis2"的容器,並在容器內部和宿主機中查看容器中的進程信息
docker@default:~$ docker run -d --name redis2 redis
356eca186321ab6ef4c4337aa0c7de2af1e01430587d6b0e1add2e028ed05f60
docker@default:~$ docker exec redis2 ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 01:50 ? 00:00:00 redis-server *:6379
root 10 0 401:50 ? 00:00:00 ps -ef
docker@default:~$ docker top redis2
UID PID PPID C STIME TTY TIME CMD
999 9342 1264 0 01:50 ? 00:00:00 redis-server *:6379
我們可以使用docker exec命令進入容器PID名空間,並執行應用。通過ps -ef命令,可以看到每個Redis容器都包含一個PID為1的進程,"redis-server",它是容器的啟動進程,具有特殊意義。
利用docker top命令,可以讓我們從宿主機操作系統中看到容器的進程信息。在兩個容器中的"redis-server"是兩個獨立的進程,但是他們擁有相同的父進程 Docker Daemon。所以Docker可以父子進程的方式在Docker Daemon和Redis容器之間進行交互。
另一個值得注意的方面是,docker exec命令可以進入指定的容器內部執行命令。由它啟動的進程屬於容器的namespace和相應的cgroup。但是這些進程的父進程是Docker Daemon而非容器的PID1進程。
我們下面會在Redis容器中,利用docker exec命令啟動一個"sleep"進程
docker@default:~$ docker exec -d redis sleep 2000
docker@default:~$ docker exec redis ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 02:26 ? 00:00:00 redis-server *:6379
root 11 0 0 02:26 ? 00:00:00 sleep 2000
root 21 0 0 02:29 ? 00:00:00 ps -ef
docker@default:~$ docker top redis
UID PID PPID C STIME TTY TIME CMD
999 9955 1264 0 02:12 ? 00:00:00 redis-server *:6379
root 9984 1264 0 02:13 ? 00:00:00 sleep 2000
我們可以清楚的看到exec命令創建的sleep進程屬Redis容器的名空間,但是它的父進程是Docker Daemon。
如果我們在宿主機操作系統中手動殺掉容器的啟動進程(在上文示例中是redis-server),容器會自動結束,而容器名空間中所有進程也會退出。
docker@default:~$ PID=$(docker inspect --format="{{.State.Pid}}" redis)
docker@default:~$ sudo kill $PID
docker@default:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
356eca186321 redis "/entrypoint.sh redis" 23 minutes ago Up 4 minutes 6379/tcp redis2
f6bc57cc1b46 redis "/entrypoint.sh redis" 23 minutes ago Exited (0) 4 seconds ago redis
通過以上示例:
· 每個容器有獨立的PID名空間,
· 容器的生命周期和其PID1進程一致
· 利用docker exec可以進入到容器的名空間中啟動進程
此外,自從Docker 1.5之后,docker run命令引入了--pid=host參數來支持使用宿主機PID名空間來啟動容器進程,這樣可以方便的實現容器內應用和宿主機應用之間的交互:比如利用容器中的工具監控和調試宿主機進程。
如何指明容器PID1進程
在Docker容器中的初始化進程(PID1進程)在容器進程管理上具有特殊意義。它可以被Dockerfile中的ENTRYPOINT或CMD指令所指明;也可以被docker run命令的啟動參數所覆蓋。了解這些細節可以幫助我們更好地了解PID1的進程的行為。
關於ENTRYPOINT和CMD指令的不同,我們可以參見官方的Dockerfile說明和最佳實踐
· https://docs.docker.com/engine/reference/builder/#entrypoint
· https://docs.docker.com/engine/reference/builder/#cmd
值得注意的一點是:在ENTRYPOINT和CMD指令中,提供兩種不同的進程執行方式 shell 和 exec
在 shell 方式中,CMD/ENTRYPOINT指令以如下方式定義
CMD executable param1 param2
這種方式中的PID1進程是以/bin/sh -c ”executable param1 param2”方式啟動的
而在 exec 方式中,CMD/ENTRYPOINT指令以如下方式定義
CMD ["executable","param1","param2"]
注意這里的可執行命令和參數是利用JSON字符串數組的格式定義的,這樣PID1進程會以 executable param1 param2 方式啟動的。另外,在docker run命令中指明的命令行參數也是以 exec 方式啟動的。
為了解釋兩種不同運行方式的區別,我們利用不同的Dockerfile分別創建兩個Redis鏡像
"Dockerfile_shell"文件內容如下,會利用shell方式啟動redis服務
FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD "/usr/bin/redis-server"
"Dockerfile_exec"文件內容如下,會利用exec方式啟動redis服務
FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD ["/usr/bin/redis-server"]
然后基於它們構建兩個鏡像"myredis:shell"和"myredis:exec"
docker build -t myredis:shell -f Dockerfile_shell .
docker build -t myredis:exec -f Dockerfile_exec .
運行"myredis:shell"鏡像,我們可以發現它的啟動進程(PID1)是/bin/sh -c "/usr/bin/redis-server",並且它創建了一個子進程/usr/bin/redis-server *:6379。
docker@default:~$ docker run -d --name myredis myredis:shell
49f7fc37f4b7cf1ed7f5296537a93b2ad23b1b6686a05e5c7e40e9a2b2d3665e
docker@default:~$ docker exec myredis ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 008:12 ? 00:00:00/bin/sh -c "/usr/bin/redis-server"
root 5 1 008:12 ? 00:00:00/usr/bin/redis-server *:6379
root 8 0 008:12 ? 00:00:00 ps -ef
下面運行"myredis:exec"鏡像,我們可以發現它的啟動進程是/usr/bin/redis-server *:6379,並沒有其他子進程存在。
docker@default:~$ docker run -d --name myredis2 myredis:exec
d1df0e4f4e3bbe36fca94f08df9ad3306fa1dee86415c853ddc5593fb9fa5673
docker@default:~$ docker exec myredis2 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:13 ? 00:00:00 /usr/bin/redis-server *:6379
root 8 0 008:13 ? 00:00:00 ps -ef
由此我們可以清楚的看到,以exec和shell方式執行命令可能會導致容器的PID1進程不同。然而這又有什么問題呢?
原因在於:PID1進程對於操作系統而言具有特殊意義。操作系統的PID1進程是init進程,以守護進程方式運行,是所有其他進程的祖先,具有完整的進程生命周期管理能力。在Docker容器中,PID1進程是啟動進程,它也會負責容器內部進程管理的工作。而這也將導致進程管理在Docker容器內部和完整操作系統上的不同。
進程信號處理
信號是Unix/Linux中進程間異步通信機制。Docker提供了兩個命令docker stop和docker kill來向容器中的PID1進程發送信號。
當執行docker stop命令時,docker會首先向容器的PID1進程發送一個SIGTERM信號,用於容器內程序的退出。如果容器在收到SIGTERM后沒有結束,那么Docker Daemon會在等待一段時間(默認是10s)后,再向容器發送SIGKILL信號,將容器殺死變為退出狀態。這種方式給Docker應用提供了一個優雅的退出(graceful stop)機制,允許應用在收到stop命令時清理和釋放使用中的資源。而docker kill可以向容器內PID1進程發送任何信號,缺省是發送SIGKILL信號來強制退出應用。
注:從Docker 1.9開始,Docker支持停止容器時向其發送自定義信號,開發者可以在Dockerfile使用STOPSIGNAL指令,或docker run命令中使用--stop-signal參數中指明。缺省是SIGTERM
我們來看看不同的PID1進程,對進程信號處理的不同之處。首先,我們使用docker stop命令停止由 exec 模式啟動的“myredis2”容器,並檢查其日志
docker@default:~$ docker stop myredis2
myredis2
docker@default:~$ docker logs myredis2
[1] 11 Feb 08:13:01.631 # Warning: no config file specified, using the default config. Inorderto specify a config fileuse /usr/bin/redis-server /path/to/redis.conf
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in stand alone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 1
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[1] 11 Feb 08:13:01.632 # Server started, Redis version2.8.4
[1] 11 Feb 08:13:01.633 # WARNING overcommit_memory issetto0! Background save may fail underlowmemory condition. To fix this issue add'vm.overcommit_memory = 1'to /etc/sysctl.conf andthen reboot or run the command 'sysctl vm.overcommit_memory=1'for this to take effect.
[1] 11 Feb 08:13:01.633 * The serverisnow ready toaccept connections on port 6379
[1 | signal handler] (1455179074) Received SIGTERM, scheduling shutdown...
[1] 11 Feb 08:24:34.259 # User requested shutdown...
[1] 11 Feb 08:24:34.259 * Saving the final RDB snapshotbefore exiting.
[1] 11 Feb 08:24:34.262 * DB saved on disk
[1] 11 Feb 08:24:34.262 # Redis isnow ready toexit, bye bye...
docker@default:~$
我們發現對“myredis2”容器的stop命令幾乎立刻生效;而且在容器日志中,我們看到了“Received SIGTERM, scheduling shutdown...”的內容,說明“redis-server”進程接收到了SIGTERM消息,並優雅地退出。
我們再對利用 shell 模式啟動的“myredis”容器發出停止操作,並檢查其日志
docker@default:~$ docker stop myredis
myredis
docker@default:~$ docker logs myredis
[5] 11 Feb 08:12:40.108 # Warning: no config file specified, using the default config. Inorderto specify a config fileuse /usr/bin/redis-server /path/to/redis.conf
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in stand alone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 5
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[5] 11 Feb 08:12:40.109 # Server started, Redis version2.8.4
[5] 11 Feb 08:12:40.109 # WARNING overcommit_memory issetto0! Background save may fail underlowmemory condition. To fix this issue add'vm.overcommit_memory = 1'to /etc/sysctl.conf andthen reboot or run the command 'sysctl vm.overcommit_memory=1'for this to take effect.
[5] 11 Feb 08:12:40.109 * The serverisnow ready toaccept connections on port 6379
docker@default:~$
我們發現對”myredis”容器的stop命令暫停了一會兒才結束,而且在日志中我們沒有看到任何收到SIGTERM信號的內容。原因其PID1進程sh沒有對SIGTERM信號的處理邏輯,所以它忽略了所接收到的SIGTERM信號。當Docker等待stop命令執行10秒鍾超時之后,Docker Daemon發送SIGKILL強制殺死sh進程,並銷毀了它的PID名空間,其子進程redis-server也在收到SIGKILL信號后被強制終止。如果此時應用還有正在執行的事務或未持久化的數據,強制進程退出可能導致數據丟失或狀態不一致。
通過這個示例我們可以清楚的理解PID1進程在信號管理的重要作用。所以,
· 容器的PID1進程需要能夠正確的處理SIGTERM信號來支持優雅退出。
· 如果容器中包含多個進程,需要PID1進程能夠正確的傳播SIGTERM信號來結束所有的子進程之后再退出。
· 確保PID1進程是期望的進程。缺省sh/bash進程沒有提供SIGTERM的處理,需要通過shell腳本來設置正確的PID1進程,或捕獲SIGTERM信號。
另外需要注意的是:由於PID1進程的特殊性,Linux內核為他做了特殊處理。如果它沒有提供某個信號的處理邏輯,那么與其在同一個PID名空間下的進程發送給它的該信號都會被屏蔽。這個功能的主要作用是防止init進程被誤殺。我們可以驗證在容器內部發出的SIGKILL信號無法殺死PID1進程
docker@default:~$ docker start myredis
myredis
docker@default:~$ docker exec myredis kill -91
docker@default:~$ docker top myredis
UID PID PPID C STIME TTY TIME CMD
root 3586 1290 0 08:45 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 3591 3586 0 08:45 ? 00:00:00 /usr/bin/redis-server *:6379
孤兒進程與僵屍進程管理
熟悉Unix/Linux進程管理的同學對多進程應用並不陌生。
當一個子進程終止后,它首先會變成一個“失效(defunct)”的進程,也稱為“僵屍(zombie)”進程,等待父進程或系統收回(reap)。在Linux內核中維護了關於“僵屍”進程的一組信息(PID,終止狀態,資源使用信息),從而允許父進程能夠獲取有關子進程的信息。如果不能正確回收“僵屍”進程,那么他們的進程描述符仍然保存在系統中,系統資源會緩慢泄露。
大多數設計良好的多進程應用可以正確的收回僵屍子進程,比如NGINX master進程可以收回已終止的worker子進程。如果需要自己實現,則可利用如下方法:
1. 利用操作系統的waitpid()函數等待子進程結束並請除它的僵死進程,
2. 由於當子進程成為“defunct”進程時,父進程會收到一個SIGCHLD信號,所以我們可以在父進程中指定信號處理的函數來忽略SIGCHLD信號,或者自定義收回處理邏輯。
下面這些文章詳細介紹了對僵屍進程的處理方法
· http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html
· http://lbolla.info/blog/2014/01/23/die-zombie-die
如果父進程已經結束了,那些依然在運行中的子進程會成為“孤兒(orphaned)”進程。在Linux中Init進程(PID1)作為所有進程的父進程,會維護進程樹的狀態,一旦有某個子進程成為了“孤兒”進程后,init就會負責接管這個子進程。當一個子進程成為“僵屍”進程之后,如果其父進程已經結束,init會收割這些“僵屍”,釋放PID資源。
然而由於Docker容器的PID1進程是容器啟動進程,它們會如何處理那些“孤兒”進程和“僵屍”進程?
下面我們做幾個試驗來驗證不同的PID1進程對僵屍進程不同的處理能力
首先在myredis2容器中啟動一個bash進程,並創建子進程“sleep 1000”
docker@default:~$ docker restart myredis2
myredis2
docker@default:~$ docker exec -ti myredis2 bash
root@d1df0e4f4e3b:/# sleep 1000
在另一個終端窗口,查看當前進程,我們可以發現一個sleep進程是bash進程的子進程。
docker@default:~$ docker exec myredis2 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 12:21 ? 00:00:00 /usr/bin/redis-server *:6379
root 8 0 0 12:21 ? 00:00:00 bash
root 21 8 0 12:21 ? 00:00:00 sleep 1000
root 22 0 3 12:21 ? 00:00:00 ps -ef
我們殺死bash進程之后查看進程列表,這時候bash進程已經被殺死。這時候sleep進程(PID為21),雖然已經結束,而且被PID1進程(redis-server)接管,但是其沒有被父進程回收,成為僵屍狀態。
docker@default:~$ docker exec myredis2 kill -98
docker@default:~$ docker exec myredis2 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 012:09 ? 00:00:00 /usr/bin/redis-server *:6379
root 21 1 012:10 ? 00:00:00 [sleep] <defunct>
root 32 0 012:10 ? 00:00:00 ps -ef
docker@default:~$
這是因為PID1進程“redis-server”沒有考慮過作為init對僵屍子進程的回收的場景。
我們來做另一個試驗,在用/bin/sh作為PID1進程的myredis容器中,再啟動一個bash進程,並創建子進程“sleep 1000”
docker@default:~$ docker start myredis
myredis
docker@default:~$ docker exec -ti myredis bash
root@49f7fc37f4b7:/# sleep 1000
查看容器中進程情況,
docker@default:~$ docker exec myredis ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:29 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 5 1 0 01:29 ? 00:00:00 /usr/bin/redis-server *:6379
root 8 0 0 01:30 ? 00:00:00 bash
root 22 8 0 01:30 ? 00:00:00 sleep 1000
root 36 0 0 01:30 ? 00:00:00 ps -ef
我們殺死bash進程之后查看進程列表,發現“bash”和“sleep 1000”進程都已經被殺死和回收
docker@default:~$ docker exec myredis kill -98
docker@default:~$ docker exec myredis ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 001:29 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 5 1 001:29 ? 00:00:00 /usr/bin/redis-server *:6379
root 45 0 001:31 ? 00:00:00 ps -ef
docker@default:~$
這是因為sh/bash等應用可以自動清理僵屍進程。
關於僵屍進程在Docker中init處理所需注意細節的詳細描述,可以在如下文章得到
· http://www.oschina.net/translate/docker-and-the-pid-1-zombie-reaping-problem
簡單而言,如果在容器中運行多個進程,PID1進程需要有能力接管“孤兒”進程並回收“僵屍”進程。我們可以
1. 利用自定義的init進程來進行進程管理,比如 S6 , phusion myinit,dumb-init, tini 等
2. Bash/sh等缺省提供了進程管理能力,如果需要可以作為PID1進程來實現正確的進程回收。
進程監控
在Docker中,如果docker run命令中指明了restart policy,Docker Daemon會監控PID1進程,並根據策略自動重啟已結束的容器。
restart 策略 |
結果 |
no |
不自動重啟,缺省值 |
on-failure[:max-retries] |
當PID1進程退出值非0時,自動重啟容器;可以指定最大重試次數 |
always |
永遠自動重啟容器;當Docker Daemon啟動時,會自動啟動容器 |
unless-stopped |
永遠自動重啟容器;當Docker Daemon啟動時,如果之前容器不為stoped狀態就自動啟動容器 |
注意:為防止頻繁重啟故障應用導致系統過載,Docker會在每次重啟過程中會延遲一段時間。Docker重啟進程的延遲時間從100ms開始並每次加倍,如100ms,200ms,400ms等等。
利用Docker內置的restart策略可以大大簡化應用進程監控的負擔。但是Docker Daemon只是監控PID1進程,如果容器在內包含多個進程,仍然需要開發人員來處理進程監控。
大家一定非常熟悉Supervisor,Monit等進程監控工具,他們可以方便的在容器內部中實現進程監控。Docker提供了相應的文檔來介紹,互聯網上也有很多資料,我們今天就不再贅述了。
另外利用Supervisor等工具作為PID1進程是在容器中支持多進程管理的主要實現方式;和簡單利用shell腳本fork子進程相比,采用Supervisor等工具有很多好處:
· 一些傳統的服務不能以PID1進程的方式執行,利用Supervisor可以方便的適配
· Supervisor這些監控工具大多提供了對SIGTERM的信號傳播支持,可以支持子進程優雅的退出
然而值得注意的是:Supervisor這些監控工具大多沒有完全提供Init支持的進程管理能力,如果需要支持子進程回收的場景需要配合正確的PID1進程來完成
總結
進程管理在Docker容器中和在完整的操作系統有一些不同之處。在每個容器的PID1進程,需要能夠正確的處理SIGTERM信號來支持容器應用的優雅退出,同時要能正確的處理孤兒進程和僵屍進程。
在Dockerfile中要注意shell模式和exec模式的不同。通常而言我們鼓勵使用exec模式,這樣可以避免由無意中選擇錯誤PID1進程所引入的問題。
在Docker中“一個容器一個進程的方式”並非絕對化的要求,然而在一個容器中實現對於多個進程的管理必須考慮更多的細節,比如子進程管理,進程監控等等。所以對於常見的需求,比如日志收集,性能監控,調試程序,我們依然建議采用多個容器組裝的方式來實現。