用 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.WhenAll10 万请求,大概率导致本地
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<string></string>流式生成,配合
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 更可靠。
