.NET Core HttpClient请求异常分析

开发 后端
最近项目上每天间断性捕获到HttpClient请求异常,感觉有点奇怪,于是乎观察了两三天,通过日志以及对接方沟通确认等等,查看对应版本源码,尝试添加部分配置发布后,观察十几小时暂无异常情况出现,貌似问题已得到解决,若有后续继续更新。

[[403870]]

本文转载自微信公众号「JeffckyShare」,作者Jeffcky 。转载本文请联系JeffckyShare公众号。

最近项目上每天间断性捕获到HttpClient请求异常,感觉有点奇怪,于是乎观察了两三天,通过日志以及对接方沟通确认等等,查看对应版本源码,尝试添加部分配置发布后,观察十几小时暂无异常情况出现,貌似问题已得到解决,若有后续继续更新。

HttpClient来源:netstandard2.0

场景:将相关厂家地磁设备(停车进出场)推送数据,转发至对接方。

最近一个星期经过观察会出现两种异常情况,一种是请求连接操作被取消,另外一种则是请求处理过程中操作被取消,具体异常信息请见如下图

我们知道HttpClient默认超时时间为100s,但项目默认设置请求超时时间为30s,初次分析异常情况来看,请求超时导致请求连接被取消异常,首先我telnet对接方端口通畅,于是乎与对接方交涉,是否存在从请求到对接方接口有额外前置处理,以及网络是否存在波动等等,排查得知相关猜测都予以否决,网络无任何问题

问题排查分析

既然网络没有任何问题,难道是对接方即服务端处理数据量巨大,导致请求应答超时?于是乎对接方甩出几张最近消息接收数据

从上述两张图来看,最多的一天也才90来万,最近几天请求失败的数据大概2百来条,从我们平台打印日志来看,每秒请求大致是10个左右,通过HTTP对接完全可以承载。

然后,因为我们将数据(JSON,数据大小几乎可以忽略不计)转发到对接方,对接方拿到数据后不会进行任何额外处理,直接存储,所以我们请求超时时间30s,怎么会导致超时而引发异常呢?很奇怪

没辙了,只能拿起终极武器,tcp抓包分析,重新学习了tcp/ip协议族一波

WireShark抓包分析

为打开分析上述pcap文件,提前安装抓包软件WireShark最新版本,由于软件项目部署在Linux上,我们通过如下命令进行抓包

  1. tcpdump -i any port 1443 -w exception.pcap 

从如下抓包信息可以直接知道,对接方使用了HTTPS协议,具体请看如下图

如图

默认打开如上图所示,其中time为时间戳,为找到我们指定时间点(2021-06-04 15:46:17.296),通过tab:视图-时间显示格式-日期和时间

接下来我们找到包文件中导致请求被取消异常的具体时间节点(2021-06-04 15:46:17.296)

在TCP协议中RST表示复位,用于异常时关闭连接。在发送RST包关闭连接时,不必等待缓冲区的包都发出去,直接丢弃缓冲区的包而发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

好像有点眉头了,继续往下看

好家伙,结合这张图来看,基本上可以得出结论:原来是我们平台主动重置了连接,紧接着又开始了多次进行三次握手连接即(SYN、SYN/ACK、ACK)

示例代码分析

推送逻辑是在类库中使用HttpClient,所以没有使用HttpClientFactory,因此定义静态变量来使用HttpClient,而非每一个请求就实例化一个HttpClient

接下来我们来详细分析项目示例代码并对其进行改进

  1. static class Program 
  2.     static HttpClient httpClient = CreateHttpClient(); 
  3.     static Program() 
  4.     { 
  5.         ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 
  6.  
  7.         ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, error) => true
  8.     } 
  9.  
  10.     static async Task Main(string[] args) 
  11.     { 
  12.         await httpClient.PostAsync("", new StringContent("")); 
  13.     } 
  14.  
  15.     static HttpClient CreateHttpClient() 
  16.     { 
  17.         var client = new HttpClient(new HttpClientHandler 
  18.         { 
  19.             ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true 
  20.         }) 
  21.         { 
  22.             Timeout = TimeSpan.FromSeconds(30) 
  23.         }; 
  24.  
  25.         client.DefaultRequestHeaders.Accept.Clear(); 
  26.  
  27.         client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 
  28.         client.DefaultRequestHeaders.Add("ContentType""application/json"); 
  29.         return client; 
  30.     } 

若对接方仅使用HTTPS协议,无需验证证书,最好是忽略证书验证,否则有可能会引起建立验证证书连接异常,即添加

  1. ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true 

我们观察上述代码,有两个地方都对证书验证进行了设置,一个是在静态构造函数中ServicePointManager(简称SP),另外则在实例化HttpClient构造函数中即HttpClientHandler(简称HCH),那么这二者是否有使用上的限制呢?

在.NET Framework中,内置的HttpClient建立在HttpWebRequest之上,因此可以使用SP来配置

在.NET Core中,通过SP配置证书信息仅影响HttpWebRequest,而对HttpClient无效,需通过HCH配置来达到相同目的

所以去除在静态构造函数中对忽略证书的配置,改为在HttpClientHandler中

  1. var client = new HttpClient(new HttpClientHandler 
  2.     ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true
  3.     SslProtocols = SslProtocols.Tls12 
  4. }) 

回到本文的话题,为什么会重置连接即主动关闭连接呢?我们已分析过,和上述配置30s超时没有关系,主要有两方面原因

