真泄漏是百万请求后RSS持续线性上涨且GC不回落,假性溢出则由xdebug、var_dump残留或单次大请求导致,压测后RSS可自然回落;须用ps查RSS而非memory_get_usage(),并结合gc_collect_cycles()验证。

Webman内存溢出不是“一重启就没事”的临时故障,而是常驻进程模型下引用链未切断、静态结构失控的明确信号。真泄漏往往在百万级请求后仍线性上涨,且RSS不回落;假性溢出则多由xdebug、var_dump残留或单次大请求导致,压测后能自然回落。
怎么确认是真泄漏还是假性溢出
别只看memory_get_usage(),它反映的是PHP堆内分配量,而操作系统真正扣住的是RSS(常驻集大小)。泄漏的本质是RSS持续爬升且GC后不降。
- 用
php start.php status查Summary字段里的内存值,连续压测30秒前后对比——若上涨超50MB且后续不回落,需警惕 - 在入口中间件加
echo "RSS: " . (int)shell_exec("ps -o rss= -p " . getmypid()) . "\n";,比memory_get_peak_usage(true)更贴近真实压力 - 关掉
xdebug:执行php -d zend_extension= -f start.php status,若错误消失,说明是调试扩展拖累 - 检查是否在
onRequest里用了file_get_contents($big_file)或$query->all(),这类操作会瞬时拉高RSS,但属于单次行为,非泄漏
static数组无限膨胀是最常见泄漏源
Webman里static $cache = []不是缓存,是内存钉子。只要没清理逻辑,每次请求[]=或array_push()就是在给进程喂内存。
- 全局搜索
static.*\[(?:public|protected|private)?\s*\$[a-zA-Z_],重点盯app/Listener/和app/Middleware/下的类 - 发现
self::$log = [];这种结构,立刻改造成带容量限制的循环队列:if (count(self::$log) > 1000) { array_shift(self::$log); } - 禁用裸数组做计数器或会话映射,改用
Webman\Support\Cache::set('key', $value, 300),TTL自动兜底 - 若必须用static存储连接上下文(如WebSocket用户ID映射),PHP 8.0+优先选
WeakMap:private static WeakMap $connections;,对象销毁后引用自动解除
闭包捕获$this或大型对象极易隐式锁死内存
闭包里use ($this)或use ($request)不是语法糖,是强引用链的起点。一旦$this持有数据库连接、事件监听器或服务容器,整条链就再也收不回来。
立即学习“PHP免费学习笔记(深入)”;
- 检查所有
Worker::onWorkerStart、onConnect、onMessage中注册的匿名函数 - 把
use ($request, $response)改成use ($request->id),后续用ID查库,避免绑定整个Request实例 - 定时器回调中禁止
use ($connection),改用WeakMap存连接句柄,或在onClose里显式unset() - 每次处理完手动触发回收:
gc_collect_cycles(); gc_mem_caches();,再观察RSS是否回落——不落就是还有强引用钉着
monitor进程的内存阈值保护有盲区
app/process/Monitor.php默认每60秒读一次/proc/[pid]/statm,靠VmRSS判断是否超限。但它只杀超限进程,不告诉你哪段代码在漏。
- 配置里
memory_limit设为1024(单位MB),别信“自动适配”——它不会根据业务动态调,只是硬重启阈值 - 监控频率可调:在
Monitor::checkMemory()里把Timer::add(60, ...)改成30,高频检测能更快止损,但别低于15秒(避免IO抖动) - 它不监控
memory_limit配置是否生效——若你在start.php里漏了ini_set('memory_limit', '512M'),monitor看到的永远是默认128M,误判频繁 - 真正要定位泄漏点,得结合
Xhprof抓慢接口+strace -p [pid] -e trace=brk,mmap看系统调用,RSS涨在哪一步最猛



















