.NET Core HttpClient請求異常詳細情況分析

Jeffcky發表於2021-06-05

前言

最近專案上每天間斷性捕獲到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;
    }
}

進行如上設定後,通過一天觀察,再未出現相關異常,至此問題解決告一段落,希望沒有後續......

總結

強烈建議:利用HttpClient傳送請求設定持久化連線和根據實際業務評估增加連線數,而非預設的持久化連線為2,非持久化連線為4,否則極易出現相關異常。

 

若非常清楚預設連線數限制,可能並算不上什麼問題,也不存在如此諸多分析,不過對於我而言,收穫的是在問題排查過程中,對可能干擾資訊的過濾、篩選、確認以及對網路協議進一步的加深。 

 

引發思考:利用HttpClientFactory建立HttpClient是否有預設連線限制,據我所知,好像可以通過屬性MaxConnectionsPerServer來配置

 

? 在.NET Core和.NET Framework中相關配置還是有些變化,最好是根據對應版本在官網上確認下

相關文章