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)