解决容器内运行laravel定时任务导致大量进程的问题

起因是内网服务器每隔一阵就会失去网络连接,而且外接键鼠也会没有反应。那必定是什么程序占用了大量系统资源,想想只有近期部署的horizon和schedule服务可能出现这个问题。直觉以为php artisan schedule:run运行结束后就退出了,没有副作用。我便关注起horizon服务了,这反而让我踩了坑,因为僵尸进程恰恰就是schedule服务间接造成的。还是要经常打破思维惯性,常规之外总有意外。

在此说明下schedule服务的部署方式,方便大家与自身场景比对。laravel schedule中定义的任务都是runInBackground模式。schedule服务是以容器内部的定时来运行的,即crond是容器的init进程(pid 0),它根据我定义的crontabs文件,进一步调用schedule命令。直接造成僵尸进程的“罪魁祸首”便是有些无辜的crond了。我仿佛听到它一脸无辜地申辩:我不知道啊!言归正传,下面记录了僵尸进程的发现和解决方法。

定位问题

top命令中的zombie是僵尸进程的数量。

以下是ps查看僵尸进程的命令。

ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

ps或者top命令查看系统进程,发现php的很多僵尸进程。通过pstree命令分析,发现这些僵尸进程都跟在crond进程下。很显然,是schedule服务产生了问题。

在测试crond功能的时候,我设置了一个每分钟运行的date命令。由于date命令运行正常,且没有变成僵尸进程,我便排除了crond的问题。

同时,我也注意到schedule:run命令在执行后立即退出了,但是相应的runInBackground的命令还在由相关的php进程运行。因此可以判定,schedule在调度相应的任务后,主进程退出了,子进程继续执行相关命令。在了解了僵尸进程的形成原因后,初步判定这些子进程就是僵尸进程的前身了。

于是,我把runInBackground任务的个数与产生的僵尸进程数量对比,进一步证实僵尸进程就是运行结束但是没有被销毁的schedule子进程。

解决问题

不得不说,国内的技术氛围比较差,百度几乎搜不到有效信息,尤其是百度权重很高的CSDN。在谷歌搜索laravel schedule zombie等关键词后,找到了github上的issueZombie processes when using the runInBackground method in the schedule.

第一次找到这个issue的时候,我陷入了一个误区,认为schedule命令不产生副作用,运行后退出了,子进程运行后也会直接退出。不过还是尝试了issue中最简单的两个方案,@BetaJasonpcntl_waitpid(-1, $status, WNOHANG);@maccup--init。这两个方案并没有解决我的问题。

结合僵尸进程形成的原因,以及“waitpid”等关键词关键词推测:schedule主进程退出后不会再等待子进程结束的信号从而销毁子进程。事实上主进程调度好任务后就完全退出了,不可能再等待子进程。这些子进程就变成了孤儿进程,被最终crond接收了。然而,作为容器主进程的crond并非合格的init进程,它不知道这些子进程的存在,不会主动销毁这些子进程。最终,这些子进程变成了僵尸进程。

schedule主进程调度完成后直接退出是符合预期的,因此考虑让init进程(pid 0)接收这些孤儿进程并负责销毁他们,正好尝试一下@fabriciojstini的方案。然而,tini这个项目太古老了,很久没有维护。我想到了之前用到了s6-overlay这个项目,它发挥了类似supervisor的作用,作为容器的主进程来管理其他服务。尝试了一下,果然不再产生僵尸进程了。

后来,我在s6-overlay项目的首页找到这段文字:

You'll never have zombie processes hanging around in your container, they will be properly cleaned up.

原来,答案一直在眼皮底下。

问题总结

随着工作和生活压力的增长,我的精力被消耗得所剩无几,已经没有太多精力仔细查看各种资料,学习知识和解决问题了。从遇到问题,到解决问题,实际上经过了两个多月。

当这个问题真正解决的时候,我心里有着几分骄傲和遗憾。骄傲的是,作为一个非科班出生的程序员,我确实掌握了一些知识,也许超过很多人了。遗憾的是,随着php的衰落,这个issue的经验可能不会被更多的人看到了,同时我与IT工作也越来越遥远了。带着这份骄傲和遗憾,我在这个issue下认真的发布了我的评论

init(pid 0, 比如crond)进程接管了schedule主进程(php artisan schedule:run)遗留的shedule子进程(runInBackground,比如 php artisan command-name),却不主动负责销毁这些子进程。主进程并不会等待并销毁runInBackground的子进程,于是这些子进程就变成了僵尸进程。
schedule主进程不等待并销毁是间接原因,init进程不负责销毁是直接原因。前者的行为是符合预期的,如果后者是crond,那也是符合预期的。但可以把后者替换成具备销毁孤儿进程能力的程序,比如s6 init。然后通过s6启动crond。
在此推荐使用 s6-overlay


The init process (PID 0, such as crond) assumes control over child processes left behind by the primary scheduling process (php artisan schedule:run), which are designed to run in the background (like php artisan command-name with runInBackground).
However, init does not actively take on the responsibility of terminating these child processes. The main scheduling process does not wait for and destroy these background-running child processes, causing them to become zombie processes.
The lack of waiting and cleanup by the primary scheduling process is an indirect cause, while init's failure to handle their destruction is the direct reason. The former's behavior aligns with expectations; if the latter is indeed crond, this too is expected behavior. Nonetheless, an alternative solution could involve replacing init with a program capable of reaping orphaned processes, such as s6 init. Then start crond with s6.
It is therefore recommended to utilize s6-overlay for addressing this issue.

我确实是个喜欢技术的普通人。

参考文章

posted @   幸福的路痴  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示