7

.NET Core HttpClient请求异常详细情况分析

 3 years ago
source link: https://www.cnblogs.com/CreateMyself/p/14852464.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

.NET Core HttpClient请求异常详细情况分析

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

场景:将相关厂家地磁设备(停车进出场)推送数据,转发至对接方。最近一个星期经过观察会出现两种异常情况,一种是请求连接操作被取消,另外一种则是请求处理过程中操作被取消,具体异常信息请见如下图

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

问题排查分析

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

从上述两张图来看,最多的一天也才90来万,最近几天请求失败的数据大概2百来条,从我们平台打印日志来看,每秒请求大致是10个左右,通过HTTP对接完全可以承载。然后,因为我们将数据(JSON,数据大小几乎可以忽略不计)转发到对接方,对接方拿到数据后不会进行任何额外处理,直接存储,所以我们请求超时时间30s,怎么会导致超时而引发异常呢?很奇怪,没辙了,只能拿起终极武器,tcp抓包分析,重新学习了tcp/ip协议族一波

WireShark抓包分析

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

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,

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

static class Program
{
    static HttpClient httpClient = CreateHttpClient();
    static Program()
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

        ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, error) => true,
    }

    static async Task Main(string[] args)
    {
        await httpClient.PostAsync("", new StringContent(""));
    }

    static HttpClient CreateHttpClient()
    {
        var client = new HttpClient(new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true
        })
        {
            Timeout = TimeSpan.FromSeconds(30)
        };

        client.DefaultRequestHeaders.Accept.Clear();

        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("ContentType", "application/json");
        return client;
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public static void SetTcpKeepAlive(bool enabled, int keepAliveTime, int keepAliveInterval)
{
    if (enabled)
    {
        if (keepAliveTime <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(keepAliveTime));
        }
        if (keepAliveInterval <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(keepAliveInterval));
        }
    }
}

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

  public class ServicePointManager
  {
      public const int DefaultNonPersistentConnectionLimit = 4;
      public const int DefaultPersistentConnectionLimit = 2;
 
      private ServicePointManager() { }
  }

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

static class Program
{
    static HttpClient httpClient = CreateHttpClient();

    static Program()
    {
        //默认连接数限制为2,增加连接数限制
        ServicePointManager.DefaultConnectionLimit = 512;

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

    static async Task Main(string[] args)
    {
        await httpClient.PostAsync("", new StringContent(""));

        Console.WriteLine("Hello World!");
    }

    static HttpClient CreateHttpClient()
    {
        var client = new HttpClient(new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true,
            SslProtocols = SslProtocols.Tls12
        })
        {
            Timeout = TimeSpan.FromSeconds(30)
        };

        client.DefaultRequestHeaders.Accept.Clear();
        //增加保活机制,表明连接为长连接
        client.DefaultRequestHeaders.Connection.Add("keep-alive");
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("ContentType", "application/json");
        return client;
    }
}

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

最新更新:2021-06-06 20:56

本文解决方案存在不严谨的地方,还有待进一步考究,待我查漏补缺后继续发表博客更新......

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

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

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

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

scan.gif
为了方便大家在移动端也能看到我分享的博文,现已注册个人公众号,扫描上方左边二维码即可,欢迎大家关注,有时间会及时分享相关技术博文。

感谢花时间阅读此篇文章,如果您觉得这篇文章你学到了东西也是为了犒劳下博主的码字不易不妨打赏一下吧,让楼主能喝上一杯咖啡,在此谢过了!
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!
本文版权归作者和博客园共有,来源网址:http://www.cnblogs.com/CreateMyself)/欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK