yiguodev:Xray-core XHTTP 出站删除时释放缓存客户端,1000 节点实验缓存残留降为 0
yiguodev 在 Xray-core PR #7118(XHTTP: Release cached clients on outbound removal,状态 closed、未合并)给出中文修改说明:反复新增、拨号和删除 XHTTP outbound 时,每个新建的流配置都会在全局拨号缓存中留下一个条目;即使额外调用 Handler.Close(),关联的缓存客户端也没有被释放。本次修改让节点删除同时关闭 handler 及其拥有的传输资源,包括独立下载配置和所有目标地址的缓存;清理前先把配置标记为已关闭以免并发获取重新创建缓存,实际关闭在管理器锁与缓存锁外执行;底层 TCP 连接在 TLS/REALITY 封装前登记,XMUX 在释放选择锁前预留请求计数;H1 上传会读取、排空并关闭响应后再归还连接。验证:修复前新增 1000 个 XHTTP 节点后缓存仍有 1005 项;修复后 16 组 Linux/arm64 实验的 240 个采样点缓存均为 0,含预热的 12,060 次 HEAD 请求全部成功,空闲后 FD 与 goroutine 回到基线、socket FD 为 0;各场景修复前后空闲后存活堆增量由约 2.8–3.1 MiB 降至约 0.05 MiB。
作者原文@yiguodev修改说明(中文)
反复新增、拨号和删除 XHTTP outbound 时,每个新建的流配置都会在全局拨号缓存中留下一个条目。即使额外调用
Handler.Close(),关联的缓存客户端也没有被释放。本次修改让节点删除同时关闭 handler 及其拥有的传输资源,包括独立下载配置和所有目标地址的缓存。清理前先将配置标记为已关闭,避免并发获取重新创建缓存;实际关闭操作在管理器锁和缓存锁外执行。
RemoveHandler(tag) 在管理器锁内注销对应 handler 释放管理器锁 + 关闭该 handler,且只执行一次 + 关闭其流配置 + 摘除关联的 XHTTP 缓存 + 取消请求,关闭当前及已退休的客户端和连接底层 TCP 连接在 TLS/REALITY 封装前登记,保留已有的调用方取消语义。XMUX 在释放选择锁前预留请求计数,普通轮换等待活动请求完成,删除节点则关闭全部关联客户端。迟完成的拨号和关闭后归还的连接不能重新入池。
H1 上传会读取、排空并关闭响应后再归还连接,使请求计数覆盖完整响应过程。多个上传仍可并发;无效响应和
Connection: close连接会被丢弃。stream-up 部分拨号成功后失败时,会关闭已经打开的下载端。验证结果
- 修复前: 回归测试暴露了 handler/配置未清理、关闭客户端后请求仍存活的问题;H1 慢响应轮换测试也能复现上传计数在写入完成时过早释放。
修复后: 当前保留 13 项关键生命周期回归测试,覆盖锁外关闭和同名替换、handler 清理、配置隔离和独立下载、并发获取与关闭、已退休但仍活动的客户端、迟完成拨号及连接归还、实际 TLS/H1/H2 请求取消、H1/H2 慢响应轮换、HTTP1Connection: close处理及部分拨号失败清理。- 修复前: 新增 1000 个 XHTTP 节点后,删除节点并额外关闭 handler,缓存仍有 1005 项,其中 5 项来自预热。
修复后: 16 组 Linux/arm64 实验的全部 240 个采样点缓存均为 0;包含预热的 12,060 次 HEAD 请求全部成功。空闲后 FD 和 goroutine 恢复到各次运行的基线,socket FD 为 0。| 场景 | 修复前空闲后存活堆增量 | 修复后空闲后存活堆增量 |
| --- | ---: | ---: |
| stream-one / Remove | 2.826–2.828 MiB | 0.053–0.058 MiB |
| stream-one / Remove + Close | 2.866–2.897 MiB | 0.050–0.053 MiB |
| packet-up / Remove | 3.075–3.077 MiB | 0.047–0.052 MiB |
| packet-up / Remove + Close | 3.072–3.075 MiB | 0.048–0.052 MiB |每次测量先预热 5 个节点,再执行 200 批、每批最多 5 个节点,共新增 1000 个节点;各场景独立重复两次。修复前后使用相同的 loopback VLESS/XHTTP HTTP/1.1 后端、2 CPU / 2 GiB Linux 容器、Go 1.27.2,并在空闲 10 秒后执行两次 GC。实验还包含直连和仅新增/删除对照。RSS 单独记录,不作为缓存残留或存活 Go 堆的等价指标。
以下检查已通过:
go run ./infra/vformat/main.go -mode check -pwd ./ go test ./app/proxyman/outbound ./transport/internet ./transport/internet/splithttp ./core ./app/proxyman/command -count=1 -timeout=180s go test ./... -run '^$' -count=1 -timeout=120s # 全部包编译检查 go test -race ./transport/internet/splithttp -count=1 -timeout=120s go test -race ./app/proxyman/outbound -run '^(TestRemoveHandlerCloseMayReplaceTag|TestManagerCloseClearsHandlersBeforeCallbacks|TestHandlerCloseOwnsStreamConfigAndRetainsErrors|TestConcurrentSelectAndHandlerRemoval)$' -count=1 -timeout=90s使用临时 module replacement 指向本次 Core 修改后,libXray 的完整测试也通过。
go vet仍报告原始基线中已有的 8 项 protobufXmuxConfig复制锁告警。兼容性与验证边界
本次修改可通过代码回退撤销,无需配置或数据迁移;影响范围为网络连接生命周期。
被删除的 handler 和已关闭的流配置不能再次使用,重新注册时需要构造新对象。H1 不再在上一响应完成前通过同一 TCP 连接流水线发送下一请求;公网高延迟环境下的吞吐表现未测量。活动 HTTP/3 请求关闭、挂起的 uTLS/REALITY 握手没有端到端验证;Browser Dialer 浏览器端的请求取消和设备构建也不在本次验证范围内。