快捷搜索:  汽车  科技

docker容器存活监控:阿里Docker踩坑实践之一

docker容器存活监控:阿里Docker踩坑实践之一在Docker容器中的初始化进程(PID1进程)在容器进程管理上具有特殊意义。它可以被Dockerfile中的ENTRYPOINT或CMD指令所指明;也可以被docker run命令的启动参数所覆盖。了解这些细节可以帮助我们更好地了解PID1的进程的行为。如何指明容器PID1进程容器的生命周期和其PID1进程一致利用docker exec可以进入到容器的名空间中启动进程此外,自从Docker 1.5之后,docker run命令引入了--pid=host参数来支持使用宿主机PID名空间来启动容器进程,这样可以方便的实现容器内应用和宿主机应用之间的交互:比如利用容器中的工具监控和调试宿主机进程。

今天开工!看到头条号为云栖社区提供的小分享,做的很不错。不过文章分享还可以更多,努力【握拳】!分享一篇不可多得的帮助大家避免入坑的docker实践文章!

————————————————————————————————————————————

Docker在进程管理上有一些特殊之处,如果不注意这些细节中的魔鬼就会带来一些隐患。另外Docker鼓励“一个容器一个进程(one process per container)”的方式。这种方式非常适合以单进程为主的微服务架构的应用。然而由于一些传统的应用是由若干紧耦合的多个进程构成的,这些进程难以拆分到不同的容器中,所以在单个容器内运行多个进程便成了一种折衷方案;此外在一些场景中,用户期望利用Docker容器来作为轻量级的虚拟化方案,动态的安装配置应用,这也需要在容器中运行多个进程。而在Docker容器中的正确运行多进程应用将给开发者带来更多的挑战。

docker容器存活监控:阿里Docker踩坑实践之一(1)

  • 每个容器有独立的PID名空间,

  • 容器的生命周期和其PID1进程一致

  • 利用docker exec可以进入到容器的名空间中启动进程

  • 此外,自从Docker 1.5之后,docker run命令引入了--pid=host参数来支持使用宿主机PID名空间来启动容器进程,这样可以方便的实现容器内应用和宿主机应用之间的交互:比如利用容器中的工具监控和调试宿主机进程。

    如何指明容器PID1进程

    在Docker容器中的初始化进程(PID1进程)在容器进程管理上具有特殊意义。它可以被Dockerfile中的ENTRYPOINTCMD指令所指明;也可以被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.04RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*EXPOSE 6379CMD "/usr/bin/redis-server"

    "Dockerfile_exec"文件内容如下,会利用exec方式启动redis服务

    FROM ubuntu:14.04RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*EXPOSE 6379CMD ["/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:shell49f7fc37f4b7cf1ed7f5296537a93b2ad23b1b6686a05e5c7e40e9a2b2d3665e

    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 *:6379root 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 *:6379root 8 0 0 08:13 ? 00:00:00 ps -ef

    由此我们可以清楚的看到,以exec和shell方式执行命令可能会导致容器的PID1进程不同。然而这又有什么问题呢?

    原因在于:PID1进程对于操作系统而言具有特殊意义。操作系统的PID1进程是init进程,以守护进程方式运行,是所有其他进程的祖先,具有完整的进程生命周期管理能力。在Docker容器中,PID1进程是启动进程,它也会负责容器内部进程管理的工作。而这也将导致进程管理在Docker容器内部和完整操作系统上的不同。

    进程信号处理

    信号是Unix/Linux中进程间异步通信机制。Docker提供了两个命令docker stopdocker kill来向容器中的PID1进程发送信号。

    当执行docker stop命令时,docker会首先向容器的PID1进程发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有结束, 那么Docker Daemon会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死变为退出状态。这种方式给Docker应用提供了一个优雅的退出(graceful stop)机制,允许应用在收到stop命令时清理和释放使用中的资源。而docker kill可以向容器内PID1进程发送任何信号,缺省是发送SIGKILL信号来强制退出应用。

    我们来看看不同的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 6379docker@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 1docker@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 *:6379root 8 0 0 12:21 ? 00:00:00 bash

    root 21 8 0 12:21 ? 00:00:00 sleep 1000root 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 8docker@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 *:6379root 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 *:6379root 8 0 0 01:30 ? 00:00:00 bash

    root 22 8 0 01:30 ? 00:00:00 sleep 1000root 36 0 0 01:30 ? 00:00:00 ps -ef

    我们杀死bash进程之后查看进程列表,发现“bash”和“sleep 1000”进程都已经被杀死和回收

    docker@default:~$ docker exec myredis kill -9 8docker@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 *:6379root 45 0 0 01: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 等

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

    备:发现这里发代码片不太OK。建议仔细看代码的朋友们去云栖社区看下原文,或者关注阿里技术嘉年华微博。

    猜您喜欢: