摘要: Docker在進程管理上有一些特殊之處,若是不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵「一個容器一個進程(one process per container)」的方式。這種方式很是適合以單進程爲主的微服務架構的應用。然而因爲一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以html
Docker在進程管理上有一些特殊之處,若是不注意這些細節中的魔鬼就會帶來一些隱患。另外Docker鼓勵「一個容器一個進程(one process per container)」的方式。這種方式很是適合以單進程爲主的微服務架構的應用。然而因爲一些傳統的應用是由若干緊耦合的多個進程構成的,這些進程難以拆分到不一樣的容器中,因此在單個容器內運行多個進程便成了一種折衷方案;此外在一些場景中,用戶指望利用Docker容器來做爲輕量級的虛擬化方案,動態的安裝配置應用,這也須要在容器中運行多個進程。而在Docker容器中的正確運行多進程應用將給開發者帶來更多的挑戰。git
今天咱們會分析Docker中進程管理的一些細節,並介紹一些常見問題的解決方法和注意事項。github
在Docker中,進程管理的基礎就是Linux內核中的PID名空間技術。在不一樣PID名空間中,進程ID是獨立的;即在兩個不一樣名空間下的進程能夠有相同的PID。redis
Linux內核爲全部的PID名空間維護了一個樹狀結構:最頂層的是系統初始化時建立的root namespace(根名空間),再建立的新PID namespace就稱之爲child namespace(子名空間),而原先的PID名空間就是新建立的PID名空間的parent namespace(父名空間)。經過這種方式,系統中的PID名空間會造成一個層級體系。父節點能夠看到子節點中的進程,並能夠經過信號等方式對子節點中的進程產生影響。反過來,子節點不能看到父節點名空間中的任何內容,也不可能經過kill或ptrace影響父節點或其餘名空間中的進程。sql
在Docker中,每一個Container都是Docker Daemon的子進程,每一個Container進程缺省都具備不一樣的PID名空間。經過名空間技術,Docker實現容器間的進程隔離。另外Docker Daemon也會利用PID名空間的樹狀結構,實現了對容器中的進程交互、監控和回收。注:Docker還利用了其餘名空間(UTS,IPC,USER)等實現了各類系統資源的隔離,因爲這些內容和進程管理關聯很少,本文不會涉及。docker
當建立一個Docker容器的時候,就會新建一個PID名空間。容器啓動進程在該名空間內PID爲1。當PID1進程結束以後,Docker會銷燬對應的PID名空間,並向容器內全部其它的子進程發送SIGKILL。shell
下面咱們來作一些試驗,下面咱們會利用官方的Redis鏡像建立兩個容器,並觀察裏面的進程。
若是你在Windows或Mac上利用"docker-machine",請利用docker-machine ssh default
進入Boot2docker虛擬機ubuntu
建立名爲"redis"的容器,並在容器內部和宿主機中查看容器中的進程信息api
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
經過以上示例:
docker exec
能夠進入到容器的名空間中啓動進程此外,自從Docker 1.5以後,docker run
命令引入了--pid=host
參數來支持使用宿主機PID名空間來啓動容器進程,這樣能夠方便的實現容器內應用和宿主機應用之間的交互:好比利用容器中的工具監控和調試宿主機進程。
在Docker容器中的初始化進程(PID1進程)在容器進程管理上具備特殊意義。它能夠被Dockerfile中的ENTRYPOINT
或CMD
指令所指明;也能夠被docker run
命令的啓動參數所覆蓋。瞭解這些細節能夠幫助咱們更好地瞭解PID1的進程的行爲。
關於ENTRYPOINT和CMD指令的不一樣,咱們能夠參見官方的Dockerfile說明和最佳實踐
值得注意的一點是:在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進程的特殊性,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信號,或者自定義收回處理邏輯。
下面這些文章詳細介紹了對殭屍進程的處理方法
若是父進程已經結束了,那些依然在運行中的子進程會成爲「孤兒(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等工具備不少好處:
然而值得注意的是: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中「一個容器一個進程的方式」並不是絕對化的要求,然而在一個容器中實現對於多個進程的管理必須考慮更多的細節,好比子進程管理,進程監控等等。因此對於常見的需求,好比日誌收集,性能監控,調試程序,咱們依然建議採用多個容器組裝的方式來實現。