【Python】Subprocess超时杀干净子进程【原创】.md

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"

扫描二维码关注公众号,回复: 9855589 查看本文章

发现有很多的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)

发布了64 篇原创文章 · 获赞 47 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/jiandanokok/article/details/104853593