长时间运行的php脚本的内存注意事项


Memory considerations for long-running php scripts

我想使用Zend Framework 2控制器在php中为beanstalkd编写一个worker。它通过CLI启动,并将永远运行,像本例一样向beanstalkd请求作业。

在简单的类伪代码中:

while (true) {
    $data   = $beanstalk->reserve();
    $class  = $data->class;
    $params = $data->params;
    $job    = new $class($params);
    $job();
}

$job在这里当然具有__invoke()方法。然而,这些工作中的某些内容可能会运行很长时间。有些可能运行时具有相当大的内存。有些人可能已经注入了$beanstalk对象,以便自己启动新作业,或者有一个Zend'Di'Locator实例来从DIC中提取对象。

从长远来看,我担心这种针对生产环境的设置,因为可能会发生循环引用,而且(目前)我没有明确"执行"任何垃圾收集,而此操作可能会运行数周/月/年*。

*)在豆茎中,reserve是一个阻塞调用,如果没有可用的作业,该工作者将等待豆茎的任何响应。

我的问题:php将如何长期处理这一问题,我应该采取任何特殊的预防措施来防止它被阻塞吗?

我确实考虑过这一点,可能会有所帮助(但如果我错了,请纠正,如果可能的话,请添加更多):

  1. 在开始循环之前使用gc_enable()
  2. 在每次迭代中使用gc_collect_cycles()
  3. 在每次迭代中取消设置$job
  4. $job显式取消设置__destruct()中的引用

(注意:从这里更新)

我确实用任意作业运行了一些测试。我包括的工作是:"简单",只是设定一个值;"longarray",创建一个包含1000个值的数组;"生产者",让循环注入$pheanstalk,并向队列中添加三个simplejob(因此现在有一个从job到beanstall的引用);"locatorware",其中给定Zend'Di'Locator,并实例化所有作业类型(尽管未调用)。我在队列中添加了10000个作业,然后保留了队列中的所有作业。

"simplejob"(每1000个作业的内存消耗,使用memory_get_usage())的结果

0:     56392
1000:  548832
2000:  1074464
3000:  1538656
4000:  2125728
5000:  2598112
6000:  3054112
7000:  3510112
8000:  4228256
9000:  4717024
10000: 5173024

随机挑选一份工作,测量与上述相同。分销:

["Producer"] => int(2431)
["LongArray"] => int(2588)
["LocatorAware"] => int(2526)
["Simple"] => int(2456)

内存:

0:     66164
1000:  810056
2000:  1569452
3000:  2258036
4000:  3083032
5000:  3791256
6000:  4480028
7000:  5163884
8000:  6107812
9000:  6824320
10000: 7518020

上面的执行代码更新为:

$baseMemory = memory_get_usage();
gc_enable();
for ( $i = 0; $i <= 10000; $i++ ) {
    $data = $bheanstalk->reserve();
    $class = $data->class;
    $params = $data->params;
    $job = new $class($params);
    $job();
    $job = null;
    unset($job);
    if ( $i % 1000 === 0 ) {
        gc_collect_cycles();
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "<br>";
    }
}

正如大家所注意到的,php中的内存消耗不是所利用并保持在最低限度,而是随着时间的推移而增加。

我通常会定期重新启动脚本,尽管你不必在每个作业运行后都这样做(除非你愿意,而且清除内存很有用)。例如,您可以一次运行多达100个或更多的作业,或者直到脚本使用了20MB RAM,然后退出脚本,立即重新运行。

我在的博客文章http://www.phpscaling.com/2009/06/23/doing-the-work-elsewhere-sidebar-running-the-worker/有一些重新运行脚本的shell脚本示例。

我最终对我当前的代码基线进行了一行行的基准测试,之后我得到了这个:

$job = $this->getLocator()->get($data->name, $params);

它使用Zend'Di依赖注入,实例管理器在整个过程中跟踪实例。因此,在调用并可以删除作业之后,实例管理器仍然将其保存在内存中。不使用Zend'Di来实例化作业会立即导致静态内存消耗,而不是线性内存消耗。

为了内存安全,不要在PHP中的每个序列作业之后使用循环。但只需创建简单的bash脚本来进行循环:

while [ true ] ; do
    php  do_jobs.php 
done

嘿,有了do_jobs.php就包含了这样的内容:

// ...
$data   = $beanstalk->reserve();
$class  = $data->class;
$params = $data->params;
$job    = new $class($params);
$job();

// ...

简单对吗?)