0. 前提
做发布系统的时候,一开始接入的是前端发布任务,前端使用的是Node.js,是需要编译打包的,即需要npm install和npm run xxx等操作
而这两步相对来说是比较耗时的,所以使用Python在执行命令的时候,用的是subprocess库,加了超时自动断开并清理子进程的逻辑
执行命令的方法如下,超时时间默认是十分钟,为什么使用以下方法来执行命令可以参考我的另一篇文章:【命令】Python执行命令超时控制【原创】
import os
import signal
import subprocess
def run_cmd(cmd_string, timeout=600):
"""
执行命令
:param cmd_string: string 字符串
:param timeout: int 超时设置
:return:
"""
p = subprocess.Popen(cmd_string, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True, close_fds=True,
start_new_session=True)
format = 'utf-8'
if platform.system() == "Windows":
format = 'gbk'
try:
(msg, errs) = p.communicate(timeout=timeout)
ret_code = p.poll()
if ret_code:
code = 1
msg = "[Error]Called Error : " + str(msg.decode(format))
else:
code = 0
msg = str(msg.decode(format))
except subprocess.TimeoutExpired:
# 注意:不能使用p.kill和p.terminate,无法杀干净所有的子进程,需要使用os.killpg
p.kill()
p.terminate()
os.killpg(p.pid, signal.SIGUSR1)
# 注意:如果开启下面这两行的话,会等到执行完成才报超时错误,但是可以输出执行结果
# (outs, errs) = p.communicate()
# print(outs.decode('utf-8'))
code = 1
msg = "[ERROR]Timeout Error : Command '" + cmd_string + "' timed out after " + str(timeout) + " seconds"
except Exception as e:
code = 1
msg = "[ERROR]Unknown Error : " + str(e)
return code, msg
1. 问题
从2020年3月11号开始,某个前端发布项目就经常处于发布失败的情况,如下图:
发布失败全部都是因为在发布机上执行npm run sit超时(10分钟)导致的
2. 原因
初步怀疑是代码问题,后来用户使用以前发布成功的tag来发布,也是失败的,那就表示和代码没关系,很有可能是发布系统在执行命令的时候出问题了
登上发布机,查看是否有运行的npm进程:
ps -ef | grep npm | grep -v "grep"
发现并没有运行的npm进程
再查看是否有运行的node进程:
ps -ef | grep node | grep -v "grep"
发现有很多的node进程在运行:
ps -ef | grep node | grep -v "grep"
10525 1 0 23:02 pts/3 00:00:00 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/cross-env BUILD_ENV=sit webpack --progress --config build/webpack.prod.config.js
10532 10525 2 23:02 pts/3 00:00:09 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/webpack --progress --config build/webpack.prod.config.js
10568 10532 0 23:02 pts/3 00:00:00 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
10575 10532 0 23:02 pts/3 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
那问题的原因就很明显了:
发布系统在执行npm run sit命令的时候,由于未知的超时,执行命令的方法干掉了npm子进程,却没有干掉由npm生成的node孙进程
由于后面需要解决这个问题以及为了测试是否解决,我写了一个测试代码来进行测试:
test.py:
import os
import signal
import subprocess
def run_cmd11(cmd_string):
p = os.popen(cmd_string)
x = p.read()
p.close()
return x
def run_cmd(cmd_string, timeout=600):
"""
执行命令
:param cmd_string: string 字符串
:param timeout: int 超时设置
:return:
"""
p = subprocess.Popen(cmd_string, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True, close_fds=True,
start_new_session=True)
try:
(msg, errs) = p.communicate(timeout=timeout)
ret_code = p.poll()
if ret_code:
code = 1
msg = "[Error]Called Error : " + str(msg.decode('utf-8'))
else:
code = 0
msg = str(msg.decode('utf-8'))
except subprocess.TimeoutExpired:
print('超时了,判断是否有npm进程在运行的:')
msg = run_cmd11('ps -ef | grep npm | grep -v "grep"')
print(msg)
print('超时了,判断是否有node进程在运行的:')
msg = run_cmd11('ps -ef | grep node | grep -v "grep"')
print(msg)
print('超时了,获取npm进程号')
msg = run_cmd11("ps -ef | grep npm | grep -v 'grep' | awk '{print $2}'")
print(msg)
print('超时了,获取npm进程的进程树')
msg = run_cmd11("pstree -p " + str(msg))
print(msg)
# 注意:不能使用p.kill和p.terminate,无法杀干净所有的子进程,需要使用os.killpg
p.kill()
p.terminate()
os.killpg(p.pid, signal.SIGUSR1)
# 注意:如果开启下面这两行的话,会等到执行完成才报超时错误,但是可以输出执行结果
# (outs, errs) = p.communicate()
# print(outs.decode('utf-8'))
code = 1
msg = "[ERROR]Timeout Error : Command '" + cmd_string + "' timed out after " + str(timeout) + " seconds"
except Exception as e:
code = 1
msg = "[ERROR]Unknown Error : " + str(e)
return code, msg
try:
print('开始之前,判断是否有npm进程在运行的:')
msg = run_cmd11('ps -ef | grep npm | grep -v "grep"')
print(msg)
print('开始之前,判断是否有node进行在运行的:')
msg = run_cmd11('ps -ef | grep node | grep -v "grep"')
print(msg)
print('开始执行npm run sit')
(code, msg) = run_cmd('npm run sit', timeout=10)
print(msg)
print('结束之后,判断是否有npm进程在运行的:')
msg = run_cmd11('ps -ef | grep npm | grep -v "grep"')
print(msg)
print('结束之后,判断是否有node进行在运行的:')
msg = run_cmd11('ps -ef | grep node | grep -v "grep"')
print(msg)
except Exception as e:
print(str(e))
脚本的步骤:
- 先判断有没有运行的npm和node进程
- 执行npm run sit,超时时间为10秒
- 在超时之后,再来判断有没有运行的npm和node进程,顺便打印npm的进程树
- 杀掉子进程
- 最后判断有没有运行的npm和node进程
由于timeout设置为10秒,即十秒的时间来执行npm run sit,这个是一定会超时的
先来运行一下:
开始之前,判断是否有npm进程在运行的:
开始之前,判断是否有node进行在运行的:
开始执行npm run sit
超时了,判断是否有npm进程在运行的:
root 12365 12324 2 18:21 ? 00:00:00 npm
超时了,判断是否有node进程在运行的:
root 12377 12376 0 18:21 ? 00:00:00 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/cross-env BUILD_ENV=sit webpack --progress --config build/webpack.prod.config.js
root 12384 12377 84 18:21 ? 00:00:07 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/webpack --progress --config build/webpack.prod.config.js
root 12395 12384 29 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
root 12402 12384 31 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
超时了,获取npm进程号
12365
超时了,获取npm进程的进程树
npm(12365)-+-sh(12376)---node(12377)-+-node(12384)-+-node(12395)-+-{node}(12396)
| | | |-{node}(12397)
| | | |-{node}(12398)
| | | |-{node}(12399)
| | | |-{node}(12400)
| | | |-{node}(12401)
| | | |-{node}(12409)
| | | |-{node}(12410)
| | | |-{node}(12411)
| | | `-{node}(12412)
| | |-node(12402)-+-{node}(12403)
| | | |-{node}(12404)
| | | |-{node}(12405)
| | | |-{node}(12406)
| | | |-{node}(12407)
| | | |-{node}(12408)
| | | |-{node}(12413)
| | | |-{node}(12414)
| | | |-{node}(12415)
| | | `-{node}(12416)
| | |-{node}(12385)
| | |-{node}(12386)
| | |-{node}(12387)
| | |-{node}(12388)
| | |-{node}(12389)
| | |-{node}(12390)
| | |-{node}(12391)
| | |-{node}(12392)
| | |-{node}(12393)
| | `-{node}(12394)
| |-{node}(12378)
| |-{node}(12379)
| |-{node}(12380)
| |-{node}(12381)
| |-{node}(12382)
| `-{node}(12383)
|-{npm}(12366)
|-{npm}(12367)
|-{npm}(12368)
|-{npm}(12369)
|-{npm}(12370)
|-{npm}(12371)
|-{npm}(12372)
|-{npm}(12373)
|-{npm}(12374)
`-{npm}(12375)
[ERROR]Timeout Error : Command 'npm run sit' timed out after 10 seconds
结束之后,判断是否有npm进程在运行的:
root 12365 12324 2 18:21 ? 00:00:00 [npm] <defunct>
结束之后,判断是否有node进行在运行的:
root 12377 1 0 18:21 ? 00:00:00 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/cross-env BUILD_ENV=sit webpack --progress --config build/webpack.prod.config.js
root 12384 12377 85 18:21 ? 00:00:07 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/webpack --progress --config build/webpack.prod.config.js
root 12395 12384 29 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
root 12402 12384 31 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
root 12377 1 0 18:21 ? 00:00:00 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/cross-env BUILD_ENV=sit webpack --progress --config build/webpack.prod.config.js
root 12384 12377 85 18:21 ? 00:00:07 [node] <defunct>
root 12395 12384 29 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
root 12402 12384 31 18:21 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
可以看到,npm的进程树里面生成了不少子进程,而在超时之后使用os.killpg来杀掉了npm进程,但是node进程还是在的,即node(12377)包括后面的进程都没有被杀死
注意:残留的node进程可以通过killall node来干掉
3. 解决
查阅了相关文档,初步猜测,可能是以下代码有问题:
p.kill()
p.terminate()
os.killpg(p.pid, signal.SIGUSR1)
即p.kill()和p.terminate()干掉了npm进程,导致后面的os.killpg无法传送信号给npm的子进程让其终止
现在可以通过注释掉这两行来验证以下,即:
# p.kill()
# p.terminate()
os.killpg(p.pid, signal.SIGUSR1)
但是运行之后发现结果还是一样的,同样会保留node进程
再查阅相关文档,参考:
杀死 subprocess.Popen 的子子孙孙
subprocess之preexec_fn
安全开发 | Python Subprocess库在使用中可能存在的安全风险总结
python subprocess.Popen系列问题
即signal.SIGUSR1并不是终止信号,而是用户自定义信号,可以使用终止信号来试一下,修改成:
os.killpg(p.pid, signal.SIGTERM)
运行:
开始之前,判断是否有npm进程在运行的:
开始之前,判断是否有node进行在运行的:
开始执行npm run sit
超时了,判断是否有npm进程在运行的:
root 14262 14221 2 18:33 ? 00:00:00 npm
超时了,判断是否有node进程在运行的:
root 14274 14273 0 18:33 ? 00:00:00 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/cross-env BUILD_ENV=sit webpack --progress --config build/webpack.prod.config.js
root 14281 14274 98 18:33 ? 00:00:09 node /data/zaspace/upload/test/za-app-web/node_modules/.bin/webpack --progress --config build/webpack.prod.config.js
root 14292 14281 18 18:33 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
root 14299 14281 13 18:33 ? 00:00:01 /usr/bin/node /data/zaspace/upload/test/za-app-web/node_modules/thread-loader/dist/worker.js 20
超时了,获取npm进程号
14262
超时了,获取npm进程的进程树
npm(14262)-+-sh(14273)---node(14274)-+-node(14281)-+-node(14292)-+-{node}(14293)
| | | |-{node}(14294)
| | | |-{node}(14295)
| | | |-{node}(14296)
| | | |-{node}(14297)
| | | |-{node}(14298)
| | | |-{node}(14306)
| | | |-{node}(14307)
| | | |-{node}(14308)
| | | `-{node}(14309)
| | |-node(14299)-+-{node}(14300)
| | | |-{node}(14301)
| | | |-{node}(14302)
| | | |-{node}(14303)
| | | |-{node}(14304)
| | | |-{node}(14305)
| | | |-{node}(14310)
| | | |-{node}(14311)
| | | |-{node}(14312)
| | | `-{node}(14313)
| | |-{node}(14282)
| | |-{node}(14283)
| | |-{node}(14284)
| | |-{node}(14285)
| | |-{node}(14286)
| | |-{node}(14287)
| | |-{node}(14288)
| | |-{node}(14289)
| | |-{node}(14290)
| | `-{node}(14291)
| |-{node}(14275)
| |-{node}(14276)
| |-{node}(14277)
| |-{node}(14278)
| |-{node}(14279)
| `-{node}(14280)
|-{npm}(14263)
|-{npm}(14264)
|-{npm}(14265)
|-{npm}(14266)
|-{npm}(14267)
|-{npm}(14268)
|-{npm}(14269)
|-{npm}(14270)
|-{npm}(14271)
`-{npm}(14272)
[ERROR]Timeout Error : Command 'npm run sit' timed out after 10 seconds
结束之后,判断是否有npm进程在运行的:
root 14262 14221 2 18:33 ? 00:00:00 [npm] <defunct>
结束之后,判断是否有node进行在运行的:
再手动运行一下判断有没有运行的npm和node进程:
ps -ef | grep npm | grep -v "grep"
ps -ef | grep node | grep -v "grep"
发现是真的没有残留node进程了
最后来判断有没有僵尸进程,防止杀不彻底变成僵尸:
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
并没有,ok
4. 总结
在subprocess中如果想要杀掉子进程的话,需要使用os.killpg(p.pid, signal.SIGTERM)