composer autoload重复注册导致classloader实例堆积,swoole长连接中反复require vendor/autoload.php会持续新增实例并占用内存;需确保仅启动时加载一次,禁用运行时重复加载,并排查插件单例及realpath缓存问题。

Composer autoload 重复注册导致静态变量堆积
Composer 的 vendor/autoload.php 本质是注册一个 Composer\Autoload\ClassLoader 实例到 PHP 的自动加载队列。Swoole 长连接场景下,若每次请求都 require 'vendor/autoload.php'(比如 Web UI 调用 Composer 命令、或错误地在中间件里反复加载),就会不断新增 ClassLoader 实例——每个实例都持有自己的 $prefixes、$classMap 和 $files 静态映射表,且不会被 GC。
常见错误现象:
- 访问 Composer 管理界面后,
memory_get_usage(true)每次上涨 200KB+,spl_autoload_functions()返回数组长度持续增长 -
var_dump(spl_autoload_functions())中出现多个不同对象 ID 的ClassLoader::loadClass
实操建议:
- 确保
vendor/autoload.php只在进程启动时加载一次(如 SwooleonStart回调中) - Web 环境中禁用任何运行时
requireautoload 的逻辑;改用预加载(opcache.preload)或 CLI 模式隔离 Composer 调用 - 检查是否有第三方插件(如某些调试面板)偷偷执行
require_once __DIR__.'/vendor/autoload.php'
Swoole 协程内调用 Composer API 触发的闭包引用泄漏
Composer 提供的 Composer\Repository\InstalledRepository 或 Composer\Package\CompletePackage 类常含闭包属性(如 $package->getInstallationSource() 返回的 callable)。若在协程中创建这些对象并将其存入全局静态变量(如 static $repo = null),闭包会隐式捕获当前协程上下文(包括 $this、$request 等),导致整个请求栈无法释放。
典型场景:
- 自定义命令行服务在
onWorkerStart中初始化ComposerFactory并赋值给static $factory - WebSocket 处理器里调用
composer dump-autoload --no-dev后缓存结果到static $cache
实操建议:
- 避免在静态属性中保存任何 Composer 运行时对象;改用函数级局部变量 + 显式销毁(
unset($repo)) - 如需复用 Repository,用
weakref包装或改用无状态方式(如只缓存 JSON 结果而非对象实例) - 协程内调用
exec('php -r "require...; echo json_encode(...);"')替代直接加载 Composer 类——用进程隔离代替内存共享
opcache + realpath_cache 叠加导致的“伪泄漏”
PHP 8.1.10 / 8.2.0–8.2.3 / 8.3.0 存在 realpath 缓存不清理 Bug:反复解析 vendor/ 下成百上千个路径时,realpath_cache_size() 持续上涨,且 clearstatcache(true) 无效。Swoole Worker 长驻进程会把这部分 C 层缓存一直留着,表现为 RSS 不降,但 memory_get_usage() 看不出异常。
验证方式:
- 在请求中执行
echo realpath_cache_size();,连续刷新 10 次,数值从 1MB 涨到 12MB 就是它 - 对比
ps -o rss= -p $(pgrep -f swoole)与memory_get_usage(true)差值是否长期 >30MB
实操建议:
- 升级 PHP 至 8.2.4+ 或 8.3.1+(已修复)
- 临时缓解:启动时加
ini_set('realpath_cache_size', '4194304');(4MB 上限) - 禁用 opcache 在 Swoole 中的动态重载:
opcache.enable_cli=0,避免opcache_reset()被误触发
第三方 Composer 插件在 Swoole 中的单例失控
部分 Composer 插件(如 hirak/prestissimo、composer-unused)会在首次加载时注册全局单例(static::$instance),并绑定事件监听器或持久化连接(如 cURL handler、Redis 客户端)。这些单例在 Swoole Worker 生命周期内永不销毁,且插件自身无 reset() 接口。
容易踩的坑:
- CI 环境中用
composer install --no-plugins安全,但生产环境若启用了插件,其静态状态会跨请求污染 - 插件内部的
curl_init()句柄未显式curl_close(),底层 socket 连接持续累积
实操建议:
- 生产环境强制禁用所有非必要插件:
COMPOSER_NO_PLUGINS=1环境变量 - 若必须使用,改写插件入口,在
onWorkerStop中调用其私有清理方法(如反射调用Plugin::destroy()) - 监控
/proc/$(pgrep -f swoole)/fd/目录文件数,超 500 个 fd 时立即排查插件连接泄漏











