web-dev-qa-db-ja.com

HttpClientHandler / HttpClientのメモリリーク

HttpClientを使用して単純なHTTPSAPI呼び出しを実行するメソッドを呼び出す10〜150の長寿命クラスオブジェクトがあります。 PUTコールの例:

_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;
        }
    }
}
_

usingステートメントによる適切な廃棄を含むこれらのメソッドを2〜3時間実行した後、プログラムは1GB〜1.5GBのメモリに忍び寄り、最終的にはさまざまなメモリ不足エラーでクラッシュします。多くの場合、接続は信頼できないプロキシを介して行われるため、接続が期待どおりに完了しない可能性があります(タイムアウトやその他のエラーが一般的です)。

.NETメモリプロファイラーは、ここでHttpClientHandlerが主な問題であることを示し、「直接デリゲートルートを持つ破棄されたインスタンス」(赤い感嘆符)と「破棄されたがまだGCされていないインスタンス」の両方があると述べています(黄色の感嘆符)。プロファイラーがルート化されたことを示すデリゲートは、HttpWebRequestに由来するAsyncCallbacksです。

RemoteCertValidationCallbackは、ルートのさらに下にある「破棄されたがGCされていない」オブジェクトであるため、HTTPS証明書の検証に関係するTlsStreamにも関連している可能性があります。

これらすべてを念頭に置いて-HttpClientをより正しく使用して、これらのメモリの問題を回避するにはどうすればよいですか? GC.Collect()を1時間ごとに強制する必要がありますか?私はそれが悪い習慣と見なされていることを知っていますが、適切に廃棄されていないこのメモリを再利用する方法が他にわかりません。また、これらの短命のオブジェクトのより良い使用パターンは、私には明らかではありません。 .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

21
user1111380

再現フォームのAlexandrNikitinを使用して、これは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();
        }
    }
}
14
Matt Clark

これは、オブジェクトを再作成せずに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;
    }
}
1
chancity

以下は、HttpClientとHttpClientHandlerを効率的に使用する基本的なAPIクライアントです。リクエストごとにHTTPClientを再作成しないでください。可能な限りHttpclientを再利用する

私のPerformance APIクライアント

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace MyApiClient 
{
    public class MyApiClient : IDisposable
    {
        private readonly TimeSpan _timeout;
        private HttpClient _httpClient;
        private HttpClientHandler _httpClientHandler;
        private readonly string _baseUrl;
        private const string ClientUserAgent = "my-api-client-v1";
        private const string MediaTypeJson = "application/json";

        public MyApiClient(string baseUrl, TimeSpan? timeout = null)
        {
            _baseUrl = NormalizeBaseUrl(baseUrl);
            _timeout = timeout ?? TimeSpan.FromSeconds(90);
        }

        public async Task<string> PostAsync(string url, object input)
        {
            EnsureHttpClientCreated();

            using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
            {
                using (var response = await _httpClient.PostAsync(url, requestContent))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }

        public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
        {
            var strResponse = await PostAsync(url, input);

            return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
        {
            var strResponse = await GetAsync(url);

            return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        public async Task<string> GetAsync(string url)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.GetAsync(url))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public async Task<string> PutAsync(string url, object input)
        {
            return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
        }

        public async Task<string> PutAsync(string url, HttpContent content)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.PutAsync(url, content))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public async Task<string> DeleteAsync(string url)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.DeleteAsync(url))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public void Dispose()
        {
            _httpClientHandler?.Dispose();
            _httpClient?.Dispose();
        }

        private void CreateHttpClient()
        {
            _httpClientHandler = new HttpClientHandler
            {
                AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
            };

            _httpClient = new HttpClient(_httpClientHandler, false)
            {
                Timeout = _timeout
            };

            _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

            if (!string.IsNullOrWhiteSpace(_baseUrl))
            {
                _httpClient.BaseAddress = new Uri(_baseUrl);
            }

            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
        }

        private void EnsureHttpClientCreated()
        {
            if (_httpClient == null)
            {
                CreateHttpClient();
            }
        }

        private static string ConvertToJsonString(object obj)
        {
            if (obj == null)
            {
                return string.Empty;
            }

            return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        private static string NormalizeBaseUrl(string url)
        {
            return url.EndsWith("/") ? url : url + "/";
        }
    }
}

使用法;

using ( var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

注:依存関係注入ライブラリを使用している場合は、MyApiClientをシングルトンとして登録してください。具象リクエストに対して同じオブジェクトを再利用してもステートレスで安全です。

1
Alper Ebicoglu

Matt Clarkが述べたように、デフォルトのHttpClientは、それを短命のオブジェクトとして使用し、リクエストごとに新しいHttpClientを作成するとリークします。

回避策として、組み込みのSystem.Net.Httpアセンブリの代わりに次のNugetパッケージを使用することで、HttpClientを短期間のオブジェクトとして使用し続けることができました: https://www.nuget.org/ packages/HttpClient

ただし、このパッケージの起源が何かはわかりませんが、参照するとすぐにメモリリークがなくなりました。組み込みの.NETSystem.Net.Httpライブラリへの参照を削除し、代わりにNugetパッケージを使用してください。

0
Elad Nava