应复用 HttpClient 实例或使用 IHttpClientFactory,配合 Task.WhenAll 实现高并发异步请求;避免 Parallel.For 同步阻塞、需用 SemaphoreSlim 限流、预热 DNS 并调大 MaxConnectionsPerServer。

用 HttpClient + Task.WhenAll 模拟并发请求最直接
直接创建多个 HttpClient 实例并行发请求,是最贴近“大量用户”语义的写法。但要注意:不能为每个请求都新建一个 HttpClient,否则会快速耗尽端口(SocketException: Address already in use)。必须复用单个静态实例,或使用 IHttpClientFactory(推荐)。
实操建议:
- 在
Program.cs或服务注册阶段配置IHttpClientFactory,例如:services.AddHttpClient("loadtest", client => { client.BaseAddress = new Uri("https://api.example.com/"); client.Timeout = TimeSpan.FromSeconds(10); }); - 压测逻辑中通过
IHttpClientFactory.CreateClient("loadtest")获取客户端,而非new HttpClient() - 用
Task.WhenAll启动所有请求任务,例如发起 1000 个并发 GET:var tasks = Enumerable.Range(0, 1000) .Select(_ => client.GetAsync("/status")) .ToArray(); await Task.WhenAll(tasks);
Parallel.For 不适合模拟 HTTP 并发用户
有人误用 Parallel.For 循环内同步调用 HttpClient.GetAsync().Result,这会导致线程阻塞、线程池饥饿,实际并发数远低于预期,且极易触发 AggregateException 包裹超时或连接拒绝错误。
关键区别:
-
Parallel.For是 CPU 密集型并行,适用于计算;HTTP 请求是 I/O 密集型,必须用异步非阻塞方式 -
.Result或.Wait()会死锁 ASP.NET Core 同步上下文(尤其在 Web 项目中) - 即使在外层控制台应用中能跑通,吞吐量也远不如纯
Task并发
控制并发数避免打垮自己或目标服务
无节制地 Task.WhenAll 10 万请求,大概率导致本地 OutOfMemoryException、目标服务 503、或中间代理(如 Nginx)主动断连。真实压测需分批、限流。
推荐做法:
- 用
SemaphoreSlim控制最大并发请求数,例如限制同时最多 200 个请求:var semaphore = new SemaphoreSlim(200); var tasks = urls.Select(async url => { await semaphore.WaitAsync(); try { return await client.GetAsync(url); } finally { semaphore.Release(); } }); - 记录每批完成时间、成功/失败数、响应延迟(用
Stopwatch包裹GetAsync) - 避免把全部 URL 一次性加载进内存——用
IEnumerable流式生成,配合Chunk分批提交
别忽略 DNS 缓存和连接池行为
HttpClient 默认复用 TCP 连接,但首次请求仍要走 DNS 查询。如果压测域名解析慢(比如指向本地 hosts 的测试环境),大量并发请求可能卡在 Dns.GetHostAddressesAsync 上,表现为大量请求超时而非连接拒绝。
应对方式:
- 提前用
Dns.GetHostAddressesAsync("api.example.com")预热 DNS 缓存 - 设置
HttpClientHandler.MaxConnectionsPerServer(默认 2,太低!),例如设为 1000:var handler = new HttpClientHandler { MaxConnectionsPerServer = 1000 }; services.AddHttpClient("loadtest").ConfigurePrimaryHttpMessageHandler(() => handler); - 确认目标服务未开启连接数限制(如 Kestrel 的
MaxConcurrentConnections)
真正难调的不是并发数,而是让每个请求都走真实网络路径、不被本地 DNS 或连接池策略意外截断。先跑通 10 个并发,再逐级加压,比一上来就设 10000 更可靠。










