容器实战高手课
李程远
eBay 总监级工程师,云平台架构师
24647 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 31 讲
容器实战高手课
15
15
1.0x
00:00/00:00
登录|注册

04 | 理解进程(3):为什么我在容器中的进程被强制杀死了?

解决容器中进程被强制杀死的问题的方法
了解信号处理的基本概念和系统调用
分析代码输出
在容器的init进程中对收到的信号做转发,发送到容器中的其他子进程
tini作为init进程对信号的转发
SIGKILL和SIGSTOP信号的特权性质
信号处理的三个选择:缺省行为、捕获、忽略
信号的两个系统调用:kill() 和 signal()
在容器平台上停止容器时,init进程收到SIGTERM信号,其他进程收到SIGKILL信号
如何让容器中的进程收到SIGTERM信号而不是SIGKILL信号
容器中的进程被强制杀死的原因
进程管理中的重要性
总结
思考题
解决问题
知识详解
场景再现
问题
为什么我在容器中的进程被强制杀死了?

该思维导图由 AI 生成,仅供参考

你好,我是程远。
今天我们来讲容器中 init 进程的最后一讲,为什么容器中的进程被强制杀死了。理解了这个问题,能够帮助你更好地管理进程,让容器中的进程可以 graceful shutdown。
我先给你说说,为什么进程管理中做到这点很重要。在实际生产环境中,我们有不少应用在退出的时候需要做一些清理工作,比如清理一些远端的链接,或者是清除一些本地的临时数据。
这样的清理工作,可以尽可能避免远端或者本地的错误发生,比如减少丢包等问题的出现。而这些退出清理的工作,通常是在 SIGTERM 这个信号用户注册的 handler 里进行的。
但是,如果我们的进程收到了 SIGKILL,那应用程序就没机会执行这些清理工作了。这就意味着,一旦进程不能 graceful shutdown,就会增加应用的出错率。
所以接下来,我们来重现一下,进程在容器退出时都发生了什么。

场景再现

