将PHP脚本作为守护进程运行

171

我需要将一个php脚本作为守护进程运行(等待指令并执行操作)。cron作业无法满足我的需求,因为需要在指令到达时立即执行操作。我知道由于内存管理问题,PHP不是实现守护进程的最佳选择,但由于某些原因,在这种情况下我必须使用PHP。我发现了一个名为Daemon的libslack工具(http://libslack.org/daemon),它似乎可以帮助我管理守护进程,但是在过去的5年中没有任何更新,所以我想知道您是否知道其他适合我情况的替代品。非常感谢您提供的任何信息。


相关链接:https://dev59.com/pG445IYBdhLWcg3wustT - user212218
1
我看到了这篇文章http://gonzalo123.com/2010/05/23/building-network-services-with-php-and-xinetd/,我相信它既轻松又稳定。 - Teson
使用 systemd 做这件事情非常容易。 - LeonanCarvalho
15个回答

177
您可以使用命令行(即bash)启动您的PHP脚本,方法是使用以下命令:nohup php myscript.php &,其中&将进程放入后台。
编辑:
是的,确实存在一些缺点,但无法控制?这是错误的。一个简单的kill processid将停止它。而且这仍然是最好和最简单的解决方案。

1
如果终端存在,进程将不会退出。这就是为什么有“nohup”命令的原因。我已经使用一个PHP脚本作为守护进程在所有服务器上运行了多年。可能有更好的解决方案,但这是最快的方法。 - CDR
31
如果守护进程失败,这不会重新启动它,并且没有简单的方法来管理该守护进程。 - Phil Wallach
6
我同意这里所说的内容--这是一个糟糕的解决方案。你应该创建一个初始化脚本,有几个原因:1)初始化脚本会在启动时自动运行;2)你可以使用启动/停止/重启命令来管理守护进程。这里有一个来自servefault的示例:http://serverfault.com/questions/229759/launching-a-php-daemon-from-an-lsb-init-script-w-start-stop-daemon - Simian
1
嘿,大家好...在我看来,nohup&似乎做的是同样的事情:将启动的进程与当前shell实例分离。我为什么需要它们两个?我不能只用php myscript.php &或者nohup myscript.php吗?谢谢。 - nourdine
1
如果脚本通过echo或var_dump写入stdout,则可以使用类似于以下的日志文件捕获此信息:nohup php myscript.php > myscript.log & - Mischa
显示剩余2条评论

176

另一种选择是使用Upstart。它最初是为Ubuntu开发的(并默认随其打包),但旨在适用于所有Linux发行版。

这种方法类似于Supervisorddaemontools,因为它会在系统启动时自动启动守护进程,并在脚本完成后重新启动。

设置方法:

/etc/init/下创建一个新的脚本文件myphpworker.conf。以下是一个示例:

# Info
description "My PHP Worker"
author      "Jonathan"

# Events
start on startup
stop on shutdown

# Automatically respawn
respawn
respawn limit 20 5

# Run the script!
# Note, in this example, if your PHP script returns
# the string "ERROR", the daemon will stop itself.
script
    [ $(exec /usr/bin/php -f /path/to/your/script.php) = 'ERROR' ] && ( stop; exit 1; )
end script

启动和停止您的守护进程:

sudo service myphpworker start
sudo service myphpworker stop

检查您的守护进程是否正在运行:

sudo service myphpworker status

感谢

非常感谢Kevin van Zonneveld,从他那里学到了这个技巧。


2
喜欢这个。只是想知道,是否可以拥有多个并发的工作线程?我现在遇到的问题是一个工作线程已经不够用了。 - Manuel
2
Sudo "service myphpworker start" 对我没用。我使用了"sudo start myphpworker",它完美地工作了。 - Matt Sich
1
在执行 "sudo service myphpworker start" 时,它显示 myphpworker:未识别的服务。 - Pradeepta
3
这是由于帖子中存在错误-我不确定是什么(也没有测试过),但我认为sudo service myphpworker start/stop/status只适用于在/etc/init.d目录下的服务,而不适用于upstart服务。@matt-sich似乎已经找到了正确的语法。另一个选项是使用Gearman或Resque,它允许后台处理和守护进程化。 - ckm
3
Ubuntu正在转向使用systemd而不是upstart:http://www.zdnet.com/article/after-linux-civil-war-ubuntu-to-adopt-systemd/ - Kzqai
显示剩余6条评论

105
使用新的systemd,您可以创建一个服务。
您必须在/etc/systemd/system/中创建一个文件或符号链接,例如myphpdaemon.service,并放置像这样的内容,myphpdaemon将是服务的名称:
[Unit]
Description=My PHP Daemon Service
#May your script needs MySQL or other services to run, eg. MySQL Memcached
Requires=mysqld.service memcached.service 
After=mysqld.service memcached.service

