【背景】
在一次问题排查过程中,误杀了yarn任务container的其中一个进程,导致yarn application kill不再生效,并且在rm中任务状态显示为失败,但实际进程还在运行。在分析问题的同时,抽时间对yarn任务的进程、以及kill命令的执行流程进行了整理。本文就来聊聊这些内容。
【yarn任务相关的进程】
在yarn中,任务提交时(不管是AM还是任务container),会指定任务的启动命令,对于AM而言,由客户端提交任务时指定,对于任务container,由AM来指定。
启动命令最终会被传递到NodeManager中,NodeManager会进行一些包装组成多个shell脚本,然后调用这些脚本启动任务。具体涉及的脚本包括:
1)wrapper包装脚本(default_container_executor.sh)
该脚本中直接调用会话脚本,并等待其执行结果。例如:
#!/bin/bash
/bin/bash "/opt/hadoop/yarn/nodemanager/local/usercache/root/appcache/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/default_container_executor_session.sh"
rc=$?
...
2)会话脚本(default_container_executor_session.sh)
在该脚本里通过exec的方式调用container启动脚本。例如:
#!/bin/bash
echo $$ > /opt/hadoop/yarn/nodemanager/local/nmPrivate/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/container_e02_1667473260420_0002_01_000001.pid.tmp
/bin/mv -f /opt/hadoop/yarn/nodemanager/local/nmPrivate/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/container_e02_1667473260420_0002_01_000001.pid.tmp /opt/hadoop/yarn/nodemanager/local/nmPrivate/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/container_e02_1667473260420_0002_01_000001.pid
exec setsid /bin/bash "/opt/hadoop/yarn/nodemanager/local/usercache/root/appcache/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/launch_container.sh"
3)container启动脚本(launch_container.sh)
通过exec加输出重定向的方式,调用提交任务的命令。例如:
#!/bin/bash
...
exec /bin/bash -c "$JAVA_HOME/bin/java -Djava.io.tmpdir=$PWD/tmp -Dlog4j.configuration=container-log4j.properties -Dyarn.app.container.log.dir=/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001 -Dyarn.app.container.log.filesize=0 -Dhadoop.root.logger=INFO,CLA -Dhadoop.root.logfile=syslog -Xmx1024m org.apache.hadoop.mapreduce.v2.app.MRAppMaster 1>/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/stdout 2>/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/stderr "
因此,对于一个container,一共会启动3个进程。
一个实际的例子如下所示:
[root@nm-0 opt]# jps
35237 YarnChild
168 NodeManager
36149 YarnChild
34391 MRAppMaster
[root@nm-0 container_e02_1667473260420_0002_01_000001]# pstree -ps 34381
dumb-init(1)───java(168)───bash(34381)───bash(34383)───java(34391)
[root@nm-0 container_e02_1667473260420_0002_01_000001]# ps -efww
hadoop 34381 168 0 08:54 ? 00:00:00 bash /opt/hadoop/yarn/nodemanager/local/usercache/root/appcache/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/default_container_executor.sh
hadoop 34383 34381 0 08:54 ? 00:00:00 /bin/bash -c /usr/lib/jvm/java/bin/java -Djava.io.tmpdir=/opt/hadoop/yarn/nodemanager/local/usercache/root/appcache/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/tmp -Dlog4j.configuration=container-log4j.properties -Dyarn.app.container.log.dir=/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001 -Dyarn.app.container.log.filesize=0 -Dhadoop.root.logger=INFO,CLA -Dhadoop.root.logfile=syslog -Xmx1024m org.apache.hadoop.mapreduce.v2.app.MRAppMaster 1>/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/stdout 2>/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/stderr
hadoop 34391 34383 45 08:54 ? 00:00:23 /usr/lib/jvm/java/bin/java -Djava.io.tmpdir=/opt/hadoop/yarn/nodemanager/local/usercache/root/appcache/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001/tmp -Dlog4j.configuration=container-log4j.properties -Dyarn.app.container.log.dir=/opt/hadoop/yarn/nodemanager/log/application_1667473260420_0002/container_e02_1667473260420_0002_01_000001 -Dyarn.app.container.log.filesize=0 -Dhadoop.root.logger=INFO,CLA -Dhadoop.root.logfile=syslog -Xmx1024m org.apache.hadoop.mapreduce.v2.app.MRAppMaster
注意:整个脚本调用是比较简单,其逻辑也是很清晰的,但这里涉及到的一个知识点是:运用了不同方式来调用脚本(程序),会涉及到是否创建子进程。
假设有一个程序(假设取名main),程序内部什么也做,就死循环睡眠,然后在另外一个脚本(假设取名executor.sh)以下面几种方式调用该程序,来看看分别会创建几个进程?
方式1:直接调用该程序
#!/bin/bash
./main
方式2:通过exec的方式
#!/bin/bash
exec /bin/bash "./main"
方式3:通过exec的方式,并重定向标准输出
#!/bin/bash
exec /bin/bash "./main 1>tmp.log 2>&1"
上面三种方式的答案分别是2、1、2。
首先,在shell中,执行任何一个命令(程序)都是以创建一个新进程的方式来运行的。因此在方式1中,一共有两个进程,一个是"executor.sh"脚本自身的进程,另外一个是运行main程序的进程。
[root@localhost dockerfile]# pstree -sp 65995
systemd(1)───sshd(71917)───sshd(225687)───bash(226675)───sh(65994)───main(65995)
[root@localhost dockerfile]# ps -efww | grep 65994
root 65994 226675 0 08:32 pts/1 00:00:00 sh executor.sh
root 65995 65994 0 08:32 pts/1 00:00:00 ./main
其次,exec并不启动新的进程,而是用将要被执行的命令(程序)来替换当前的shell进程,然后将原有进程的环境变量全部清除,并且在exec之后的命令均不会再执行。因此在方式2中,只有一个进程。
[root@localhost dockerfile]# pstree -sp 70995
systemd(1)───sshd(71917)───sshd(225687)───bash(226675)───main(70995)
[root@localhost dockerfile]# ps -efww | grep 70995
最后,对于标准输出重定向,总是会创建一个新的进程出来,不管是否采用exec的方式。因此,对于第三种方式而言,也是会创建两个进程。
[root@localhost dockerfile]# pstree -sp 74781
systemd(1)───sshd(71917)───sshd(225687)───bash(226675)───bash(74780)───main(74781)
[root@localhost dockerfile]# ps -efww | grep 74780
root 74780 226675 0 08:35 pts/1 00:00:00 /bin/bash -c ./main 1>tmp.log 2>&1
root 74781 74780 0 08:35 pts/1 00:00:00 ./main
【yarn application kill命令的】
1、命令的执行流程
执行yarn任务kill命令时,本质上是向resourcemanager发送了一个rpc请求。
rm收到请求后内部会进行一些判断及处理,比如判断任务是否存在、application是否已经调度等,对于正在运行的application,最终会通过心跳告知NM需要进行清除。
NM通过定时心跳从RM得到需要清理的container,内部也会进行一系列判断和处理,(对于DefaultContainerExecutor而言)最终处理方式是对需要清除的container,为对应的进程(`default_container_executor_session.sh`)执行kill动作。
2、kill过程中的处理细节
在Nodemanager执行Kill的过程中,有两个细节需要注意:
1)kill进程的时候,是从pid文件里读取进程的pid,然后执行kill动作。但是pid文件里仅记录了"default_container_executor_session.sh"进程对应的pid,而真正的java程序的进程pid则没有记录。如果仅仅只是对shell进程进行kill,那么,java程序进程依旧会继续运行,但父进程的pid变为nodemanager。这显然是不符合逻辑的。因此,走读源码发现,使用“kill -SIGNAL -- -$gpid”的方式对整个进程组中的进程进行kill。这样保证可以将相关的进程一并删除。
例如通过strace抓取NM及其子进程的系统调用,可以观察到对应的动作:
2)kill进程的时候,会分两阶段进行。一次是"kill -15",即发送"TERM"信号;一次是"kill -9",即发送"KILL"信号。两次kill之间的间隔由配置项"yarn.nodemanager.sleep-delay-before-sigkill.ms"决定。而这么做的意义在于,第一次发送TERM信号,让AM有机会捕获该信号,进行相应的清理动作,比如清除在HDFS中上传的资源文件。第二次发送KILL信号,则是确保对应的进程强制结束。
看到这里,再回顾文章开始提到的问题,想必大家也都能分析出其中的原因来了。
小结一下,本文总结了yarn中一个container涉及的linux进程,并穿插介绍了shell命令调用与进程创建之间的关系。最后对yarn任务kill动作的执行流程进行了简单梳理,尤其是nm中的一些小细节。这里主要是针对DefaultContainerExecutor的情况,对于LinuxContainerExecutor和DockerContainerExecutor会稍有不同,后续有时间再来分析。有兴趣的也可以一起来交流。