为什么在一个Docker中运行多个程序进程? Docker在进程管理上有一些特殊之处,如果不注意这些细节中就会带来一些隐患。另外Docker鼓励“一个容器一个进程(one process per container)”的方式。这种方式非常适合以单进程为主的微服务架构的应用。然而由于一些传统的应用是由若干紧耦合的多个进程构成的,这些进程难以拆分到不同的容器中,所以在单个容器内运行多个进程便成了一种折衷方案;此外在一些场景中,用户期望利用Docker容器来作为轻量级的虚拟化方案,动态的安装配置应用,这也需要在容器中运行多个进程。而在Docker容器中的正确运行多进程应用将给开发者带来更多的挑战。
如何在一个Docker中运行多个程序进程? 基本思路是在Dockerfile 的CMD 或者 ENTRYPOINT 运行一个”东西”,然后再让这个”东西”运行多个其他进程 简单说来是用Bash Shell脚本或者三方进程守护 (Monit,Skaware S6,Supervisor),其他没讲到的三方进程守护工具同理。
docker内运行多进程问题 一 孤儿进程与僵尸进程管理
当一个子进程终止后,它首先会变成一个“失效(defunct)”的进程,也称为“僵尸(zombie)”进程,等待父进程或系统收回(reap)。在Linux内核中维护了关于“僵尸”进程的一组信息(PID,终止状态,资源使用信息),从而允许父进程能够获取有关子进程的信息。如果不能正确回收“僵尸”进程,那么他们的进程描述符仍然保存在系统中,系统资源会缓慢泄露。
大多数设计良好的多进程应用可以正确的收回僵尸子进程,比如NGINX master进程可以收回已终止的worker子进程。如果需要自己实现,则可利用如下方法:
利用操作系统的waitpid()函数等待子进程结束并请除它的僵死进程,
由于当子进程成为“defunct”进程时,父进程会收到一个SIGCHLD信号,所以我们可以在父进程中指定信号处理的函数来忽略SIGCHLD信号,或者自定义收回处理逻辑。
如果父进程已经结束了,那些依然在运行中的子进程会成为“孤儿(orphaned)”进程。在Linux中Init进程(PID1)作为所有进程的父进程,会维护进程树的状态,一旦有某个子进程成为了“孤儿”进程后,init就会负责接管这个子进程。当一个子进程成为“僵尸”进程之后,如果其父进程已经结束,init会收割这些“僵尸”,释放PID资源。
然而由于Docker容器的PID1进程是容器启动进程,它们会如何处理那些“孤儿”进程和“僵尸”进程?
二 进程的高可用,进程异常结束后如何恢复。
单进程的容器进程挂掉后整个容器也会停止。但多进程的如果遇见这样的情况 :第一个进程负责正常的对外工作,第二个进程是一个被第一个进程调用的常驻程序(或者为第一个进程提供些库的更新),不能停止,停止后会影响第一个进程的正常工作。
1、用/bin/sh 或者/bin/bash作为PID1进程,这是因为sh/bash等应用可以自动清理僵尸进程。Bash/sh等缺省提供了进程管理能力,如果需要可以作为PID1进程来实现正确的进程回收。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 19:33 $ sudo docker exec -it ditto_cron bash [root@02f08adf3cd6 s]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 19:33 ? 00:00:00 /bin/bash /home/s/script/start.sh root 16 1 0 19:33 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 43 1 10 19:33 ? 00:00:01 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 50 0 0 19:33 ? 00:00:00 bash root 73 50 0 19:33 ? 00:00:00 ps -ef [root@02f08adf3cd6 s]# kill 41 [root@02f08adf3cd6 s]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 19:33 ? 00:00:00 /bin/bash /home/s/script/start.sh root 16 1 0 19:33 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 43 1 6 19:33 ? 00:00:01 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 50 0 0 19:33 ? 00:00:00 bash root 83 50 0 19:33 ? 00:00:00 ps -ef
但是这种需要CMD或者ENTRYPOINT采用exec形式:
1 CMD ["可执行文件", "参数1", "参数2"...]
另一种格式是shell格式
exec 格式会让/bin/bash 成为1号进程,而shell格式会让后面的命令行成为1号进程。
这种方法可以解决掉僵尸进程的问题,但是进程的高可用需要增加脚本实现。
2、使用Supervisor
Supervisor是一个C/S架构进程管理工具,通过它可以监控和控制其他的进程。可以处理僵尸进程的问题及SIGTERM信号。 在Linux系统启动之后,第一个启动的用户态进程是/sbin/init ,它的PID是1,其余用户态的进程都是init进程的子进程。Supervisor在Docker容器里面充当的就类似init进程的角色,其它的应用进程都是Supervisor进程的子进程。通过这种方法就可以实现在一个容器中启动运行多个应用,。
1 2 3 4 5 6 7 8 9 [root@1e7babdbf192 s]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 20:21 ? 00:00:00 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf root 7 1 1 20:21 ? 00:00:01 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 8 1 0 20:21 ? 00:00:00 /bin/bash /home/s/script/check_dconf.sh root 30 1 0 20:21 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 214 0 0 20:22 ? 00:00:00 bash root 247 8 0 20:22 ? 00:00:00 sleep 10 root 248 214 0 20:22 ? 00:00:00 ps -ef
但要注意一点:supervisor只能管理到前台进程,对于一般的服务,没有终端的进程supervisor无法管理。 除非是把这种进程放入一个脚本中,让这个脚本前台运行并且检测该进程的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/bin/bash EXEC="/home/s/dconf_reload/src/dconf_main.php" PROG=`basename $EXEC` LogPath="/home/s/dconf_reload/log" Log="${LogPath}/check_dconf.log.`date +%F`" check() { #判断指定进程是否存在 result=`ps -ef | grep -w $PROG | grep -v grep | wc -l` if [ $result -le 0 ]; then #不存在, 启动 /bin/bash /home/s/dconf_reload/bin/dctl check ditto >/dev/null 2>&1 sleep 2 echo "`date +'%Y-%m-%d %H:%M:%S'` restart dconf" >> $Log #ps axuwwww | grep scan_unit | grep avast | grep -v grep | awk '{print $2}' | xargs kill -9 else #存在,判断状态 #取进程状态,用来判断是否僵死 val=`ps aux | grep $PROG | grep -v grep | awk '{print $8}'` if [ "$val" == "Zs" ];then # 取进程ID,用来kill掉进程 pid = `ps -aux | grep $PROG | grep -v grep | awk '{print $2}'` kill -9 $pid echo "`date +'%Y-%m-%d %H:%M:%S'` <defunct> process ..." >> $Log exit 1 else sleep 10 echo "`date +'%Y-%m-%d %H:%M:%S'` sleep 10" >> $Log fi fi } while true do check done
supervisor.conf 配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 [unix_http_server] file=/var/run/supervisor/supervisor.sock ; (the path to the socket file) chmod=0700 ; sockef file mode (default 0700) [inet_http_server] port:127.0.0.1:9001 [supervisord] pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) nodaemon=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 loglevel=debug [supervisorctl] serverurl=http://127.0.0.1:9001 [program:check_dconf] user=root command=/home/s/script/check_dconf.sh autostart=true autorestart=true startsecs=1 stopsignal=INT [program:check_ditto] user=root command=/home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml autostart=true autorestart=true startsecs=1 stdout_logfile=/home/s/scanService/log/stdout.log stdout_logfile_maxbytes=10MB stdout_logfile_backups=10 stdout_capture_maxbytes=1MB stderr_logfile=/home/s/scanService/log/stderr.log stderr_logfile_maxbytes=10MB stderr_logfile_backups=10 stderr_capture_maxbytes=1MB stopsignal=INT
supervisor 在多进程的情况如果都是前台进程会很好用,因为它解决了僵尸进程和高可用两个问题。但如果有后台程序的话处理就要配合脚本实现。
3、使用monit
Monit是一个轻量级(500KB)跨平台的用来监控Unix/linux系统的开源工具。部署简单,并且不依赖任何第三方程序、插件或者库。
Monit可以监控服务器进程、文件、文件系统、网络状态(HTTP/SMTP等协议)、远程主机、服务器资源变化等等。 并且可以设定资源变化后需要做的动作,比如服务失败后自动重启,邮件告警等等。 相对于supervisor而言,monit的功能更为强大,不仅可以管理前台、后台进程,而且还能监控文件系统,网络的资源。这里不详细讲解monit的安装使用。只贴下monit的配置
/etc/monit.conf 主配置文件
/etc/monit.d/ 各项服务单独配置文件路径,在主配置文件中将其include进来。
monit.conf monit卓配置
1 2 3 4 5 6 7 set daemon 30 # check services at 30 seconds intervals set log syslog set httpd port 2812 and use address localhost # only accept connection from localhost allow localhost # allow localhost to connect to the server and allow admin:monit # require user 'admin' with password 'monit' include /etc/monit.d/*
dconf.conf 配置,需提供dconf的启动脚本和停止脚本
1 2 3 pheck process dconf with MATCHING dconf_main.php start "/bin/bash -c /home/s/script/start_dconf.sh" stop "/bin/bash -c /home/s/script/stop_dconf.sh"
ditto.conf 配置,,需提供ditto的启动脚本和停止脚本
1 2 3 4 5 6 check process ditto with MATCHING scanService start "/bin/bash -c /home/s/script/start_ditto.sh" stop "/bin/bash -c /home/s/script/stop_ditto.sh" if failed port 9234 3 cycles then restart
monit 提供了前台运行方式,解决了多进程不管是前台运行还是后台运行,还有进程高可用的的问题。然而不幸的是,monit没有提供管理僵尸进程(回收子进程)问题的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 10:36 $ sudo docker exec -it ditto_monit bash [root@152b5b9b6423 s]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 10:43 ? 00:00:00 /usr/bin/monit -I root 14 1 10 10:43 ? 00:00:01 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 35 1 0 10:43 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 85 0 1 10:43 ? 00:00:00 bash root 97 85 0 10:43 ? 00:00:00 ps -ef [root@152b5b9b6423 s]# kill 14 [root@152b5b9b6423 s]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 10:43 ? 00:00:00 /usr/bin/monit -I root 14 1 5 10:43 ? 00:00:01 [ditto] <defunct> root 35 1 0 10:43 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 85 0 0 10:43 ? 00:00:00 bash root 108 35 68 10:44 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 109 85 0 10:44 ? 00:00:00 ps -ef
所以需要加入一个脚本,这个脚本运行为pid为1的进程,负责回收处理。my_init
1 2 3 4 5 6 7 root 1 0 0 21:37 ? 00:00:00 /usr/bin/python2.6 /home/s/script/my_init -- /usr/bin/monit -I root 8 1 0 21:37 ? 00:00:00 /usr/bin/monit -I root 16 1 4 21:37 ? 00:00:01 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 32 1 0 21:37 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 97 0 0 21:38 ? 00:00:00 bash root 118 32 85 21:38 ? 00:00:00 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 119 97 0 21:38 ? 00:00:00 ps -ef
不采用my_init 这种第三方的程序,自己实现子进程的回收处理及信号处理也可以。
docker 高版本在提供了解决方案 在run时加入–init参数可以在容器内部启动一个init 进程作为1号进程, 但是低版本的docker无此功能。
1 2 3 [jinri@23v update]$ docker run --help|grep init --health-start-period duration Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s) --init Run an init inside the container that forwards signals and reaps processes
另外如果使用的是centos7的镜像还可以使用系统自带的systemd作为容器中的1号进程。它提供进程的自启和信号处理等工作。
最终采用方案:
使用 /bin/bash + crond 的方式
/bin/bash 实现子进程的回收,crond实现对 dconf的高可用监控重启
1 2 3 4 5 6 7 8 UID PID PPID C STIME TTY TIME CMD root 1 0 0 15:13 pts/0 00:00:00 /bin/bash /home/s/script/start.sh root 21 1 0 15:13 pts/0 00:00:01 php /home/s/dconf_reload/bin/../src/dconf_main.php ditto 1 3 root 46 1 0 15:13 ? 00:00:00 crond root 48 1 0 15:13 pts/0 00:00:02 /home/s/scanService/ditto worker --config /home/s/scanService/conf/config.yaml root 1401 0 0 15:31 pts/1 00:00:00 bash root 4438 0 2 16:11 pts/2 00:00:00 bash root 4450 4438 0 16:11 pts/2 00:00:00 ps -ef
参考链接
理解Docker容器的进程管理
Monit 简介
docker 和pid 1 僵尸进程问题
一个容器多个进程