2015-01-01 99 views
17

我有任何地方從10-150長的生活類對象調用使用HttpClient執行簡單的HTTPS API調用的方法。一個PUT調用示例:HttpClientHandler/HttpClient內存泄漏

using (HttpClientHandler handler = new HttpClientHandler()) 
{ 
    handler.UseCookies = true; 
    handler.CookieContainer = _Cookies; 

    using (HttpClient client = new HttpClient(handler, true)) 
    { 
     client.Timeout = new TimeSpan(0, 0, (int)(SettingsData.Values.ProxyTimeout * 1.5)); 
     client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Statics.UserAgent); 

     try 
     { 
      using (StringContent sData = new StringContent(data, Encoding.UTF8, contentType)) 
      using (HttpResponseMessage response = await client.PutAsync(url, sData)) 
      { 
       using (var content = response.Content) 
       { 
        ret = await content.ReadAsStringAsync(); 
       } 

      } 
     } 
     catch (ThreadAbortException) 
     { 
      throw; 
     } 
     catch (Exception ex) 
     { 
      LastErrorText = ex.Message; 
     } 
    } 
} 

2-3小時後運行這些方法,其中包括通過using陳述妥善處置,該計劃已穆斯特1GB的內存,1.5GB,並最終以各種崩潰的內存不足錯誤。很多時候,連接都是通過不可靠的代理服務器進行的,因此連接可能無法按預期完成(超時和其他錯誤很常見)。

.NET內存事件探查器指出HttpClientHandler是這裏的主要問題,指出它具有'直接代表根'的廢棄實例(紅色感嘆號)和'已處理但仍未GCed的實例'(黃色感嘆號)。探查器指示的代表已根據AsyncCallback s,源自HttpWebRequest。

它也可能與RemoteCertValidationCallback有關,它與HTTPS證書驗證有關,因爲TlsStream是「Disposed but not GCed」根目錄下的一個對象。

考慮到這一切 - 我如何更正確地使用HttpClient並避免這些內存問題?我應該每小時強制一次GC.Collect()嗎?我知道這被認爲是不好的做法,但我不知道如何回收這個不太適合處理的內存,並且對於這些短期對象來說更好的使用模式對我來說並不明顯,因爲它似乎成爲.NET對象本身的一個缺陷。


UPDATE 強制GC.Collect()沒有效果。

進程的託管字節總數最多保持20-30 MB左右,而進程整體內存(在任務管理器中)持續攀升,表明存在非託管內存泄漏。因此,這種使用模式正在創建一個非託管內存泄漏。

我已經嘗試創建HttpClient和HttpClientHandler的每個建議的類級別的實例,但是這沒有可觀的效果。即使我將它們設置爲課程級別,由於代理設置通常需要更改,因此它們仍會重新創建並很少重複使用。一旦請求被啓動,HttpClientHandler不允許修改代理設置或任何屬性,所以我不斷地重新創建處理程序,就像最初對獨立的using聲明所做的那樣。

HttpClienthandler仍然與「直接委託根」處置爲AsyncCallback - > HttpWebRequest。我開始懷疑,也許HttpClient不是專爲快速請求和短期生活對象而設計的。沒有盡頭,希望有人建議使用HttpClientHandler可行。


存儲器剖析鏡頭: Initial stack indicating that HttpClientHandler is the root issue, having 304 live instances that should have been GC'd

enter image description here

enter image description here

+0

你爲什麼要在每次調用中放置'HttpClientHandler'和'HttpClient'? 'HttpClient'應該是整個應用程序中的一個長壽命對象(因此,'HttpClientHandler'也應該是這樣的,這樣,你只需要生成一個實例。 –

+0

我剪掉了代碼中不重要的部分,但頭部根據請求而波動,並且代理會根據連接成功/失敗而改變,HttpClientHandler對象的大量維護都會發生,因此我認爲每次只重新創建一次會比較簡單,但它仍然不能滿足問題爲什麼這些對象不能重複創建而不會重複創建 – user1111380

+0

如果確實嘗試了GC.collect(),您是否看到正在收集這些TlsStream對象? – zaitsman

回答

13

使用攝製形式亞歷山大·尼基京,我能發現,這似乎只發生時,你有HttpClient的是一個短暫的物體。如果您的處理程序和客戶端長壽這似乎並沒有發生:

using System; 
using System.Net.Http; 
using System.Threading.Tasks; 

namespace HttpClientMemoryLeak 
{ 
    using System.Net; 
    using System.Threading; 

    class Program 
    { 
     static HttpClientHandler handler = new HttpClientHandler(); 

     private static HttpClient client = new HttpClient(handler); 

     public static async Task TestMethod() 
     { 
      try 
      { 
       using (var response = await client.PutAsync("http://localhost/any/url", null)) 
       { 
       } 
      } 
      catch 
      { 
      } 
     } 

     static void Main(string[] args) 
     { 
      for (int i = 0; i < 1000000; i++) 
      { 
       Thread.Sleep(10); 
       TestMethod(); 
      } 

      Console.WriteLine("Finished!"); 
      Console.ReadKey(); 
     } 
    } 
} 
+2

感謝您接近這一點。不幸的是,HttpClient類當時不符合我的要求 - 由於公共代理的動態性和不穩定性,對象必須經常重新創建。看起來HttpClient對於短連接的連接並不是一個可行的解決方案 - 更改代理設置需要重新構建HttpClientHandler,因此需要重新構建HttpClient。無論哪種方式,物體應該能夠根據需要長時間或短時間存在而不會泄漏;這絕對是HttpClient中的一個缺陷。 – user1111380

0

馬特·克拉克提到的,默認HttpClient泄漏,當您使用它作爲一個短暫的對象,並創建每個請求的新HttpClients。

作爲一種變通方法,我能夠使用的HttpClient作爲一個短暫的目的是通過使用下面的NuGet包,而不是保持內置System.Net.Http裝配: https://www.nuget.org/packages/HttpClient

不知道這是什麼包的起源但是,一旦我引用它,內存泄漏消失了。確保您刪除對內置.NET System.Net.Http庫的引用,並改爲使用Nuget包。

+0

不幸的是,似乎所有者未列出此軟件包「所有者未列出此軟件包,這可能意味着軟件包已過時或不應再使用。」 –

+0

即使未列出,您仍然可以使用它。它仍然有效。 –

0

這是我如何更改HttpClientHandler代理而不重新創建對象。

public static void ChangeProxy(this HttpClientHandler handler, WebProxy newProxy) 
{ 
    if (handler.Proxy is WebProxy currentHandlerProxy) 
    { 
     currentHandlerProxy.Address = newProxy.Address; 
     currentHandlerProxy.Credentials = newProxy.Credentials; 
    } 
    else 
    { 
     handler.Proxy = newProxy; 
    } 
}