首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >每个 PHP 开发者都应了解的长进程内存模式

每个 PHP 开发者都应了解的长进程内存模式

作者头像
Tinywan
发布2026-07-01 12:25:00
发布2026-07-01 12:25:00
560
举报
文章被收录于专栏:开源技术小栈开源技术小栈

你会反复遇到的模式

如果你从 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 处理它,发送响应,然后死亡。所有内存自动释放。为下一个请求提供干净的画布。

这种模型塑造了 PHP 的整个内存架构。Zend 内存管理器 (ZMM) 被优化用于:

  • 请求处理期间的快速分配
  • 进程终止时的零清理成本
  • 可预测的、毫秒级短生命周期

但如今的 PHP 景观不同了。RoadRunner 工作者处理数千个任务。Laravel 队列工作者 24/7 处理后台任务。自定义守护进程和长运行 CLI 脚本。

在这些场景中,PHP 的内存模式变得可见——并且至关重要,需要理解。

Zend 内存管理器如何实际工作

PHP 并不直接使用 malloc()free()。相反,它使用 Zend 内存管理器——一个针对 PHP 工作负载优化的内存分配器。

块系统

ZMM 以称为块的大型块分配内存——通常每个 2–4 MB。当你创建字符串、数组或对象时,PHP 不会每次都向 OS 请求内存。相反,它从这些预分配的块中雕琢出空间。

想象成这样:

  • OS 内存:一个出租整个楼层的仓库
  • PHP 块:那些由 PHP 租用的楼层
  • 你的变量:那些楼层上的桌子和椅子

内存单向流动

这里是抓住每个人的关键部分:

代码语言:javascript
复制
$data = fetchHugeDataset(); // 在块中分配 300 MB
processData($data);
unset($data);               // 在 PHP 内部释放内存
                            // OS 仍然看到 300 MB 已分配

当你 unset() 一个变量时:

  1. PHP 在其内部结构中标记该内存为空闲
  2. 内存返回到 PHP 的空闲池
  3. OS 永远不会拿回那块内存
  4. 块仍然分配给进程

这不是 bug——这是优化。重用已分配的块比不断向 OS 请求内存并归还它要快得多。

为什么块永远保留

即使一个块变得 100% 空闲,PHP 也很少将其返回给 OS。为什么?

  • 性能。从 OS 获取内存代价高昂。
  • 碎片化。混合分配阻止块完全为空。
  • 没有收缩机制。PHP 没有自动块去分配。
  • 假设。PHP 假设你很快又需要那块内存。

对于传统的 PHP-FPM,这无关紧要——进程在每个请求后死亡。对于长运行进程,这成为你的新现实。

楼梯模式

这就是长运行 PHP 进程中内存消耗的样子:

代码语言:javascript
复制
内存  │
380MB ┤       ┌─────────────────
      │       │
      │       │
      │       │ (峰值操作)
      │      ╱
60MB  ┤─────╯
      │
      └─────────────────────────────> 时间

每个峰值成为新的基线。永远。

这个模式在工作者的生命周期中反复出现。工作者从 60 MB 开始。处理大批量,峰值到 200 MB,停留在 200 MB。处理另一个重任务,跳到 350 MB,停留在 350 MB。处理海量报告,达到 500 MB,停留在 500 MB。

一旦你看到这个模式,你就忘不掉了。

“等等,”你可能会说,“PHP 不是有垃圾回收吗?”

是的,但它并不像你想的那样工作。

引用计数

PHP 主要使用引用计数。当变量的引用计数降到零时,它立即被释放:

代码语言:javascript
复制
$data = createBigArray(); // refcount = 1
$copy = $data;            // refcount = 2
unset($data);             // refcount = 1
unset($copy);             // refcount = 0, 立即释放

这很棒,自动处理 95% 的情况。

循环收集器

PHP 还有一个循环收集器,用于循环引用:

代码语言:javascript
复制
$a = [];
$b = [];
$a['ref'] = &$b;
$b['ref'] = &$a;
unset($a, $b); // 需要循环收集器

运行 gc_collect_cycles() 在这里有帮助。

问题:两者都不返回内存给 OS

两种机制都在 PHP 的内部结构中释放内存。两者都不将块返回给操作系统。

唯一可以部分返回内存给 OS 的函数是:

代码语言:javascript
复制
gc_collect_cycles();
gc_mem_caches();

但即使 gc_mem_caches() 也有限制——它只能返回完全空的块,而碎片化往往阻止这一点。我在生产中使用过它,有时有帮助,有时什么都不做。取决于你当时内存布局。

在你的代码中识别模式

现在你知道模式存在,你可以发现危险区域。

批量 ORM 操作

代码语言:javascript
复制
// 这会峰值内存并永不释放它
$users = User::all(); // 加载 50K 用户
foreach ($users as $user) {
    processUser($user);
}

