wrep / daemonizable-command
为 Symfony 提供的守护进程(无限运行)命令。
Requires
- php: >=8.2
- symfony/console: ^7.0
- symfony/dependency-injection: ^7.0
Requires (Dev)
- ext-pcntl: *
- ext-posix: *
- phpunit/phpunit: ^9.0
Suggests
- symfony/filesystem: If you can't use Upstart or systemd
README
一个小型包,用于使用 Symfony 创建无限运行的命令。
这些无限运行的命令与 Upstart 或 systemd 等工具结合使用,非常容易实现守护进程化。
为什么需要这个?
因为你想要创建长时间运行的 PHP/Symfony 进程!例如,用于发送带有大附件的邮件、处理(延迟)付款或生成大型 PDF 报告。它们查询数据库或从消息队列中读取并执行任务。这个包使得将此类进程作为 Symfony 命令创建变得非常容易。
如何安装?
使用 composer 将其包含到你的 Symfony 项目中
composer require wrep/daemonizable-command
使用哪个版本?
Symfony 进行了一些破坏性更改,因此请确保使用兼容的包版本
- 版本 5.* 用于 Symfony 7 及以上版本
- 版本 4.* 用于 Symfony 6
- 版本 3.0.* 用于 Symfony 4 和 5
- 版本 2.0.* 用于 Symfony 3
- 版本 1.3.* 用于 Symfony 2.8+
如何使用?
只需创建一个继承自 EndlessCommand
的 Symfony 命令即可。以下是一个最小示例
namespace Acme\DemoBundle\Command; use Wrep\Daemonizable\Command\EndlessCommand; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputInterface; class MinimalDemoCommand extends EndlessCommand { // This is just a normal Command::configure() method protected function configure() { $this->setName('acme:minimaldemo') ->setDescription('An EndlessCommand implementation example'); } // Execute will be called in a endless loop protected function execute(InputInterface $input, OutputInterface $output) { // Tell the user what we're going to do. // This will be a NullOutput if the user doesn't want any output at all, // so you don't have to do any checks, just always write to the output. $output->write('Updating timestamp... '); // Do some work file_put_contents( '/tmp/acme-timestamp.txt', time() ); // Tell the user we're done $output->writeln('done'); } }
通过 php app/console acme:minimaldemo
运行。
还有一个包含所有功能的示例,它提供了最佳实践和基本操作的概述。
如何实现守护进程化?
好吧,现在我们有一个在 前台 运行的无限运行命令。这对于调试很有用,但在生产中毫无用处!那么我们如何将这个家伙变成真正的守护进程呢?
你应该使用 systemd 来守护进程化命令。它们提供了非常健壮的守护进程化,重启时启动守护进程,并监控进程以便在崩溃时尝试重启。
如果你不能使用 Upstart 或 systemd,可以使用 .lock
文件和 LockHandler 以及 crontab 中的启动脚本,每分钟启动一次。
有一个Upstart 脚本示例,将你的脚本放置在 /etc/init/
中,并使用 start example-daemon
启动守护进程。该 .conf
文件的名称将是守护进程的名称。尚未提供 systemd 示例,但这应该不难找出。
命令行选项
默认提供了一些选项,以便使生活变得更加轻松
- 使用
-q
来抑制所有输出 - 使用
--run-once
来只运行一次命令,这对于调试很有用 - 使用
--detect-leaks
来在每次运行后打印内存使用报告,更多信息请参阅下一节
内存使用和泄漏
对于长时间运行的进程,内存使用非常重要。Symfony 不是最小的框架之一,如果在你的执行方法中泄漏了一些内存,你的守护进程将会崩溃!已经对 EndlessCommand
类进行了内存泄漏检查,但你应该检查自己的代码。
如何防止泄漏?
始终以-e prod --no-debug
标志开始您的命令。这将禁用Symfony的所有调试功能,这些功能会消耗越来越多的内存。
请确保在execute
方法中进行清理,确保您不会在每次迭代时向数组追加数据,或者例如留下套接字/文件句柄打开。
如果您在Monolog中使用交叉手指的处理程序,这也将是内存泄漏的来源。这个处理程序的思路是保留所有低于阈值的日志条目在内存中,只有在超过阈值的条目出现时才刷新。只要您在execute
方法的末尾手动刷新它,您仍然可以使用交叉手指处理程序。
foreach ($this->getContainer()->get('logger')->getHandlers() as $handler)
{
if ($handler instanceof FingersCrossedHandler) {
$handler->clear();
}
}
检测内存泄漏
使用--detect-leaks
标志运行您的命令。请记住,调试模式会消耗内存,因此您需要以-e prod --no-debug --detect-leaks
运行以获得准确的报告。
在每个迭代后,您的控制台都会打印出这样的内存报告
== MEMORY USAGE ==
Peak: 30038.86 KByte stable (0.000 %)
Cur.: 29856.46 KByte stable (0.000 %)
前三次迭代可能在内存使用方面不稳定,但之后应该稳定。即使是轻微的内存使用增加,随着时间的推移也会导致您的守护程序崩溃!
如果您看到增加/稳定/减少循环,您可能很安全。可能是垃圾回收器没有清理,您可以通过使用unset来清理内存来修复此问题。
破除一些神话
调用gc_collect_cycles()
并不能帮助解决泄漏。PHP会自行在适当的时间清理内存,调用此方法可能会减慢泄漏的内存,但不会解决问题。这也使得找到泄漏更加困难,所以请尽量不要使用它。
如果您在生产模式下以非调试模式运行Symfony,它不会泄漏内存,您也不需要禁用任何SQL记录器。我遇到过的唯一泄漏是上面提到的MonologBundle中的泄漏。
与Doctrine一起工作
由于EndlessContainerAwareCommand在每个迭代后清除Doctrine的EntityManager,请注意这一点。您可以通过覆盖finishIteration()来避免这种行为,但那时您必须自己处理EM。