我們的等待迴圈自己等了自己 35 小時:透過 SSH 使用 pgrep -f 與 pkill -f
維運
10 月 31 日,我們透過 SSH 啟動了一個小型迴圈:等待另一台機器上的翻譯工作完成,然後執行後續工作。翻譯工作當晚就完成了。但當我們兩天後再次查看時,迴圈已經等待了 35 小時,而後續工作從未執行。
迴圈的內容
ssh build-host 'while pgrep -f "i18n_job.py example.com" >/dev/null; do sleep 30; done; ./follow-up.sh'
pgrep -f 會將模式與每個程序的完整命令列進行比對。SSH 會將引號內的命令以 bash -c 'while pgrep -f "i18n_job.py example.com" …' 的方式執行,而該命令列中包含此模式。pgrep 會排除自己的程序,但不會排除啟動它的 shell。因此,只要迴圈還活著,它就會找到匹配項:也就是它自己。
它的孿生兄弟:pkill -f 會殺掉它所執行的 shell
同樣的情況也會發生在 pkill -f 上,而且結果更糟:
ssh build-host 'pkill -f "worker.py --queue a"; nohup python3 worker.py --queue a &'
shell 自己的命令列中包含此模式,因此 pkill 會殺掉 shell。worker 被停止且從未再次啟動,而 SSH 會以錯誤碼 255 退出,且沒有錯誤訊息。這看起來像是引號問題,我們第一次也是這麼認為。
從 9 月到 10 月底,我們至少十幾次掉入其中一種情況:從未發生的重啟、一個卡住檢查達三個半小時的迴圈、另一個卡住翻譯佇列達 50 分鐘的迴圈,以及這次的情況。
我們現在的做法
- 在模式中使用方括號:
pgrep -f "[i]18n_job.py example.com"。作為正規表示式,[i]會匹配字母 i,因此工作仍然匹配;shell 的命令列中包含字元[i],而該表示式不會匹配它。只有當該行其他地方沒有出現純模式時才有幫助:同一行後面的重啟命令會再次匹配。 - 在獨立的 SSH 呼叫中執行查找、停止與啟動。 第一次呼叫使用
pgrep -af列出程序,第二次殺掉它印出的 PID,第三次啟動新程序。 - 等待 PID,而非模式:
tail --pid=1234 -f /dev/null,或while kill -0 1234; do sleep 30; done。 - 更好的做法是,讓 systemd 負責管理任務。將其作為單元執行,並使用
systemctl is-active查詢狀態,或使用systemd-run --wait啟動。這樣就不會弄錯模式。 - 每次等待都設定截止時間(
timeout 3h),並說明何時放棄。沒有截止時間的迴圈會靜默失敗。
Factory Floor 是驅動我們機器的套件,透過 systemd 單元運作:它從不透過命令列尋找程序。讓我們吃虧的迴圈,正是那些我們手動輸入的迴圈。