应用服务(Web App)实战:用 .NET 代码把 Connection 耗尽与 SNAT 耗尽演练一次

发布时间:2026/6/29 20:51:49

应用服务(Web App)实战:用 .NET 代码把 Connection 耗尽与 SNAT 耗尽演练一次 概念还是抽象所以我做了一个 .NET Demo把问题拆成四个小实验实验 1Connection 耗尽 — 每次 new HttpClient()实验 2 Connection 优化 — IHttpClientFactory 复用实验 3 SNAT 耗尽 — 关闭连接池 Connection: close实验 4 SNAT 优化 — 单例 HttpClient MaxConnectionsPerServer ≤ 128问题解答实验 1让 App Service Instance 的出站连接快速耗尽反例很简单每个请求都new HttpClient()而且不复用、不释放。这样每个请求都会带来新的 handler 和连接池短时间内大量并发时worker 上的 TCP 连接资源会迅速堆积。实验1的代码片段// BAD: new HttpClient 每次都创建handler 与 socket 累积 app.MapGet(/api/demo/connection-bad, async ( int count, int concurrency, string? url) { return await Runner.RunAsync(count, concurrency, async _ { var client new HttpClient(); // 每次新建 using var resp await client.GetAsync(url); resp.EnsureSuccessStatusCode(); }); });异常错误信息HttpRequestException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. (blog.mylubu.com:443) -- SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.实验结果截图实验 2Connection 优化用单例HttpClient/IHttpClientFactory复用优化思路是只保留少量长期存活的连接让请求复用这些连接。复用HttpClient或使用IHttpClientFactory用PooledConnectionLifetime定期刷新连接避免 DNS 漂移用MaxConnectionsPerServer控制到同一目标的物理连接数。实验2的代码片段// GOOD: 在 DI 中注册一次 builder.Services.AddHttpClient(pooled, c c.Timeout TimeSpan.FromSeconds(30)) .ConfigurePrimaryHttpMessageHandler(() new SocketsHttpHandler { PooledConnectionLifetime TimeSpan.FromMinutes(2), // 解决 DNS 漂移 MaxConnectionsPerServer 20, // 受限连接池 }); app.MapGet(/api/demo/connection-good, async ( int count, int concurrency, string? url, IHttpClientFactory factory) { var client factory.CreateClient(pooled); // 从工厂复用 return await Runner.RunAsync(count, concurrency, async _ { using var resp await client.GetAsync(url); resp.EnsureSuccessStatusCode(); }); });关键优化vs 实验 1不再 new HttpClient()用IHttpClientFactory.CreateClient(pooled)拿到共享实例。配置 PooledConnectionLifetime 2min定期回收连接避免 DNS 漂移问题。配置 MaxConnectionsPerServer 20可在上方参数区动态调节把单一目的端的并发物理连接控制在安全水位。结果N 个 HTTP 请求 ↔ 至多 20 条物理 TCP 流socket 不再泄漏。实验结果截图实验 3让 App Service Instance 的 SNAT Port 耗尽Connection 优化解决的是 worker 本地资源但 SNAT 是另一层限制。只要每个 HTTP 请求都是一条新的 TCP 流出站负载均衡器仍然要不断分配新的 SNAT 端口。App Service 单实例通常按128 个 SNAT 端口估算耗尽后新连接会卡住直到超时。这个反例通过禁用连接池 Connection: close强制每个请求都新建 TCP 连接。实验3的代码片段// BAD: 禁用连接池 Connection: close 每个请求都是一条全新 TCP 流 app.MapGet(/api/demo/snat-bad, async ( int count, int concurrency, string? url) { return await Runner.RunAsync(count, concurrency, async _ { using var handler new SocketsHttpHandler { PooledConnectionLifetime TimeSpan.Zero, // 禁用连接池 }; using var client new HttpClient(handler); client.DefaultRequestHeaders.ConnectionClose true; // 强制断开 using var resp await client.GetAsync(url); resp.EnsureSuccessStatusCode(); }); });异常错误信息HttpRequestException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. (blog.mylubu.com:443)--SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.实验结果截图实验测试SNAT的端口占用数 128 个实验 4SNAT 优化Keep-Alive 复用 MaxConnectionsPerServer ≤ 128优化方式也很直接保留连接池允许请求复用已有 TCP 连接。去掉Connection: close保留 Keep-Alive启用连接池不再把PooledConnectionLifetime设为Zero控制MaxConnectionsPerServer让同一目标的物理连接数低于 SNAT 安全水位。实验4的代码片段// GOOD: 注册单例 HttpClient所有请求共享一个连接池 builder.Services.AddSingletonSharedHttpClient(_ { var handler new SocketsHttpHandler { PooledConnectionLifetime TimeSpan.FromMinutes(2), // 启用连接池 PooledConnectionIdleTimeout TimeSpan.FromSeconds(30), // 空闲回收 MaxConnectionsPerServer 20, // 128 }; return new SharedHttpClient(new HttpClient(handler)); }); app.MapGet(/api/demo/snat-good, async ( int count, int concurrency, string? url, SharedHttpClient shared) { return await Runner.RunAsync(count, concurrency, async _ { // 不设置 ConnectionClose keep-alive 复用 using var resp await shared.Client.GetAsync(url); resp.EnsureSuccessStatusCode(); }); });关键优化vs 实验 3移除 Connection: close保留 keep-alive让服务端不会立刻关闭连接。启用连接池PooledConnectionLifetime 2min而不是Zero。添加 PooledConnectionIdleTimeout 30s空闲连接超时回收但活跃连接长保留。MaxConnectionsPerServer 20可在上方参数区动态调节硬上限远低于 128 SNAT 安全估算确保不会撞墙。HttpClient 注册为 Singleton整个进程共享一个所有请求复用同一连接池。实验结果截图总结在以上实验中观察App Service的Connects指标变动当服用链接后肉眼可见connections指标的快速下降。

相关新闻