[Service]
User=root
Type=simple
TimeoutSec=0
PIDFile=/var/run/myphpdaemon.pid
ExecStart=/usr/bin/php -f /srv/www/myphpdaemon.php arg1 arg2> /dev/null 2>/dev/null
#ExecStop=/bin/kill -HUP $MAINPID #It's the default you can change whats happens on stop command
#ExecReload=/bin/kill -HUP $MAINPID
KillMode=process

Restart=on-failure
RestartSec=42s

StandardOutput=null #If you don't want to make toms of logs you can set it null if you sent a file or some other options it will send all PHP output to this one.
StandardError=/var/log/myphpdaemon.log
[Install]
WantedBy=default.target

您可以使用以下命令来启动、获取状态、重新启动和停止服务: systemctl <start|status|restart|stop|enable> myphpdaemon 您也可以使用PHP本地服务器,命令为:php -S 127.0.0.1:<port>,或者将其作为脚本运行。如果使用PHP脚本,您需要编写一个“无限循环”以保持服务持续运行。
<?php
gc_enable();//
while (!connection_aborted() || PHP_SAPI == "cli") {
  
  //Code Logic
  
  //sleep and usleep could be useful
    if (PHP_SAPI == "cli") {
        if (rand(5, 100) % 5 == 0) {
            gc_collect_cycles(); //Forces collection of any existing garbage cycles
        }
    }
}

工作示例:

[Unit]
Description=PHP APP Sync Service
Requires=mysqld.service memcached.service
After=mysqld.service memcached.service

[Service]
User=root
Type=simple
TimeoutSec=0
PIDFile=/var/run/php_app_sync.pid
ExecStart=/bin/sh -c '/usr/bin/php -f /var/www/app/private/server/cron/app_sync.php  2>&1 > /var/log/app_sync.log'
KillMode=mixed

Restart=on-failure
RestartSec=42s

[Install]
WantedBy=default.target

如果您的PHP例程应该在循环中执行一次(例如挖掘),则可以使用shell或bash脚本来代替直接调用PHP,例如:

#!/usr/bin/env bash
script_path="/app/services/"

while [ : ]
do
#    clear
    php -f "$script_path"${1}".php" fixedparameter ${2}  > /dev/null 2>/dev/null
    sleep 1
done

如果您选择了这些选项,您应该将KillMode更改为mixed,以便进程、bash(main)和PHP(child)被杀死。
ExecStart=/app/phpservice/runner.sh phpfile parameter  > /dev/null 2>/dev/null
KillMode=process

如果你遇到了内存泄漏问题,这种方法也很有效。

