【问题标题】:Big size of ServicePoint object after several hours sending HTTP request in parallel几个小时并行发送 HTTP 请求后,ServicePoint 对象变大
【发布时间】:2015-03-17 10:18:29
【问题描述】:

我们正在使用HttpClient 向远程Web API 并行发送请求:

public async Task<HttpResponseMessage> PostAsync(HttpRequestInfo httpRequestInfo)
{
    using (var httpClient = new HttpClient())
    {
        httpClient.BaseAddress = new Uri(httpRequestInfo.BaseUrl);
        if (httpRequestInfo.RequestHeaders.Any())
        {
            foreach (var requestHeader in httpRequestInfo.RequestHeaders)
            {
                httpClient.DefaultRequestHeaders.Add(requestHeader.Key, requestHeader.Value);
            }
        }

        return await httpClient.PostAsync(httpRequestInfo.RequestUrl, httpRequestInfo.RequestBody);
    }
}

这个 API 可以被多个线程同时调用。运行大约四个小时后,我们发现发生了内存泄漏问题,从 profiling 工具中,似乎有两个 ServicePoint 对象,其中一个相当大,大约 160 MB。

据我所知,我可以看到代码上面的一些问题:

  • 我们应该尽可能共享HttpClient 实例。在我们的例子中,请求地址和标头可能会有很大的不同,那么这是我们可以做些什么还是不会损害太多性能?我只是想到我们可以准备一个字典来存储和查找HttpClient 实例。
  • 我们没有修改ServicePointDefaultConnectionLimit,所以默认只能同时向同一个服务器发送两个请求。如果我们把这个值改大一点,内存泄漏问题就可以解决了吗?
  • 我们还抑制了 HTTPS 证书验证:ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; 这是否与问题有关?

由于这个问题不容易重现(需要很多时间),我只需要一些想法,以便我可以优化我们的代码以长时间运行。

【问题讨论】:

  • 您使用的是哪种内存分析器,是否在检查哪些对象仍处于活动状态(msdn.microsoft.com/en-us/library/ee787088%28v=vs.110%29.aspx)之前强制对所有代进行堆生成清理?
  • @Alexandru 我使用 dotMemory,当然我强制 GC 收集发生并等待足够长的时间。无论如何,我已经找到了问题所在,这与互联网流量性能有关,而不是内存泄漏。
  • 根据您的回答,下次发生这种情况时,您应该切断任何新连接,然后在一段时间后调用生成垃圾收集。这应该让旧的连接消失,你应该看看是否存在真正的内存泄漏。听起来您排队的请求多于您一次能够处理的数量,实际上,您的应用程序本身几乎被 DoS 攻击,因为它最终可能会耗尽内存。
  • 您下次可以尝试做的事情(顺便说一句,这很有帮助)是要求客户从任务管理器中生成应用程序的转储,在 Visual Studio 中打开它以及您发送给客户的代码,并调试到您的应用程序中。查看应用程序的活动线程。您可能会看到很多线程等待发布。

标签: c# .net multithreading httpclient servicepoint


【解决方案1】:

自己解释一下情况,以防其他人以后也遇到这个问题。

首先,这不是内存泄漏,而是性能问题。

我们在打开代理的虚拟机中测试我们的应用程序。它导致互联网很慢。所以在我们的例子中,每个 HTTP 请求可能会花费 3-4 秒。随着时间的推移,ServicePoint 队列中将会有很多连接。因此,这不是内存泄漏,只是因为之前的连接没有足够快地完成。

只要确保每个 HTTP 请求都没有那么慢,一切都会变得正常。

我们还尝试减少 HttpClient 实例,以提高 HTTP 请求性能:

private readonly ConcurrentDictionary<HttpRequestInfo, HttpClient> _httpClients;

private HttpClient GetHttpClient(HttpRequestInfo httpRequestInfo)
{
    if (_httpClients.ContainsKey(httpRequestInfo))
    {
        HttpClient value;
        if (!_httpClients.TryGetValue(httpRequestInfo, out value))
        {
            throw new InvalidOperationException("It seems there is no related http client in the dictionary.");
        }

        return value;
    }

    var httpClient = new HttpClient { BaseAddress = new Uri(httpRequestInfo.BaseUrl) };
    if (httpRequestInfo.RequestHeaders.Any())
    {
        foreach (var requestHeader in httpRequestInfo.RequestHeaders)
        {
            httpClient.DefaultRequestHeaders.Add(requestHeader.Key, requestHeader.Value);
        }
    }

    httpClient.DefaultRequestHeaders.ExpectContinue = false;
    httpClient.DefaultRequestHeaders.ConnectionClose = true;
    httpClient.Timeout = TimeSpan.FromMinutes(2);

    if (!_httpClients.TryAdd(httpRequestInfo, httpClient))
    {
        throw new InvalidOperationException("Adding new http client thrown an exception.");
    }

    return httpClient;
}

在我们的例子中,相同的服务器地址只有请求正文不同。我还覆盖了HttpRequestInfoEqualsGetHashCode 方法。

同时,我们设置ServicePointManager.DefaultConnectionLimit = int.MaxValue;

希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-09
    • 2020-06-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多