我见过这崩溃生产工作者次数比我能数的还多。最糟糕的部分?在开发环境中数据库只有 100 个用户时,它运行得很好。

大文件处理

代码语言:javascript
复制
// 整个文件加载到内存中
$content = file_get_contents('huge-file.csv');
$lines = explode("\n", $content);

聚合结果

代码语言:javascript
复制
// 不断增长的数组永不缩小
$results = [];
foreach ($items as $item) {
    $results[] = expensiveOperation($item);
}

嵌套水合

代码语言:javascript
复制
// ORM 加载整个对象图
$orders = Order::with('customer.address.country')
    ->with('items.product.category')
    ->get();

这些都会创建成为永久的内存峰值。模式会累积:触发 3–4 次,你的 60 MB 工作者就会变成 400 MB 工作者。

围绕模式设计

一旦你认识到模式,你就可以架构你的应用来与之合作,而不是对抗它。

思考流,而不是集合

模式感知的方法:

代码语言:javascript
复制
// 内存保持平稳——无峰值
Record::where('status', 'pending')
    ->chunk(100, function ($records) {
        foreach ($records as $record) {
            processRecord($record);
        }
    });

Laravel 在 PHP 8+ 中的懒加载集合:

代码语言:javascript
复制
// 完美适合模式
User::lazy()->each(function ($user) {
    processUser($user);
});

这是我现在的首选模式。一旦你习惯这样写代码,你就会停止考虑内存。

Doctrine 迭代器:

代码语言:javascript
复制
// 流式结果,清除内存
$query = $em->createQuery('SELECT u FROM User u');
foreach ($query->toIterable() as $user) {
    process($user);
    $em->detach($user); // 别忘了这个!
}

那个 detach() 调用至关重要。没有它,Doctrine 会将所有实体保留在身份映射中,你就回到了原点。

作用域隔离

利用 PHP 在函数退出时的自动清理:

代码语言:javascript
复制
class JobHandler
{
    publicfunction handle($job)
    {
        // 重工作发生在隔离作用域中
        $this->processLargeDataset($job);
        
        // 所有局部变量在这里释放
        // 模式峰值最小化
    }

    privatefunction processLargeDataset($job)
    {
        $data = fetchData(); // 100 MB 峰值
        transform($data);
        save($data);
        // 峰值在函数退出时结束
    }
}

这出奇地有效。函数作用域边界是你的朋友。

拥抱工作者轮换

接受模式并为此设计。

RoadRunner:

代码语言:javascript
复制
# .rr.yaml
pool:
  max_jobs: 1000  # 每 1K 任务刷新工作者

文档:Workers pool

Laravel 队列:

代码语言:javascript
复制
php artisan queue:work --max-jobs=1000

文档:Queues — Laravel 12.x

Symfony Messenger:

代码语言:javascript
复制
php bin/console messenger:consume --limit=1000

文档:Messenger: Sync & Queued Message Handling

工作者轮换不是变通方法——它是模式感知的解决方案。有些人抵制这个,因为它感觉像放弃。它不是。它是与 PHP 架构合作而不是对抗。

防止真实泄漏

模式是预期行为。真实泄漏不是。

模式违规:增长的静态状态

代码语言:javascript
复制
class Cache
{
    private static array $data = [];

    public static function store($key, $value)
    {
        self::$data[$key] = $value; // 永远累积
    }
}

模式感知:有界缓存

代码语言:javascript
复制
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 架构对抗

🤔 在理解模式之后:

  • “每个操作的预期峰值内存是多少?”
  • “我如何保持峰值小而可预测?”
  • “何时应该轮换工作者?”
  • 与 PHP 架构合作

模式不是限制——它是设计围绕的约束。而约束孕育更好的设计。

实际要点

  • 接受模式。内存峰值直到重启都是永久的。
  • 为峰值设计,而不是平均值。你的工作者将永远消耗其最高峰值。
  • 保持峰值小。批量操作、流式数据、隔离作用域。
  • 轮换工作者。它不是变通方法,而是解决方案。
  • 积极监控。在它们累积之前捕捉意外峰值。
  • 防止真实泄漏。无界增长的静态/全局状态。

那些在长运行 PHP 中挣扎的开发者往往是那些与模式对抗的。那些成功的开发者拥抱它并相应构建。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 你会反复遇到的模式
  • 为什么这个模式存在:PHP 的基因
  • Zend 内存管理器如何实际工作
    • 块系统
    • 内存单向流动
  • 为什么块永远保留
    • 楼梯模式
    • 引用计数
    • 循环收集器
    • 问题:两者都不返回内存给 OS
  • 在你的代码中识别模式
    • 批量 ORM 操作
    • 大文件处理
    • 聚合结果
    • 嵌套水合
  • 围绕模式设计
    • 思考流,而不是集合
    • 作用域隔离
    • 拥抱工作者轮换
  • 防止真实泄漏
  • 模式改变一切
  • 实际要点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档