并发请求本身不会直接导致内存泄漏,真正出问题是每个请求中重复加载、未释放或意外持留的对象;laravel的octane常驻进程与swoole协程模型会放大单次请求的内存管理缺陷,如静态属性缓存、未清理的eloquent模型、未禁用的查询日志及事件监听器,导致内存持续累积。

并发请求本身不会直接导致内存泄漏,真正出问题的是在每个请求(或队列任务、Octane 请求周期)中重复加载、未释放、或意外持留的对象。Laravel 的并发模型(如多 worker、Octane 常驻进程、Swoole 服务)放大了单次请求的内存管理缺陷——没清理干净,就会越积越多。
DB 查询别用 all() 或 get() 加载全量数据
在并发场景下,多个请求同时执行 Model::all() 或 DB::table('logs')->get(),每条记录都生成一个 Eloquent 模型或数组,内存瞬间吃紧。尤其当表有几十万行、字段又多时,单次请求就可能占掉 50MB+。
- 改用分块:用
Model::query()->chunkById(500, function ($models) { ... }),每次只处理一批,执行完自动释放内部引用 - 只读统计类操作,直接走聚合:用
Model::where(...)->count()、Model::sum('amount'),不实例化任何对象 - 必须全量?处理完立刻
unset($models),再调一次gc_collect_cycles()(效果有限,但比不做强)
禁用模型事件与查询日志是并发下的硬性要求
每个 Eloquent 操作默认触发 creating、saving、created 等事件,监听器若注册在全局(比如 Observer 或 Event::listen()),会在所有并发请求中持续驻留,且监听器闭包容易捕获外部变量形成循环引用。
- 批量写入前统一关闭:
Model::withoutEvents(function () { ... }) - 单个模型临时禁用:
$model->fireModelEvent = false,操作完恢复(仅限明确需保留事件的少数场景) - 务必在并发入口处(如控制器方法开头、队列
handle()开始)调用DB::disableQueryLog(),避免调试模式下 SQL 日志累积成山
Octane / Swoole 场景下严禁静态属性存请求数据
Octane 启动后,应用常驻内存,生命周期跨越成百上千次请求。如果在某个 Service 类里写了 public static $cache = [],或在中间件里把用户数据塞进 static::$user,这些数据永远不会被 GC 清理,只会越攒越多。
- 所有请求上下文数据(如当前用户、请求参数、临时计算结果)必须放在 request scope 内,例如通过
request()->attributes->set()或函数参数传递 - 缓存用
Cache::store('redis'),别用static数组模拟;Redis 本身有 TTL 和淘汰策略,可控 - 检查
app()->getBindings()和app()->getInstances(),确认没手动 bind 长生命周期对象(比如把 DB 连接实例 bind 到容器)
队列任务要设 --max-jobs 和 --memory 防止“慢性死亡”
一个 queue:work 进程跑一天,中间处理了 2000 个任务,哪怕每个任务只漏 10KB,最终也累积到 20MB+。这不是 bug,是 PHP 长进程的自然现象。
- 启动时强制加参数:
php artisan queue:work --max-jobs=200 --memory=128,让进程在处理 200 个任务后自动退出,由 Supervisor 拉起新进程 - 在任务内部加主动检测:
if (memory_get_usage() > 80 * 1024 * 1024) { $this->release(60); return; },避免单个任务就把内存撑爆 - Supervisor 配置里必须开
autorestart=true和合理startsecs,否则进程被 kill 后卡住不重启
最易被忽略的一点:内存泄漏往往不是某一行代码造成的,而是多个小疏漏叠加的结果——一个没 unset 的模型、一个没 forget 的事件监听器、一个被静态属性 hold 住的 collection。在并发环境下,这些“小问题”会被乘以请求数量级,迅速变成雪崩点。盯住 memory_get_usage() 在关键节点的差值,比看峰值更有价值。