翻开并温习《图解HTTP》一书,如果请求频繁,最好建立持久连接,减少TCP连接的重复建立和断开所造成的额外开销,从而减轻服务端负载即重用HTTP连接,也称为Http keep-alive,持久连接的特点是,只要任意一方没有明确提出断开连接,否则保持TCP连接状态

配置keep-alive我们俗称为保活机制,所以在默认请求头中添加如下一行

  1. //增加保活机制,表明连接为长连接 
  2. client.DefaultRequestHeaders.Connection.Add("keep-alive"); 

上述只是在报文头中添加持久化连接标识,但不意味着就一定生效,因为默认是禁用持久化连接,所以为了保险起见,添加如下代码

  1. //启用保活机制(保持活动超时设置为 2 小时,并将保持活动间隔设置为 1 秒。) 
  2. ServicePointManager.SetTcpKeepAlive(true, 7200000, 1000); 

有个让我很疑惑的问题,通过查看设置启用持久化连接源码得知,这样设置意义在哪里?没弄明白,源码如下

  1. public static void SetTcpKeepAlive(bool enabled, int keepAliveTime, int keepAliveInterval) 
  2.     if (enabled) 
  3.     { 
  4.         if (keepAliveTime <= 0) 
  5.         { 
  6.             throw new ArgumentOutOfRangeException(nameof(keepAliveTime)); 
  7.         } 
  8.         if (keepAliveInterval <= 0) 
  9.         { 
  10.             throw new ArgumentOutOfRangeException(nameof(keepAliveInterval)); 
  11.         } 
  12.     } 

最关键的一点则是默认持久化连接数为2,非持久化连接为4

  1. public class ServicePointManager 
  2.  { 
  3.      public const int DefaultNonPersistentConnectionLimit = 4; 
  4.      public const int DefaultPersistentConnectionLimit = 2; 
  5.  
  6.      private ServicePointManager() { } 
  7.  } 

那么问题是否就已很明了,项目中使用非持久化连接,即连接为4,未深究源码具体细节,大胆猜想一下,若连接大于4,是否会出现将此前连接主动关闭,重建新的连接请求呢?

最终我们讲原始代码修改为如下形式

  1. static class Program 
  2.     static HttpClient httpClient = CreateHttpClient(); 
  3.  
  4.     static Program() 
  5.     { 
  6.         //默认连接数限制为2,增加连接数限制 
  7.         ServicePointManager.DefaultConnectionLimit = 512; 
  8.  
  9.         //启用保活机制(保持活动超时设置为 2 小时,并将保持活动间隔设置为 1 秒。) 
  10.         ServicePointManager.SetTcpKeepAlive(true, 7200000, 1000); 
  11.     } 
  12.  
  13.     static async Task Main(string[] args) 
  14.     { 
  15.         await httpClient.PostAsync("", new StringContent("")); 
  16.  
  17.         Console.WriteLine("Hello World!"); 
  18.     } 
  19.  
  20.     static HttpClient CreateHttpClient() 
  21.     { 
  22.         var client = new HttpClient(new HttpClientHandler 
  23.         { 
  24.             ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true
  25.             SslProtocols = SslProtocols.Tls12 
  26.         }) 
  27.         { 
  28.             Timeout = TimeSpan.FromSeconds(30) 
  29.         }; 
  30.  
  31.         client.DefaultRequestHeaders.Accept.Clear(); 
  32.         //增加保活机制,表明连接为长连接 
  33.         client.DefaultRequestHeaders.Connection.Add("keep-alive"); 
  34.         client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 
  35.         client.DefaultRequestHeaders.Add("ContentType""application/json"); 
  36.         return client; 
  37.     } 

进行如上设置后,通过一天观察,再未出现相关异常,至此问题解决告一段落,希望没有后续......

强烈建议:利用HttpClient发送请求设置持久化连接和根据实际业务评估增加连接数,而非默认的持久化连接为2,非持久化连接为4,否则极易出现相关异常。

引发思考:利用HttpClientFactory创建HttpClient是否有默认连接限制,据我所知,好像可以通过属性MaxConnectionsPerServer来配置

若非常清楚默认连接数限制,可能并算不上什么问题,也不存在如此诸多分析,不过对于我而言,收获的是在问题排查过程中,对可能干扰信息的过滤、筛选、确认以及对网络协议进一步的理解加深。

 

在.NET Core和.NET Framework中相关配置还是有些变化,最好是根据对应版本在官网上确认下

 

责任编辑:武晓燕 来源: JeffckyShare
相关推荐

2021-06-06 13:07:06

.NETWindowsLinux

2021-12-02 07:25:58

ASP.NET CorAjax请求

2021-05-11 15:50:28

ASP.NET单元测试

2024-09-10 08:13:16

Asp项目轻量级

2013-08-22 17:10:09

.Net异常处理

2024-09-24 17:34:05

2024-08-29 09:11:38

GolangEOF连接池

2024-05-13 09:32:06

拦截器HTTP中间件

2016-12-06 10:23:24

.NETCore首例Office

2024-11-12 07:28:39

2011-04-13 14:57:11

ASP.NET请求处理

2019-08-07 14:25:21

.NET.NET Core 3编程语言

2018-08-20 08:03:46

跨平台 Web操作系统

2021-12-05 18:22:20

.NETLS Cipher套件

2024-06-11 09:00:00

异步编程代码

2024-09-09 07:37:51

AspJWT权限

2024-06-27 10:48:48

2024-09-30 09:48:41

RabbitMQ消息中间件

2009-07-29 09:53:24

ASP.NET异常管理

2024-04-15 16:11:33

C#HTTP请求.NET
点赞
收藏

51CTO技术栈公众号