C# Httpclient Asp.net Core 2.0 on Kestrel Waiting, Lock Contention, high CPU

Posted

技术标签:

【中文标题】C# Httpclient Asp.net Core 2.0 on Kestrel Waiting, Lock Contention, high CPU【英文标题】: 【发布时间】:2017-09-07 00:56:49 【问题描述】:

我有一个非常简单的 API,它有一个单例实例,其中有许多 HTTPClients 在整个应用程序中重用。 API被调用,我创建了一个任务列表,每个任务调用一个客户端。 我使用 CancellationToken 和另一个任务来严格超时。

public class APIController : Controller

    private IHttpClientsFactory _client;
    public APIController(IHttpClientsFactory client)
    
        _client = client;
    

    [HttpPost]
    public async Task<IActionResult> PostAsync()
    
        var cts = new CancellationTokenSource();
        var allTasks = new ConcurrentBag<Task<Response>>();
        foreach (var name in list)//30 clients in list here
        
            allTasks.Add(CallAsync(_client.Client[name], cts.Token));
        
        cts.CancelAfter(1000);
        await Task.WhenAny(Task.WhenAll(allTasks), Task.Delay(1000));
        //do something with allTasks 
    

CallAsync 也很简单,只需使用客户端调用并等待应答即可。

 var response = await client.PostAsync(endpoint, content, token);

现在这段代码完美运行,1 秒后它返回,然后将取消请求发送到任何尚未返回的任务。 任务列表大约有 30 个客户端,因此 API 每次调用 30 个端点,平均响应时间为 800 毫秒。

此应用程序每秒管理 3000 个并发调用,因此大约 每秒完成 100k Httpclient 调用

问题在于 HttpClient 中存在一些瓶颈,实际上 CPU 总是非常高,我需要大约 80 个(八十个)具有 32GB RAM 的 16 核虚拟机来处理流量。显然有问题。

我得到的一个提示是,在将我的 nugget 包更新到 Asp.net Core 2 之前,完全相同的代码执行得更好。

我在服务器上进行了诊断,我的代码中没有任何问题,但似乎 HttpClients 客户端有点相互等待或卡住了。

跟踪中确实没有其他内容。 我正在使用工厂为每个端点创建单个实例:

  public class HttpClientsFactory : IHttpClientsFactory

    public static Dictionary<string, HttpClient> HttpClients  get; set; 

    public HttpClientsFactory()
    
        HttpClients = new Dictionary<string, HttpClient>();
        Initialize();
    

    private static void Initialize()
    
        HttpClients.Add("Name1", CreateClient("http://......"));  
        HttpClients.Add("Name2", CreateClient("http://...."));
        HttpClients.Add("Name3", CreateClient("http://...."));

    

    public Dictionary<string, HttpClient> Clients()
    
        return HttpClients;
    

    public HttpClient Client(string key)
    
        try
        
            return Clients()[key];
        
        catch
        
            return null;
        
    

    public static HttpClient CreateClient(string endpoint)
    
        try
        
            var config = new HttpClientHandler()
            
                MaxConnectionsPerServer = int.MaxValue,
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
            ;
            var client = new HttpClient(config)
            
                Timeout = TimeSpan.FromMilliseconds(1000),
                BaseAddress = new Uri(endpoint)
            ;

            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Connection.Clear();
            client.DefaultRequestHeaders.ExpectContinue = false;
            client.DefaultRequestHeaders.ConnectionClose = false;
            client.DefaultRequestHeaders.Connection.Add("Keep-Alive");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return client;
        
        catch (Exception)
        
            return null;
        
    

然后在启动中

 services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>();

这里发生了什么,HttpClient 的单例不适合这种情况吗? 我应该为每个线程创建一个 HttpClient 实例吗?我该怎么做?

更新

经过几天的测试,我确信 HTTPClient 调用期间的超时会使一些连接处于打开状态,从而导致端口耗尽。 关于如何避免这种情况的任何建议?

【问题讨论】:

我最初的想法是客户端太多,并且已知会导致问题。 HttpClient 旨在让一个客户端在应用程序的整个生命周期中重复使用。从工厂方法来看,除了基本 url 之外,客户端之间似乎没有太大区别。 与 HttpClient 的传出连接数存在限制:请查看此博客条目 - blogs.msdn.microsoft.com/timomta/2017/10/23/… 【参考方案1】:

由于 HTTP 的设计工作方式,您似乎已经达到了操作系统对 HTTP 请求的处理能力的极限。如果您使用的是 .NET 框架(而不是 dotnet 核心),那么您可以使用 ServicePointManager 调整操作系统管理 HTTP 请求的方式。大多数 HTTP 服务器和客户端使用keep-alive,这意味着它们将为多个 HTTP 请求重用相同的 TCP 连接。您可以使用ServicePointManager.FindServicePoint() 函数获取ServicePoint 实例以连接到特定主机。对同一主机的每个 HTTP 请求都使用相同的 ServicePoint 实例。

尝试调整ServicePoint 中的一些值,看看它如何影响您的应用程序。例如ConnectionLimit,它控制客户端和服务器之间将使用多少并行 TCP 连接。虽然keep-alive 允许您将现有的 TCP 连接重用于新的 HTTP 请求,而pipelining(使用SupportsPipelining 检查您连接的服务器是否支持此功能)允许您同时发送多个请求,但 HTTP 需要响应与请求的顺序相同。这意味着最初的缓慢响应将阻止所有后续请求/响应。通过拥有多个 TCP 连接,您可以并行处理多个非阻塞 HTTP 请求。但当然也有不利的一面,因为您现在有多个 TCP 连接,它们必须相互争夺网络资源。所以,调整这个值,看看它是否有所改善,但要小心!

所以如上所述,这里真正的问题可能是 HTTP 协议以及它如何一次只能真正处理一个请求。幸运的是,在 HTTP/2 中有一个解决方案,不幸的是,asp.net 还没有很好地支持它。我还没有尝试过,因为我工作的系统还不支持它,但理论上它应该允许您并行发送和接收多个 HTTP 请求而不会阻塞。看看this thread,它描述了一种让它工作的方法。

编辑

我完全错过了您在 asp.net core 2.0 上运行。在这种情况下,你don't have access to ServicePointManager。但是如果你只需要在 Windows 上运行,那么你可以安装 WinHttpHandler nuget 并设置 MaxConnectionsPerServer 属性。但是,如果您使用的是 WinHttpHandler,那么我建议您尝试 HTTP/2,看看这是否会为您带来改善。

EDIT2

我们刚刚发现了一个问题,即 POST 请求花费的时间是 GET 请求的两倍,但请求的其余部分完全相同。在将 localhost 连接到本地服务器时,我们从异地位置发现了该问题,因此 ping 比平时高得多。这表明在执行 POST 时进行了两次往返,而在执行 GET 时仅进行了一次往返。解决方案是这两行:

ServicePointManager.UseNagleAlgorithm = false;
ServicePointManager.Expect100Continue = false;

也许这对你也有帮助?

【讨论】:

不幸的是,我已经在使用 HttpClientHandler() => MaxConnectionsPerServer = int.MaxValue 它位于底部的第二个代码块中。我降低了虚拟机的功率,而是增加了数量(从 80 个 D5V2 十六核到 120 个 D4V2 八核)并且看起来好一点,也许我真的达到了操作系统的限制,即使看起来这么低,CPU 还没有结束15%,一切都变慢了,或者我在 Chrome 控制台 ERR_CONNECTION_RESET 中看到。我会调查 HTTP/2,谢谢【参考方案2】:

如果您查看 HttpClient 的 code,您可以看到如果 Timeout 属性不等于 Threading.Timeout.InfiniteTimeSpan,则 HttpClient 会为每个请求创建带有超时的 CancellationTokenSource:

        CancellationTokenSource cts;
        bool disposeCts;
        bool hasTimeout = _timeout != s_infiniteTimeout;
        if (hasTimeout || cancellationToken.CanBeCanceled)
        
            disposeCts = true;
            cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _pendingRequestsCts.Token);
            if (hasTimeout)
            
                cts.CancelAfter(_timeout);
            
        

