DEBUG.WIKI
腾讯云;让你的想法跑起来;轻量服务器 · 建站 / Docker / API;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →

.NET 10 已 Dispose HttpClient,HTTP/2 套接字仍持续增加

将连接寿命或空闲期限设为零,可能让 .NET 10 的 HTTP/2 连接绕过池管理器登记。检查两项期限与实际协议,不能靠强制 GC 回收仍被读循环引用的连接。

监控程序每次探测都创建并释放 HttpClient 与 SocketsHttpHandler,进程里的套接字和 RSS 却不断上涨,托管堆没有相应增长。若请求实际使用 HTTP/2,而且把 PooledConnectionLifetime 或 PooledConnectionIdleTimeout 设成了 TimeSpan.Zero,应核对这一组合的池管理路径。

.NET 10.0.7、10.0.8 和 10.0.10 的 Linux、HTTP/2 over TLS 场景有明确故障表现。相关优化还要求 MaxConnectionsPerServer 保持默认的无限上限,并且没有配置 keep-alive ping。正式 10.0.13 源码仍保留这条条件,不能只因补丁版本变新就认定该路径已修复。

零期限让管理器不登记池,HTTP/2 却保存了连接

满足这些条件时,管理器认为连接不会被保存,因此不创建清理定时器,也不把临时池登记进自己的池集合。这个假设与 HTTP/2 的新连接返回逻辑发生了冲突:新连接跳过过期检查,仍进入可用 HTTP/2 连接列表。

释放 handler 时,管理器只遍历已经登记的池。没有登记的池及其 HTTP/2 连接便可能漏掉释放,后台读取循环仍持有连接、TLS 流和套接字。它们仍然可达,反复 GC.Collect() 或等待终结器不能解决引用关系。

先保留实际协议、两项连接期限和 handler 创建位置。文件描述符增长也可能来自其他资源;不能仅凭 RSS 上升就给所有 HttpClient 内存问题套上这个结论。

两项期限都不要用零来表达“不复用”

若业务允许在 handler 生命周期内正常管理连接,可为寿命和空闲期限设置合适的非零值。下面只是配置示例,时间需要按实际 DNS、代理和探测要求选择:

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
};

两项要一起核对:条件里用的是“寿命为零或空闲期限为零”,只改其中一个,另一个仍为零时可能继续触发。InfiniteTimeSpan 与 Zero 也不是同一个含义;期限不应在请求已经开始后再修改。

非零寿命不会强行切断正在服务请求的连接,也不是一个请求超时设置。如果业务必须为每次探测隔离连接,需要在探测结束后正确释放相应 handler,并验证资源回落;不要单靠设置 Zero 来代替所有权管理。

对普通长期服务,优先沿用经过管理的长生命周期客户端或现有 HttpClientFactory 方案,避免每个请求重建 handler。明确 HttpClient 是否拥有 handler:采用 disposeHandler: false 时,释放 client 不会替调用方释放 handler,必须由真正的所有者处理。

用同样的 HTTP/2 负载验证释放

保持请求次数、并发和真实协议一致,比较原零期限配置与修改后的配置。在响应正文按约定读取或释放、client 和 handler 都结束后,观察套接字是否仍随每轮探测持续累积,再结合网络诊断确认连接池确实进入释放路径。

不要只看托管堆,也不要把服务端同时存在的连接端点全算成客户端单独泄漏的数量。对照测试若使用同一进程内的服务端,应分别识别客户端和服务端描述符。

保留原配置和部署包,先受控切换,再观察业务隔离、DNS 更新与请求取消是否仍符合要求。关闭证书验证、停止读取正文或随意降级 HTTP 协议,都不是这条池登记缺陷的必要修复。