
如果你从 PHP-FPM 转向长运行进程 —— RoadRunner、Laravel、ThinkPHP 队列工作者、Symfony Messenger 消费者——你会遇到一个令人困惑的模式:
内存只会上涨。永不下降。
一个工作者从 ~40 MB 开始。处理一个繁重任务,跳到 200 MB。处理另一个大批量,达到 350 MB。然后……它就停留在 350 MB。永远。直到重启。
你检查内存泄漏。没有。你运行 gc_collect_cycles()。没有变化。你到处添加 unset() 调用。内存仍然很高。
这在我们生产系统中发生过:一个未优化的 ORM 查询一次性加载了大约 100,000 条记录,内存从 ~40 MB 飙升到 360 MB,然后再也没有下降。工作者在其余生命周期中一直消耗 360 MB。
这不是 bug。这是模式。一旦你理解它,你就会完全不同地设计你的长运行应用。
PHP 被设计用于短生命周期进程。传统的请求-响应周期:请求进来,PHP 处理它,发送响应,然后死亡。所有内存自动释放。为下一个请求提供干净的画布。
这种模型塑造了 PHP 的整个内存架构。Zend 内存管理器 (ZMM) 被优化用于:
但如今的 PHP 景观不同了。RoadRunner 工作者处理数千个任务。Laravel 队列工作者 24/7 处理后台任务。自定义守护进程和长运行 CLI 脚本。
在这些场景中,PHP 的内存模式变得可见——并且至关重要,需要理解。
PHP 并不直接使用 malloc() 和 free()。相反,它使用 Zend 内存管理器——一个针对 PHP 工作负载优化的内存分配器。
ZMM 以称为块的大型块分配内存——通常每个 2–4 MB。当你创建字符串、数组或对象时,PHP 不会每次都向 OS 请求内存。相反,它从这些预分配的块中雕琢出空间。
想象成这样:
这里是抓住每个人的关键部分:
$data = fetchHugeDataset(); // 在块中分配 300 MB
processData($data);
unset($data); // 在 PHP 内部释放内存
// OS 仍然看到 300 MB 已分配
当你 unset() 一个变量时:
这不是 bug——这是优化。重用已分配的块比不断向 OS 请求内存并归还它要快得多。
即使一个块变得 100% 空闲,PHP 也很少将其返回给 OS。为什么?
对于传统的 PHP-FPM,这无关紧要——进程在每个请求后死亡。对于长运行进程,这成为你的新现实。
这就是长运行 PHP 进程中内存消耗的样子:
内存 │
380MB ┤ ┌─────────────────
│ │
│ │
│ │ (峰值操作)
│ ╱
60MB ┤─────╯
│
└─────────────────────────────> 时间
每个峰值成为新的基线。永远。
这个模式在工作者的生命周期中反复出现。工作者从 60 MB 开始。处理大批量,峰值到 200 MB,停留在 200 MB。处理另一个重任务,跳到 350 MB,停留在 350 MB。处理海量报告,达到 500 MB,停留在 500 MB。
一旦你看到这个模式,你就忘不掉了。
“等等,”你可能会说,“PHP 不是有垃圾回收吗?”
是的,但它并不像你想的那样工作。
PHP 主要使用引用计数。当变量的引用计数降到零时,它立即被释放:
$data = createBigArray(); // refcount = 1
$copy = $data; // refcount = 2
unset($data); // refcount = 1
unset($copy); // refcount = 0, 立即释放
这很棒,自动处理 95% 的情况。
PHP 还有一个循环收集器,用于循环引用:
$a = [];
$b = [];
$a['ref'] = &$b;
$b['ref'] = &$a;
unset($a, $b); // 需要循环收集器
运行 gc_collect_cycles() 在这里有帮助。
两种机制都在 PHP 的内部结构中释放内存。两者都不将块返回给操作系统。
唯一可以部分返回内存给 OS 的函数是:
gc_collect_cycles();
gc_mem_caches();
但即使 gc_mem_caches() 也有限制——它只能返回完全空的块,而碎片化往往阻止这一点。我在生产中使用过它,有时有帮助,有时什么都不做。取决于你当时内存布局。
现在你知道模式存在,你可以发现危险区域。
// 这会峰值内存并永不释放它
$users = User::all(); // 加载 50K 用户
foreach ($users as $user) {
processUser($user);
}
我见过这崩溃生产工作者次数比我能数的还多。最糟糕的部分?在开发环境中数据库只有 100 个用户时,它运行得很好。
// 整个文件加载到内存中
$content = file_get_contents('huge-file.csv');
$lines = explode("\n", $content);
// 不断增长的数组永不缩小
$results = [];
foreach ($items as $item) {
$results[] = expensiveOperation($item);
}
// ORM 加载整个对象图
$orders = Order::with('customer.address.country')
->with('items.product.category')
->get();
这些都会创建成为永久的内存峰值。模式会累积:触发 3–4 次,你的 60 MB 工作者就会变成 400 MB 工作者。
一旦你认识到模式,你就可以架构你的应用来与之合作,而不是对抗它。
模式感知的方法:
// 内存保持平稳——无峰值
Record::where('status', 'pending')
->chunk(100, function ($records) {
foreach ($records as $record) {
processRecord($record);
}
});
Laravel 在 PHP 8+ 中的懒加载集合:
// 完美适合模式
User::lazy()->each(function ($user) {
processUser($user);
});
这是我现在的首选模式。一旦你习惯这样写代码,你就会停止考虑内存。
Doctrine 迭代器:
// 流式结果,清除内存
$query = $em->createQuery('SELECT u FROM User u');
foreach ($query->toIterable() as $user) {
process($user);
$em->detach($user); // 别忘了这个!
}
那个 detach() 调用至关重要。没有它,Doctrine 会将所有实体保留在身份映射中,你就回到了原点。
利用 PHP 在函数退出时的自动清理:
class JobHandler
{
publicfunction handle($job)
{
// 重工作发生在隔离作用域中
$this->processLargeDataset($job);
// 所有局部变量在这里释放
// 模式峰值最小化
}
privatefunction processLargeDataset($job)
{
$data = fetchData(); // 100 MB 峰值
transform($data);
save($data);
// 峰值在函数退出时结束
}
}
这出奇地有效。函数作用域边界是你的朋友。
接受模式并为此设计。
RoadRunner:
# .rr.yaml
pool:
max_jobs: 1000 # 每 1K 任务刷新工作者
文档:Workers pool
Laravel 队列:
php artisan queue:work --max-jobs=1000
文档:Queues — Laravel 12.x
Symfony Messenger:
php bin/console messenger:consume --limit=1000
文档:Messenger: Sync & Queued Message Handling
工作者轮换不是变通方法——它是模式感知的解决方案。有些人抵制这个,因为它感觉像放弃。它不是。它是与 PHP 架构合作而不是对抗。
模式是预期行为。真实泄漏不是。
模式违规:增长的静态状态
class Cache
{
private static array $data = [];
public static function store($key, $value)
{
self::$data[$key] = $value; // 永远累积
}
}
模式感知:有界缓存
class BoundedCache
{
private array $data = [];
private int $maxSize = 1000;
public function store($key, $value)
{
if (count($this->data) >= $this->maxSize) {
array_shift($this->data); // 防止模式升级
}
$this->data[$key] = $value;
}
}
一旦你内化这个模式,你对长运行 PHP 应用的方法就会转变。
👎 在理解模式之前:
unset() 问题”🤔 在理解模式之后:
模式不是限制——它是设计围绕的约束。而约束孕育更好的设计。
那些在长运行 PHP 中挣扎的开发者往往是那些与模式对抗的。那些成功的开发者拥抱它并相应构建。