webman内存占用高不等于内存泄漏,仅当worker进程经百万级请求后rss持续上涨超100m且gc后不回落,才属真泄漏;需重点排查static/单例无限追加、闭包捕获request、orm循环引用及第三方sdk全局绑定等强引用场景。

Webman 内存占用高不等于内存泄漏,绝大多数情况是常驻进程的正常现象;只有当单个 Worker 进程在百万级请求后仍持续上涨、突破 100M 且无收敛迹象时,才需按泄漏排查。
怎么判断是不是真泄漏:看 memory_get_usage(true) 和 RSS 的关系
只看 memory_get_usage() 容易误判——它返回的是 PHP 堆内分配量,不反映实际操作系统 RSS(常驻集大小)。真正危险的信号是:top 或 ps aux 中某个 Worker 的 RSS 持续爬升、GC 后也不回落。
- 用
gc_collect_cycles()+gc_mem_caches()主动触发回收后,RSS 仍不降 → 很可能有强引用钉住对象 - 压测单个接口 10 万次,
memory_get_peak_usage(true)每次都比前一次高几 KB 以上 → 重点关注该接口的 static / 单例 / 闭包 use - 监控日志里出现
memory_limit exceeded或 monitor 进程频繁重启 Worker → 已触发保护机制,必须干预
最常踩的坑:static 数组和单例属性无限追加
Webman 里 static、单例实例属性、global 变量生命周期 = Worker 进程寿命。只要往里面 [] = $value 或 $this->cache[$key] = $data,又不清理,就满足泄漏两个必要条件。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 错误写法:
self::$log[] = ['time' => time(), 'uri' => $request->uri()]—— 日志数组越积越多 - 错误写法:
Cache::instance()->data[$key] = $bigArray—— 单例缓存没 TTL、没 forget、没 size 限制 - 正确做法:改用带 TTL 的 Redis 缓存;或本地缓存加
count($cache) > 1000 && array_shift($cache)控制长度 - 临时调试:在中间件末尾加
unset($GLOBALS['debug_cache'])或self::$temp = []强制清空
闭包、定时器、事件监听器偷偷持有 Request/Response 实例
这类泄漏最难察觉,因为代码看起来“只是读个字段”,但 use ($request) 会让整个 Request 对象无法被 GC,而 Request 又持有了上传文件、原始 body、解析后的 JSON 等大块内存。
- 典型场景:在
onWorkerStart里注册定时器,new Crontab('* * * * * *', function () use ($request) { ... })——$request被捕获后永远活在定时器闭包里 - 安全替代:只传必要字段,如
use ($request->id, $request->ip());或改用 ID + 延迟查库 - ORM 关联 + 事件监听器互相引用:比如
User::with('posts')->get()后触发了监听器,监听器又把 User 实例塞进静态队列 → 形成循环引用,gc_collect_cycles()都难收 - 检查方法:用
xdebug_debug_zval('var_name')看 refcount,或导出堆快照用php-meminfo分析谁在引用
数据库和大文件处理不流式,导致峰值内存爆炸
不是泄漏,但会直接触发 OOM 或让 monitor 杀进程。Webman 不像 FPM 那样“请求结束就重来”,一次撑爆,全进程卡死。
- 禁止写
User::all()或Db::table('logs')->get()—— 全量加载几百 MB 数据到内存 - 改用游标:
User::cursor()或分批:Db::table('logs')->chunk(500, function ($rows) { ... }) - 大文件上传后立刻
move_uploaded_file(),别用file_get_contents($_FILES['file']['tmp_name'])把整个文件读进变量 - 导出类接口强制用
response()->stream():return response()->stream(function () { foreach (Order::cursor() as $order) echo json_encode($order) . "\n"; })
真正难搞的泄漏往往藏在第三方 SDK 的全局注册逻辑里,比如某支付回调 SDK 在初始化时把整个 App 实例 bind 到静态容器中;这种得翻源码、打日志、甚至 patch 扩展。不要迷信“框架封装好”,常驻进程下,每一行 static、每一次 use、每一个没清理的单例,都是潜在的内存锚点。