注意:每次更改“myphpdaemon.service”时,必须运行`systemctl daemon-reload’,但是如果您不这样做,当需要时会发出警报。


14
被低估的回答。你得到了我的一票支持。 - Gergely Lukacsy
2
太棒了。希望我们也能点赞答案,因为这应该不会被埋在这个页面中。 - Justin
1
你应该检查 systemctl status <your_service_name> -l 的输出,它会给你一个线索,告诉你正在发生什么。 - LeonanCarvalho
1
@LeandroTupone,为了防止失败,您应该引用PHP服务器所依赖的任何服务,但这并非必需。 - LeonanCarvalho
3
既然现在已经是2019年,这篇回答应该真正取代原先被接受的答案。 - spice
显示剩余18条评论

49
如果可以的话,获取UNIX环境高级编程的副本。整个第13章节都专门讲解守护进程编程。示例使用C语言编写,但是所有你需要的函数在PHP中都有封装(基本上是pcntlposix扩展)。
简单来说,编写守护进程(仅适用于*nix操作系统,Windows使用服务)就像这样:
  1. 调用 umask(0) 避免权限问题。
  2. fork() 并让父进程退出。
  3. 调用 setsid()
  4. 设置 SIGHUP 信号处理(通常忽略或用于向守护进程发送重新加载配置的信号)和 SIGTERM 信号处理(告诉进程优雅地退出)。
  5. fork() 再次并让父进程退出。
  6. 使用 chdir() 更改当前工作目录。
  7. fclose() stdinstdoutstderr 并不写入它们。正确的方法是将它们重定向到 /dev/null 或文件,但我找不到在 PHP 中如何实现。当启动守护进程时,可以使用 shell 重定向它们(你需要自己找出如何做到这一点,我不知道 :)
  8. 完成你的工作!

另外,由于您正在使用PHP,请注意循环引用,因为在PHP 5.3之前的版本中,PHP垃圾回收器无法收集这些引用,进程将内存泄漏,直到最终崩溃。


1
谢谢提供这些信息。看起来libslack的守护进程程序基本上完成了你提到的所有准备工作。我想现在我会坚持使用它,直到我找到其他好的替代方案。 - Beier
1
发现了这篇文章,期望能够复制粘贴代码到一个糟糕的旧应用程序中,但是它无法关闭标准输入等,感到失望。 :p - ThiefMaster
1
为什么要再次使用(5) fork()? - TheFox
做得好 - 做得很棒! - Gautam Sharma
对于未来的读者询问为什么要进行两次fork的原因:https://dev59.com/MnNA5IYBdhLWcg3wpfmu -- 简而言之:防止僵尸进程。 - Ghedipunk
@Ghedipunk,我看了你发布的链接中的一些答案,可以说双重 fork 也用于防止第二个子进程打开 TTY 终端。如果你只进行一次 fork,它也会防止僵尸进程,因为第一个子进程将成为 init 的子进程,并将在 init 的定期 wait 系统调用执行时得到清理。 - tonix

25

我运行了大量的PHP守护进程。

我同意你的观点,即PHP不是用于此的最佳(甚至是好的)语言,但是这些守护进程与面向Web的组件共享代码,因此总体而言,这对我们来说是一个不错的解决方案。

我们使用daemontools来实现这一点。它很聪明,干净且可靠。事实上,我们使用它来运行所有的守护进程。

您可以在http://cr.yp.to/daemontools.html上查看。

编辑:功能列表如下。

  • 自动启动守护进程
  • 宕机自动重启守护进程
  • 日志处理,包括轮换和修剪
  • 管理接口:“svc”和“svstat”
  • 友好的UNIX系统支持(对于某些人来说可能不是优点)

还可以从存储库中安装,例如在apt中! - Kzqai

15
你可以:
  1. 像 Henrik 建议的那样使用 nohup
  2. 使用 screen 并将你的 PHP 程序作为常规进程在其中运行。这比使用 nohup 更加灵活。
  3. 使用一个守护进程,例如http://supervisord.org/(它是用 Python 写的,但可以守护任何命令行程序并提供远程控制来管理它)。
  4. 编写自己的守护进程包装器,像 Emil 建议的那样,但我认为这是过度设计了。
我建议使用最简单的方法(在我看来就是 screen),如果你想要更多特性或功能,再转到更复杂的方法。

你能提供一个类似的supervisord配置吗? - Alix Axel

13

解决这个问题的方法不止一种。

我不知道具体情况,但也许有另一种触发PHP进程的方式。例如,如果您需要根据SQL数据库中的事件运行代码,则可以设置触发器来执行脚本。这在PostgreSQL下非常容易实现:http://www.postgresql.org/docs/current/static/external-pl.html

老实说,我认为最好的办法是使用nohup创建守护进程。 nohup允许命令在用户注销后继续执行:

nohup php myscript.php &

然而有一个非常严重的问题。正如你所说,PHP的内存管理器非常糟糕,它是建立在假设脚本只执行几秒钟然后退出的基础上的。你的PHP脚本在仅仅几天之后就会开始使用千兆字节的内存。你还必须创建一个定时脚本,每12或24个小时运行一次,像这样终止和重新启动你的PHP脚本:

killall -3 php
nohup php myscript.php &

但是,如果脚本正在执行任务呢?kill -3 是中断命令,就像在 CLI 上使用 ctrl+c 一样。您的 PHP 脚本可以使用 PHP pcntl 库捕获此中断并优雅地退出:http://php.oregonstate.edu/manual/en/function.pcntl-signal.php

以下是示例:

function clean_up() {
  GLOBAL $lock;
  mysql_close();
  fclose($lock)
  exit();
}
pcntl_signal(SIGINT, 'clean_up');

$lock的背后思想是PHP脚本可以使用fopen("file","w");打开文件。同一时间只有一个进程能够在文件上获取写锁,因此使用$lock可以确保只运行一个副本的PHP脚本。

祝好运!


11

7

4

我最近需要解决在多个平台(Windows、Mac和Linux)上运行PHP脚本作为守护进程的问题。我通过编写自己的基于C++的解决方案并制作二进制文件来解决这个问题:

https://github.com/cubiclesoft/service-manager/

完全支持Linux(通过sysvinit),同时也支持Windows NT服务和Mac OSX launchd。

如果您只需要Linux,则此处介绍的其他解决方案中的一些都足够好,具体取决于版本。现在还有Upstart和systemd,它们具有回退到sysvinit脚本的功能。但是使用PHP的好处之一是它天生就是跨平台的语言,因此使用该语言编写的代码很有可能在任何地方原样工作。当涉及到某些外部本机操作系统级别的因素(如系统服务)时,缺陷开始显现,但大多数脚本语言都会遇到这个问题。

像某些人在这里建议的那样,在PHP用户空间中捕获信号并不是一个好主意。仔细阅读pcntl_signal()的文档,您将很快了解到PHP使用一些相当不愉快的方法(具体来说是“ticks”)处理信号,这会消耗大量的周期,用于进程很少看到的东西(即信号)。PHP中的信号处理在POSIX平台上也仅仅是基本可用的,并且支持因PHP版本而异。这听起来最初像个不错的解决方案,但它远远不能真正有用。

随着时间的推移,PHP在内存泄漏问题方面也变得越来越好。您仍然需要小心(DOM XML解析器仍然容易泄漏),但我现在很少看到失控的进程,相比以前,PHP错误跟踪器也比较安静。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接