查看 CancelAfter() 的 code 我们可以看到它内部是 creates System.Threading.Timer 对象:

public void CancelAfter(Int32 millisecondsDelay)
    
        ThrowIfDisposed();

        if (millisecondsDelay < -1)
        
            throw new ArgumentOutOfRangeException("millisecondsDelay");
        

        if (IsCancellationRequested) return;

        // There is a race condition here as a Cancel could occur between the check of
        // IsCancellationRequested and the creation of the timer.  This is benign; in the 
        // worst case, a timer will be created that has no effect when it expires.

        // Also, if Dispose() is called right here (after ThrowIfDisposed(), before timer
        // creation), it would result in a leaked Timer object (at least until the timer
        // expired and Disposed itself).  But this would be considered bad behavior, as
        // Dispose() is not thread-safe and should not be called concurrently with CancelAfter().

        if (m_timer == null)
        
            // Lazily initialize the timer in a thread-safe fashion.
            // Initially set to "never go off" because we don't want to take a
            // chance on a timer "losing" the initialization ---- and then
            // cancelling the token before it (the timer) can be disposed.
            Timer newTimer = new Timer(s_timerCallback, this, -1, -1);
            if (Interlocked.CompareExchange(ref m_timer, newTimer, null) != null)
            
                // We lost the ---- to initialize the timer.  Dispose the new timer.
                newTimer.Dispose();
            
        


        // It is possible that m_timer has already been disposed, so we must do
        // the following in a try/catch block.
        try
        
            m_timer.Change(millisecondsDelay, -1);
        
        catch (ObjectDisposedException)
        
            // Just eat the exception.  There is no other way to tell that
            // the timer has been disposed, and even if there were, there
            // would not be a good way to deal with the observe/dispose
            // race condition.
        

    

在创建 Timer 对象时,它 takes a lock 在单例 TimerQueue.Instance 上

    internal bool Change(uint dueTime, uint period)
    
        bool success;

        lock (TimerQueue.Instance)
        
            if (m_canceled)
                throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_Generic"));

            // prevent ThreadAbort while updating state
            try  
            finally
            
                m_period = period;

                if (dueTime == Timeout.UnsignedInfinite)
                
                    TimerQueue.Instance.DeleteTimer(this);
                    success = true;
                
                else
                
                    if (FrameworkEventSource.IsInitialized && FrameworkEventSource.Log.IsEnabled(EventLevel.Informational, FrameworkEventSource.Keywords.ThreadTransfer))
                        FrameworkEventSource.Log.ThreadTransferSendObj(this, 1, string.Empty, true);

                    success = TimerQueue.Instance.UpdateTimer(this, dueTime, period);
                
            
        

        return success;
    

如果您有大量并发 HTTP 请求,您可能会遇到 lock convoy 问题。这个问题在here 和here 进行了描述。

要确认这一点,请尝试使用 VS 中的 Parallel Stacks 窗口调试您的代码,或使用 SOS 扩展中的 syncblock WinDbg 命令。

【讨论】:

有一个类似的问题,大量并发请求超时,通过 HttpClientFactory 锁定整个应用程序,这就是原因。我们消除了超时,问题完全消失了。【参考方案3】:

尝试设置MaxConnectionsPerServer = Environment.ProcessorCount

使用MaxConnectionsPerServer = int.MaxValue,您的程序会创建/使用太多线程,这些线程会相互竞争 CPU 资源。因此,在线程之间切换加上线程开销会降低性能。

【讨论】:

以上是关于C# Httpclient Asp.net Core 2.0 on Kestrel Waiting, Lock Contention, high CPU的主要内容,如果未能解决你的问题,请参考以下文章

用于 C# 应用程序的 HttpClient

ASP.NET MVC 4 HttpClient() 问题

ASP.NET MVC 中的 HttpClient 单例实现

《ASP.NET Core 6框架揭秘》实例演示[18]:HttpClient处理管道

如何在使用 HttpClient 使用 Asp Net Web Api 的 Asp Net Mvc 中提供实时数据?

在 ASP.NET Core 应用程序中使用多个 HttpClient 对象