c# 如何用 C# 模拟大量并发用户请求进行压力测试

来源:这里教程网 时间:2026-02-21 17:38:33 作者:

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<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 更可靠。

相关推荐