我们的等待循环自己等了 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 退出且没有错误消息。这看起来像是引号问题,我们第一次也这么认为。
从九月到十月底,我们至少十几次栽进了其中一种坑里:该重启却没重启、某个循环卡住一项检查长达三个半小时、另一个循环卡住翻译队列 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 单元工作:从不通过命令行查找进程。曾让我们吃亏的,正是那些我们手动输入的循环。