【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"

發現有很多的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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章