如何在.NET Core中测量HTTP请求各阶段耗时?
如何在.NET Core HttpClient中采集类似curl的HTTP请求细分耗时
.NET Core 中可以通过三种主流方案采集和curl维度对齐的请求耗时数据,不需要依赖第三方组件,完全基于框架原生能力实现:
方案1:基于SocketsHttpHandler回调采集(最接近curl的底层耗时精度)
.NET Core 2.1+ 版本HttpClient默认底层实现就是SocketsHttpHandler,它暴露了完整的请求生命周期回调,可以直接对应到curl的各个耗时维度:
- DNS解析(lookup)耗时:
ResolveAddressAsyncStart回调触发到ResolveAddressAsyncEnd回调触发的时间差 - TCP连接(connect)耗时:
ConnectAsyncStart回调触发到ConnectAsyncEnd回调触发的时间差 - TLS握手(appconnect)耗时:
EstablishSslConnectionAsyncStart回调触发到EstablishSslConnectionAsyncEnd回调触发的时间差 - 预传输(pretransfer)耗时:连接建立完成到请求头全部发送完成的时间差
- 首包响应(starttransfer)耗时:请求发送完成到收到第一个响应字节的时间差
- 总响应耗时:请求发起到底层响应流读取完成的总时长
参考实现代码:
var customHandler = new SocketsHttpHandler { ConnectCallback = async (context, cancellationToken) => { // 采集DNS解析耗时 var dnsStartTs = Stopwatch.GetTimestamp(); var ipAddresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, cancellationToken); var dnsCost = Stopwatch.GetElapsedTime(dnsStartTs); // 此处写入你自己的日志/监控存储 // 采集TCP连接耗时 var connectStartTs = Stopwatch.GetTimestamp(); var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(ipAddresses, context.DnsEndPoint.Port, cancellationToken); var connectCost = Stopwatch.GetElapsedTime(connectStartTs); // 此处写入你自己的日志/监控存储 // 采集HTTPS场景的TLS握手耗时 if (context.InitialRequestMessage.RequestUri.Scheme == Uri.UriSchemeHttps) { var sslStartTs = Stopwatch.GetTimestamp(); var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true)); await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { TargetHost = context.DnsEndPoint.Host }, cancellationToken); var sslCost = Stopwatch.GetElapsedTime(sslStartTs); // 此处写入你自己的日志/监控存储 return sslStream; } return new NetworkStream(socket, ownsSocket: true); } }; // 用自定义Handler初始化HttpClient即可生效 var httpClient = new HttpClient(customHandler);
方案2:自定义DelegatingHandler上层埋点(适合业务侧快速接入)
如果不需要底层网络维度的细粒度耗时,只需要拆分请求发送、响应接收的核心时长,可以直接基于HttpClient的管道机制实现无侵入埋点:
public class RequestTimingHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var totalStartTs = Stopwatch.GetTimestamp(); // 采集首包响应耗时 var firstByteStartTs = Stopwatch.GetTimestamp(); var response = await base.SendAsync(request, cancellationToken); var firstByteCost = Stopwatch.GetElapsedTime(firstByteStartTs); // 此处写入你自己的日志/监控存储 // 采集响应内容读取耗时 var contentReadStartTs = Stopwatch.GetTimestamp(); _ = await response.Content.ReadAsByteArrayAsync(cancellationToken); var contentReadCost = Stopwatch.GetElapsedTime(contentReadStartTs); // 采集总耗时 var totalCost = Stopwatch.GetElapsedTime(totalStartTs); // 此处写入你自己的日志/监控存储 return response; } } // 依赖注入场景下直接注册到HttpClient管道即可 builder.Services.AddHttpClient("YourServiceClient") .AddHttpMessageHandler<RequestTimingHandler>();
方案3:监听.NET内置DiagnosticSource事件(全局无侵入采集)
如果需要对项目中所有HttpClient请求做统一监控,不需要修改任何业务代码,可以直接监听框架默认抛出的诊断事件:
System.Net.Http.HttpRequest.Start:请求开始触发System.Net.Http.HttpRequest.Stop:请求结束触发System.Net.Http.HttpConnection.Connect.Start:新建连接开始触发System.Net.Http.HttpConnection.Connect.Stop:新建连接结束触发System.Net.Http.HttpConnection.WaitForAvailableConnection.Start:等待连接池空闲连接开始触发
所有事件都自带高精度时间戳,直接计算两个关联事件的时间差即可拿到对应维度的耗时数据。
注意:如果HttpClient开启了连接池复用,TCP连接、TLS握手耗时只会在新建连接的时候产生,复用连接的请求这两个维度耗时为0,和curl的默认行为完全一致。
内容的提问来源于stack exchange,提问作者Shanmugam M
相关产品推荐
相关产品推荐

