.NET HttpClient.SendAsync调用缓慢问题排查求助
问题场景
系统架构为客户端→服务器→服务端,整体功能正常,但服务器与服务端间的请求耗时过长(1-1.5秒),服务端本身响应速度很快。使用NSwag生成的GeocodingClient类调用服务,经测量var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);这一行执行缓慢,其他环节无异常。
服务端日志显示请求到达时间存在延迟,但用Postman直接调用该服务时响应快速正常。此外,结构类似的其他服务调用运行流畅。
生成的GeocodingClient类代码
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0))")] public partial class GeocodingClient { private string _baseUrl = ""; private System.Net.Http.HttpClient _httpClient; private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings; public GeocodingClient(string baseUrl, System.Net.Http.HttpClient httpClient) { BaseUrl = baseUrl; _httpClient = httpClient; _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() => { var settings = new Newtonsoft.Json.JsonSerializerSettings(); UpdateJsonSerializerSettings(settings); return settings; }); } public string BaseUrl { get { return _baseUrl; } set { _baseUrl = value; } } //... }
相关调用方法代码
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param> /// <returns>Success</returns> /// <exception cref="ApiException">A server side error occurred.</exception> public async System.Threading.Tasks.Task<GetCanalizationLocationsBetweenAreaResponse> GetCanalizationLocationsBetweenAreaAsync(GetCanalizationLocationsBetweenAreaRequest body, System.Threading.CancellationToken cancellationToken) { var urlBuilder_ = new System.Text.StringBuilder(); urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/TechnicalObjectQuery/GetCanalizationLocationsBetweenArea"); var client_ = _httpClient; try { using (var request_ = new System.Net.Http.HttpRequestMessage()) { var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value)); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; request_.Method = new System.Net.Http.HttpMethod("POST"); request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); PrepareRequest(client_, request_, urlBuilder_); var url_ = urlBuilder_.ToString(); request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); PrepareRequest(client_, request_, url_); var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); try { var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); if (response_.Content != null && response_.Content.Headers != null) { foreach (var item_ in response_.Content.Headers) headers_[item_.Key] = item_.Value; } ProcessResponse(client_, response_); var status_ = ((int)response_.StatusCode).ToString(); if (status_ == "200") { var objectResponse_ = await ReadObjectResponseAsync<GetCanalizationLocationsBetweenAreaResponse>(response_, headers_).ConfigureAwait(false); return objectResponse_.Object; } else if (status_ != "200" && status_ != "204") { var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null); } return default(GetCanalizationLocationsBetweenAreaResponse); } finally { if (response_ != null) response_.Dispose(); } } } finally { } }
可能原因
- HttpClient实例未复用:每次调用创建新的
HttpClient会导致TCP连接无法复用,每次请求都要重新建立TCP连接(三次握手),大幅增加延迟。Postman默认复用连接,因此速度更快。 - DNS解析延迟:服务器端DNS缓存未配置或缓存时长过短,导致每次请求都重新解析DNS;而Postman可能已缓存解析结果。
- 请求头差异:NSwag生成的请求携带的头与Postman不一致,可能触发服务端额外的校验或处理逻辑,导致延迟。
- 代理设置不一致:应用程序使用的代理与Postman不同,请求路由走了更慢的链路。
- TLS握手未复用:
HttpClient未配置TLS会话复用,每次请求都重新完成TLS握手,增加耗时。
解决方案
复用HttpClient实例
确保整个应用生命周期内复用同一个HttpClient实例,可通过依赖注入注册为单例:services.AddSingleton<GeocodingClient>(sp => { var httpClient = sp.GetRequiredService<HttpClient>(); httpClient.BaseAddress = new Uri("你的服务基础地址"); return new GeocodingClient("你的服务基础地址", httpClient); });配置DNS缓存
在HttpClientHandler中设置DNS缓存时长,减少解析次数:var handler = new HttpClientHandler { DnsRefreshTimeout = TimeSpan.FromMinutes(30) // 根据实际情况调整缓存时长 }; var httpClient = new HttpClient(handler);对齐请求头
用抓包工具(如Fiddler、Wireshark)对比Postman和应用的请求头,在PrepareRequest方法中调整应用的请求头,确保与Postman一致(比如User-Agent、Accept-Encoding等)。统一代理设置
检查并调整应用的代理配置,确保与Postman一致,必要时关闭不必要的代理:var handler = new HttpClientHandler { UseProxy = false, // 若需使用代理,配置正确地址 // Proxy = new WebProxy("http://你的代理地址:端口") };启用TLS会话复用
配置HttpClientHandler启用TLS会话复用,减少握手耗时:var handler = new HttpClientHandler { SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13, EnableTlsSessionResumption = true };检查PrepareRequest方法
查看自动生成的PrepareRequest方法是否包含额外逻辑(如自定义头添加、请求修改),这些逻辑可能导致不必要的延迟。
内容的提问来源于stack exchange,提问作者Fovezer

