摘要: 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中“一个容器一个进程的方式”并不是绝对化的要求,然而在一个容器中实现对于多个进程的管理必须考虑更多的细节,好比子进程管理,进程监控等等。因此对于常见的需求,好比日志收集,性能监控,调试程序,咱们依然建议采用多个容器组装的方式来实现。