最近重构一些老项目,为此实现了一个基于etcd的grpc服务注册、发现及负载均衡的中间件
服务注册嘛,无论docker还是k8s部署,都需要服务在监听到系统信号 SIGTERM
,实现标准的优雅停机
,摘除注册
之前的服务启动命令是一个脚本,看似实现了,但没有完全实现
Linux中 ,pid 为 1 的进程,有着特殊的使命:
- 传递信号,确保子进程完全退出
- 等待子进程退出
容器化环境中,往往直接运行应用程序,而缺少初始化系统(如 systemd、sysvinit 等)。这可能需要应用程序来处理系统信号,接管子进程,进而导致容器无法停止、产生僵尸进程等问题。Yelp 开发的 dumb-init ,旨在模拟初始化系统功能,向子进程代理发送信号和接管子进程。
1 |
|
其中基础镜像是alpine,/init.sh 的脚本内容为 nohup crond -f &
,目的是运行crontab
fluentd是收集日志的,定时任务也是给它用的,定期清理日志文件。。
真正的服务则是要依赖 /app/main
这个二进制运行,真实的进程情况
1 | /app # ps -eo pid,ppid,args |
可见,shebang中添加 /usr/bin/dumb-init
起作用了但没有完全起作用。脚本中的第一行脚本执行后,其ppid确实为1
再之后运行的命令,则隶属于脚本之下的进程id了
原因
进程树可以清晰的看到,main的ppid并非1,所以无法接收到dumb-init
传递的信号,所以无法真正实现优雅停机
dumb-init的原理就是其作为pid为1的进程,接收到的信号会向其子进程传递
但其有个配置环境变量 DUMB_INIT_SETSID
,如果设为0则只向直接子进程发送信号,即子进程的ppid必须为1才能接收到传递的信号
问题则就是出现在这个配置上,基础镜像被添加了环境变量并且值为0
验证
使用go写一些demo来模拟程序并监听信号退出,通过修改timeout来控制程序自动退出时间
1 | const timeout = 600 |
1 | # 编译三个不同时间的二进制程序 |
1 | FROM alpine:latest |
启动脚本#!/usr/bin/dumb-init /bin/sh
1 |
|
结果
不适用dumb-init
1 | / # pstree && ps -eo pid,ppid,args |
ENTRYPOINT
1 | / # pstree && ps -eo pid,ppid,args |
ENTRYPOINT + shebang
1 | / # pstree && ps -eo pid,ppid,args |
shenbang、ENTRYPOINT单独使用效果一致,不单独列出了
- | 条件 | 进程树 | log | 备注 |
---|---|---|---|---|
1 | 不使用dumb-init | 见上方 | $ docker logs -f dumb 600 ticker 60 ticker 30 ticker # 执行 docker stop dumb 600 ticker 30 ticker 60 ticker … |
执行docker stop dumb 后容器并没有停止且log中也没有信号接收的输出, 直到docker默认的等待时间(10s)被强杀 |
2 | ENTRYPOINT + DUMB_INIT_SETSID= | 见上方 | $ docker logs -f dumb 60 ticker 30 ticker 600 ticker 600 ticker 60 ticker 30 ticker # 执行 docker stop dumb 600 signal 60 signal |
执行docker stop dumb 后,log中出现了两个程序接收到信号的日志输出 多次运行后,信号输出未1-3个不等, start.sh是dumb-int的直接子进程 但start.sh并未等待它所有子进程推出后再退出 |
3 | ENTRYPOINT + DUMB_INIT_SETSID=0 | 同2 | 同1 | 执行docker stop dumb 后容器立即退出原因同2 |
4 | ENTRYPOINT + shebang + DUMB_INIT_SETSID=0 | 见上方 | 同1 | 执行docker stop dumb 后容器立即退出原因同2 直接子进程分别是start.sh 以及 shebang中的 sh |
5 | ENTRYPOINT + shebang + DUMB_INIT_SETSID= | 同4 | $ docker logs -f dumb 60 ticker 30 ticker 600 ticker 60 ticker 30 ticker 600 ticker # 执行 docker stop dumb 60 signal 600 signal 30 signal |
执行docker stop dumb 后容器等待所有子进程退出后才退出。 实现预期目标 原因是脚本shebang中设置 /usr/bin/dumb-init ,其pid为6,ppid为1,接收到pid1传递的信号后再传递 同时等待所有子进程退出后才退出 |
解决方法
如果多程序中只有一个程序需要获取到信号,则通过exec提升子进程
exec 命令用于调用并执行指定的命令。
通常用在 Shell 脚本程序中,可以调用其他的命令。如果在当前终端中使用命令,则当指定的命令执行完毕后会立即退出终端。
补充信息,使用exec后,DUMB_INIT_SETSID配置无影响
具体原因看进程树就明白了
ENTRYPOINT
此时有且只有app_60s接收到了信号,具体看进程树
start.sh
1 |
|
1 | / # pstree && ps -eo pid,ppid,args |
ENTRYPOINT + shebang
此时有且只有app_60s接收到了信号,具体看进程树
此处可发现,dumb-init多层是可以的,会一直向下传递
1 | / # pstree && ps -eo pid,ppid,args |
如果多个程序都需要获取到信号,则 ENTRYPOINT + shebang + DUMB_INIT_SETSID=
子进程都能从dumb-init获取到信号,并等待其退出