直接结论:这不是单次请求的 memory_limit 不够,而是 frankenphp worker 进程长期驻留导致内存持续累积泄漏——必须同时调优 php 单请求限制、容器总内存、worker 生命周期和 symfony 缓存行为。

直接结论:这不是单次请求的 memory_limit 不够,而是 FrankenPHP Worker 进程长期驻留导致内存持续累积泄漏——必须同时调优 PHP 单请求限制、容器总内存、Worker 生命周期和 Symfony 缓存行为。
为什么调大 memory_limit 无效甚至更糟
FrankenPHP 的 Worker 模式让 PHP 进程常驻,memory_limit 只控制单次请求的峰值内存,不释放进程级引用。EntityManager 缓存、EventDispatcher 监听器、闭包绑定等对象在请求结束后仍被强引用,内存只增不减。盲目调高 memory_limit 会掩盖泄漏,最终触发 OOM Killer 杀死整个容器(无日志、无堆栈)。
- 现象:压测中第 300–500 次请求突然报
Fatal error: Allowed memory size of 134217728 bytes exhausted,但memory_get_peak_usage()显示单次请求仅用 28MB - 本质:是 worker 进程内累计泄漏,不是单请求超限
- 错误操作:在
php.ini里把memory_limit改成 512M —— 容器总内存没同步加,OOM Killer 立刻介入
必须同步配置的四个关键项
FrankenPHP 内存问题必须四点联动,缺一不可:
-
memory_limit设为 128M–256M(够用即可,太高反而延迟 GC) - Docker
-m或mem_limit显式设容器总内存:8 线程推荐 1GB,16 线程至少 2GB(memory_limit× 线程数 ≠ 实际值,OPcache 和 autoload 额外吃 30–50MB/线程) - FrankenPHP
max_requests设为 300–600(不是无限),强制 worker 周期性重启清空所有引用 - Symfony 生产环境禁用
debug组件(APP_DEBUG=0),它会在 EventDispatcher 中埋大量调试监听器,强引用控制器和请求对象
如何确认泄漏是否真被切断
别信日志有没有报错,要看内存曲线是否“阶梯归零”:
- 在控制器开头加:
error_log('PID: ' . getmypid() . ', mem: ' . memory_get_peak_usage(true)); - 用
wrk -t4 -c100 -d30s http://localhost/health发 100 次请求,观察日志中mem:值:若每次递增(如 28MB → 32MB → 37MB),说明泄漏仍在;若每 400 次左右回落到 25MB 左右,说明max_requests生效 - 检查
var/cache/prod/App_KernelProdContainer.xml修改时间:worker 启动后该文件不该再变,否则 OPcache 校验失败,每次请求都重编译容器 - 访问
/frankenphp/phpinfo,看opcache.hits是否随压测持续上升,且opcache.memory_usage.used_memory≥ 60%
最容易被忽略的隐性泄漏点
很多团队修了前三步,99% 延迟还是降不下来,问题常出在这三个地方:
- Symfony
EntityManager没在每次请求结束时显式$em->clear()—— 尤其在批量写入或长事务后,实体状态缓存会越积越多 - 自定义
EventListener里用了use ($request)闭包,导致$request被长期持有(改用RequestStack获取) - Monolog 的
RotatingFileHandler在 worker 模式下未设max_files,日志句柄堆积导致 fd 耗尽,间接引发内存分配失败
复杂点在于:这些泄漏不一定会立刻触发 Allowed memory size exhausted,但会让 worker 在第 499 次请求时卡住 2 秒——而你只盯着平均延迟看不出来。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











