1.1 踩坑案例
踩坑的程式是個常駐的Agent類管理程式, 包括但不限於如下型別的任務在執行:
- a. 多執行緒的網路通訊包處理
- 和控制Master節點互動
- 有固定Listen埠
- b. 定期作業任務, 通過subprocess.Pipe執行shell命令
- c. etc
發現坑的過程很有意思:
- a.重啟Agent發現Port被佔用了
- => 立刻想到可能程式沒被殺死, 是不是停止指令碼出問題
- => 排除發現不是, Agent程式確實死亡了
- => 通過
netstat -tanop|grep port_number
發現埠確實有人佔用
- => 除錯環境, 直接殺掉佔用程式了之, 錯失首次發現問題的機會
- => 立刻想到可能程式沒被殺死, 是不是停止指令碼出問題
- b.問題在一段時間後重現, 重啟後Port還是被佔用
- 定位問題出現在一個叫做xxxxxx.sh的指令碼, 該指令碼佔用了Agent使用的埠
- => 奇了怪了, 一個xxx.sh指令碼使用這個奇葩Port幹啥(大於60000的Port, 有興趣的磚友可以想下為什麼Agent預設使用6W+的埠)
- => review該指令碼並沒有進行埠監聽的程式碼
- 定位問題出現在一個叫做xxxxxx.sh的指令碼, 該指令碼佔用了Agent使用的埠
- 一拍腦袋, c.程式共享了父程式資源了
- => 溯源該指令碼,發現確實是Agent啟動的任務中的指令碼之一
- => 問題基本定位, 該指令碼屬於Agent呼叫的指令碼
- => 該Agent繼承了Agent原來的資源FD, 也就是這個port
- => 雖然該指令碼由於超時被動觸發了terminate機制, 但terminate並沒有幹掉這個子程式
- => 該指令碼程式的父程式(ppid) 被重置為了1
- d.問題出在指令碼程式超時kill邏輯
1.2 填坑解法
通過程式碼review, 找到shell具體執行的庫程式碼如下:
self._subpro = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=_signal_handle
)
# 重點是shell=True !
把上述程式碼改為:
self._subpro = subprocess.Popen(
cmd.split(), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, preexec_fn=_signal_handle
)
# 重點是去掉了shell=True
1.3 坑位分析
Agent會在一個新建立的threading執行緒中執行這段程式碼, 如果執行緒執行時間超時(xx seconds), 會呼叫 self._subpro.terminate()
終止該指令碼.
表面正常:
- 啟用新執行緒執行該指令碼
- 如果出現問題,執行超時防止hang住其他任務執行呼叫terminate殺死程式
深層問題:
- Python 2.7.x中subprocess.Pipe 如果shell=True, 會預設把相關的pid設定為shell(sh/bash/etc)本身(執行命令的shell父程式), 並非執行cmd任務的那個程式
- 子程式由於會複製父程式的opened FD表, 導致即使被殺死, 依然保留了擁有這個Listened Port FD
這樣雖然殺死了shell程式(未必死亡, 可能進入defunct狀態), 但實際的執行程式確活著. 於是1.1
中的坑就被結實的踩上了.
1.4 坑後擴充套件
1.4.1 擴充套件知識
本節擴充套件知識包括二個部分:
- Linux系統中, 子程式一般會繼承父程式的哪些資訊
- Agent這種常駐程式選擇>60000埠的意義
擴充套件知識留到下篇末尾講述, 感興趣的可以自行搜尋
1.4.1 技術關鍵字
- Linux系統程式
- Linux隨機埠選擇
- 程式多執行緒執行
- Shell執行
1.5 填坑總結
- 子程式會繼承父程式的資源資訊
如果只kill某程式的父程式, 整合了父程式資源的子程式會繼續佔用父程式的資源不釋放, 包括但不限於
- listened port
- opened fd
- etc
Python Popen使用上, shell的bool狀態決定了程式kill的邏輯, 需要根據場景選擇使用方式