在容器平台上,你想要停止一个容器,无论是在 Kubernetes 中去删除一个 pod,或者用 Docker 停止一个容器,最后都会用到 Containerd 这个服务。
而 Containerd 在停止容器的时候,就会向容器的 init 进程发送一个 SIGTERM 信号。
我们会发现,在 init 进程退出之后,容器内的其他进程也都立刻退出了。不过不同的是,init 进程收到的是 SIGTERM 信号,而其他进程收到的是 SIGKILL 信号。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了容器中进程被强制杀死的原因以及实现进程的graceful shutdown的方法。在生产环境中,进程退出时需要进行清理工作,以避免错误。文章详细介绍了容器中进程收到不同信号的情况,以及信号的两个系统调用kill()和signal()。此外,还解释了进程对信号的处理方式:缺省行为、捕获和忽略。特别强调了SIGKILL和SIGSTOP信号是特权信号,不允许被捕获和忽略。解决容器中进程被强制杀死的问题的方法是在容器的init进程中对收到的信号进行转发,发送到容器中的其他子进程,从而让所有进程在停止时都收到SIGTERM信号而不是SIGKILL信号。整体而言,本文通过系统调用和内核代码的解析,深入剖析了容器中进程退出的原因和处理方式,为读者提供了深入理解容器技术的知识。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《容器实战高手课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(29)

  • 最新
  • 精选
  • 胖胖虎
    简单总结了下,子进程被kill杀死的原因是,父进程在退出时,执行do_exit中,由于是cgroup_init 组的进程,因此向所有的子进程发送了sigkill信号。而导致这个的原因是,一般情况下,容器起来的第一个进程都不是专业的init进程,没有考虑过这些细节问题。由于正常情况下,父进程被终结,信号不会传递到子进程,exit时也不会给子进程发终结命令。这会导致多进程容器在关闭时,无法被终止。为了保证容器能够被正常终结。设计者在do_exit中做文章,使用sigkill这个不可屏蔽信号,而是为了能够在没有任何前提条件的情况下,能够把容器中所有的进程关掉。而一个优雅的解决方法是,使用一个专业的init进程作为容器的第一个进程,来处理相关业务。实现容器的优雅关闭。当然,如果子进程也把SigTerm做了劫持,那也是有可能导致容器无法关闭。

    作者回复: @胖胖虎, 很好的总结!

    2020-11-30
    2
    34
  • JianXu
    CY , 能帮忙解释一下我们公司生产环境在容器image patching 过程中应用程序受影响的事情吗。 1. 我们的胖容器肯定是多进程的,那当容器收到kill 命令的时候,我们现在也是子容器都被SIGKill 吗?还是我们其实都是配置了Init 进程,而init 进程其实都像文中说的转发了 SIGTERM 命令? 2. 如果应用程序写的不够好,不相应SIGTERM 命令。所以我们才在一段时间容器还没有被杀死的情况下执行 Kill -9 吗? 3. 我们大部分的应用程序都是web 程序,使用标准JVM , 比如 Tomcat 加 OpenJDK , 不大明白为什么不能正常响应SIGTERM 做graceful shutdown 。 Kubernetes 标准操作,当我们做OS patching的时候都是换image 的,这时候当前POD 会被干掉,我们是那个POD 因为不能响应SIGTERM 而始终处于terminating 吗?

    作者回复: @JianXu, 你说的情况是这样的, 胖容器的init进程其实是一个bash脚本run.sh, 由它来启动jvm的程序。 但是run.sh本身没有注册SIGTERM handler, 也不forward SIGTERM给子进程jvm。 当stop容器的时候,run.sh先收到一个SIGTERM, run.sh没有注册SIGTERM, 所以呢对SIGTERM没有反应,contaienrd过30秒,会发SIGKILL给run.sh, 这样run.sh退出do_exit(),在退出的时候同样给子进程jvm程序发送了SIGKILL而不是SIGTERM。其实呢,jvm的程序是注册了SIGTERM handler的,但是没有机会调用handler了。

    2020-11-28
    6
    18
  • po
    老师,我做了个测试,现象有点迷惑,我打开两个终端,用sleep进行测试,方法和现象如下: 1. 在第一个终端启动sleep,在另外一个终端通过命令去kill,能通过sigterm正常杀掉进程。 # strace sleep 30000 execve("/usr/bin/sleep", ["sleep", "30000"], [/* 25 vars */]) = 0 ................................................................................................ --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=1505, si_uid=0} --- +++ killed by SIGTERM +++ 2. 启动一个容器里面的命令是sleep 30000,用strace跟踪进程,我使用kill,杀不掉sleep进程,然后通过docker stop发现,先是发送sigterm信号,没有成功,最后被强制杀掉了: # strace -p 2207 strace: Process 2207 attached restart_syscall(<... resuming interrupted nanosleep ...>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal) --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} --- restart_syscall(<... resuming interrupted restart_syscall ...> ) = ? ERESTART_RESTARTBLOCK (Interrupted by signal) --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_USER, si_pid=0, si_uid=0} --- restart_syscall(<... resuming interrupted restart_syscall ...> ) = ? ERESTART_RESTARTBLOCK (Interrupted by signal) +++ killed by SIGKILL +++ 我有点迷惑,老师能解释一下为什么在宿主机或者用docker不能用sigterm来杀死容器的进程吗?

    作者回复: @po, 对于第二个问题,我假设sleep进程在宿主机上的pid是2207, 你还是可以先查看"cat /proc/2207/status | grep SigCgt", 我的理解是SIGTERM handler应该还是没有注册,那么即使从宿主机上发送SIGTERM给这个容器里的1号进程,那么也是不能杀死的。 "docker stop"在停止容器的时候,先给容器里的1号进程发送SIGTERM, 如果不起作用,那么等待30秒后会发送SIGKILL。我想这个是你看到的现象了。 至于为什么即使在宿主机机上向容器1号进程发送SIGTERM,在1号进程没有注册handler的情况下,不能被杀死的问题 (思考题), 原因是这样的: 开始要看内核里的那段代码,“ !(force && sig_kernel_only(sig))”, 虽然由不同的namespace发送信号, 虽然force是1了,但是sig_kernel_only(sig)对于SIGTERM来说还是0, 这里是个&&, 那么 !(1 && 0) = 1。 #define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK) #define SIG_KERNEL_ONLY_MASK (\ rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))

    2020-11-25
    3
    13
  • JianXu
    老师,这里的逻辑我还没有理顺。 1. 你说的容器init 进程,是不是就是容器的第一个进程?还有是不是如果我使用docker , 容器的第一个进程一定不是我自己的进程,而是tini 进程? 2. 上文所SIGTerm 发送后,触发do exit 函数,SIGkill 其实是在内核往容器内的其他子进程发送的。那当我在init 进程配置了Sig term handler 截取信号转发sigterm 以后,do exit 函数还会被调用吗?如果不被调用,do exit 里其他的退出逻辑怎么被执行呢?如果被调用,怎么就不执行sigkill 了呢?

    作者回复: > 1 是的, init进程就是容器里的第一个进程。容器里的第一个进程大部分情况应该是我们自己的进程,除非有容器用户有意识的去使用tini进程作为init进程。 > 2 很好的问题。 init 进程自己退出,还是会调用do_exit()的。所以呢,为了保证子进程先收到转发的SIGTERM, 类似tini的做法是,自己在收到SIGTERM的时候不退出,转发SIGTERM给子进程,子进程收到SIGTERM退出之后会给父进程发送SIGCHILD, tini是收到SIGCHILD之后主动整个程序退出。

    2020-11-28
    2
    11
  • po
    老师,我能提个建议吗?这几天学习容器进程和信号相关的知识点,有点乱,自己理出来也好像怪怪的,你能不能画个图,把进程的信号相关的给我们捋一遍呢?还有我们程序代码该如何更好的设计能给一点建议吗?感觉用tini这种方式改动有点大,之前我们一直都是应用程序作为PID1来运行的,好像也没啥问题。谢谢

    作者回复: @po, 谢谢你这几天提的问题,看的出来,你做了很多的测试,也有很多的思考!我们可以一起来,先把你前面的问题逐个理清了。 你说的画图来捋一遍信号概念的,这个我会考虑的 (目前我需要先完成课程后面章节的内容 :-), 你也可以给我一些更详细的建议,可能可以和你问的具体问题想结合。 > 之前我们一直都是应用程序作为PID1来运行的,好像也没啥问题 信号对容器中进程的影响的多少,也有多方面的原因,比如程序本身对错误的容忍度比较高, 容器建立删除的频率不高,那么也就看不出有什么影响。 如果你的程序的容器化程度较高,几乎是一个容器一个进程的程度,那么不需要考虑用tini来做改动。 我觉得容器里的init进程,应该是具备这些信号处理的能力: 1. 至少转发SIGTERM给容器里其他的关键子进程。 2. 能够接受到外部的SIGTERM信号而退出,(这里可以是通过注册SIGTERM handler, 也可以像tini一样先转发SIGTERM 给子进程,然后收到SIGCHILD后自己主动退出) 3. 具有回收zombie进程的能力。

    2020-11-25
    11
  • 宝仔
    老师,容器的最佳实践一般都是一个容器即一个进程,一般如果按照这种做法,就只需要在应用程序进程中对sigterm信号做捕获并处理就行了吧,无需转发吧

    作者回复: 是的!

    2020-11-25
    10
  • Alery
    老师,我有个疑问哈,tini没有注册SIGTERM,按照前面将的,内核是不会把这个信号发送给tini进程的,为啥它又能接收所有的信号(除了SIGHILD)并转发给子进程呢?我对这块的理解的不是很清晰,望指教。

    作者回复: 很好的问题! 因为在tini里调用的sigtimedwait()系统调用,直接把发送给tini的信号先截了下来,这时候tini有没有SIGTERM的handler就没有关系了。

    2020-12-01
    7
  • Alery
    老师,请教一个问题,tini 会把其他所有的信号都转发给它的子进程,假如我的子进程又创建了子进程(也就是tini的孙子进程),tini会把信号转发给孙子进程吗?

    作者回复: 我们可以从tini转发信号的代码看一下。如果 “kill_process_group” 没有设置, 为0时,这也是tini缺省的配置,那么SIGTERM只会转发给子进程,而子子进程就不会收到转发的SIGTERM。当子进程退出的时候,子子进程就会收到SIGKILL。 而如果kill_process_group > 0的时候,同时子进程与子子进程在同一个process group的时候 (缺省fork出来的子进程会和父进程在同一个process group), 那么子子进程就会收到SIGTERM if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo))

    2020-12-01
    3
    7
  • Tendrun
    不太明白zap_pid_ns_processes()这个函数为啥是发送SIGKILL信号,不能设计成发送SIGTERM么,如果是term信号,岂不是就没有容器中子进程中收到sigkill信号的问题了么

    作者回复: @Tendrun 好问题! 不过只有SIGKILL才可以强制杀进程。如果namespace中有进程忽略了SIGTERM,那么就会有进程残留了。

    2020-12-27
    2
    5
  • 上邪忘川
    可以用strace跟踪进程的信号,用法参考https://www.cnblogs.com/machangwei-8/p/10388883.html 运维同学表示代码没看懂,哈哈

    作者回复: @上邪忘川 strace 主要用来查看程序调用了哪些系统调用已经收到什么信号。

    2020-11-23
    2
    4
收起评论
显示
设置
留言
29
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部