今天我们会分析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: