【问题标题】:TCP Socket High CPU/Memory usage after 5minsTCP Socket 5 分钟后 CPU/内存使用率高
【发布时间】:2019-01-31 10:06:10
【问题描述】:

我正在构建一个接受传入 TCP 连接的服务器应用程序。 (大约 300 个独特的客户)。请务必注意,我无法控制客户端。

我发现一些正在连接的客户端在建立初始连接并发送第一个状态更新后会保持空闲一段时间。当它们保持空闲超过 5 分钟时,应用程序的 CPU 使用率会跃升至 90% 以上并保持在那里。

为了解决这个问题,我构建了一个 4 分钟后触发的取消令牌。这使我可以终止连接。然后客户端检测到这一点并在大约一分钟后重新连接。这解决了 CPU 使用率高的问题,但有高内存使用率的副作用,似乎存在内存泄漏。我怀疑资源被前一个套接字对象持有。

我有一个客户端对象,其中包含套接字连接和有关已连接客户端的信息。它还管理传入的消息。还有一个接受传入连接的管理器类。然后它创建客户端对象,为其分配套接字并将客户端对象添加到并发字典中。它每 10 秒检查一次字典中是否已设置为 _closeConnection = true 的客户端并调用它们的 dispose 方法。

这里是一些客户端对象代码:

public void StartCommunication()
    {
        Task.Run(async () =>
        {
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[75]);
            while (IsConnected)
            {
                try
                {
                    // This is where I suspect the memory leak is originating - this call I suspect is not properly cleaned up when the object is diposed
                    var result = await SocketTaskExtensions.ReceiveAsync(ClientConnection.Client, buffer, SocketFlags.None).WithCancellation(cts.Token);

                    if (result > 0)
                    {
                        var message = new ClientMessage(buffer.Array, true);
                        if(message.IsValid)
                            HandleClientMessage(message);
                    }
                }
                catch (OperationCanceledException)
                {
                    _closeConnection = true;
                    DisconnectReason = "Client has not reported in 4 mins";
                }
                catch (Exception e)
                {
                    _closeConnection = true;
                    DisconnectReason = "Error during receive opperation";
                }
            }
        });
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _closeConnection = true;
            cts.Cancel();
            // Explicitly kill the underlying socket
            if (UnitConnection.Client != null)
            {
                UnitConnection.Client.Close();
            }

            UnitConnection.Close();
            cts.Dispose();
        }
    }

任务扩展方法:

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
            {
                throw new OperationCanceledException(cancellationToken);
            }
        }

        return task.Result;
    }

经理代码:

    public bool StartListener()
    {
        _listener = new TcpListenerEx(IPAddress.Any, Convert.ToInt32(_serverPort));
        _listener.Start();
        Task.Run(async () =>
        {
            while (_maintainConnection) // <--- boolean flag to exit loop
            {
                try
                {
                    HandleClientConnection(await _listener.AcceptTcpClientAsync());
                }
                catch (Exception e)
                {
                    //<snip>
                }
            }
        });
        return true;
    }

    private void HandleClientConnection(TcpClient client)
    {
        Task.Run(async () =>
        {
            try
            {
                // Create new Coms object
                var client = new ClientComsAsync();
                client.ClientConnection = client;
                // Start client communication
                client.StartCommunication();

                //_clients is the ConcurrentDictionary

                ClientComsAsync existingClient;
                if (_clients.TryGetValue(client.ClientName, out existingClient) && existingClient != null)
                {
                    if (existingClient.IsConnected)
                        existingClient.SendHeatbeat();
                    if (!existingClient.IsConnected)
                    {
                        // Call Dispose on existing client
                        CleanUpClient(existingClient, "Reconnected with new connection");
                    }
                }
            }
            catch (Exception e)
            {
                //<snip>
            }
            finally
            {
                //<snip>
            }
        });
    }

    private void CleanUpClient(ClientComsAsync client, string reason)
    {
        ClientComsAsync _client;
        _units.TryRemove(client.ClientName, out _client);
        if (_client != null)
        {
            _client.Dispose();
        }
    }

【问题讨论】:

  • 这个问题似乎更适合CodeReview 网站。由于您的代码正在运行,但您遇到了性能问题
  • analyze CPU usage with Windows Performance Toolkit 也许它表明这是呼叫造成的。
  • @magicandre1981 我知道导致 CPU 使用率高的原因。它的 ReceiveAsync 调用等待响应时间过长。这就是为什么我用令牌取消它,但后来我得到内存泄漏。我想知道如何处理泄漏。

标签: c# sockets memory-leaks cpu-usage tcpclient


【解决方案1】:

当它们保持空闲超过 5 分钟时,应用程序的 CPU 使用率会跃升至 90% 以上并保持在那里。

为了解决这个问题,我构建了一个取消令牌,它会在 4 分钟后触发。

正确的反应是解决CPU使用率高的问题。

在我看来它就在这里:

while (IsConnected)
{
  try
  {
    var result = await SocketTaskExtensions.ReceiveAsync(ClientConnection.Client, buffer, SocketFlags.None);

    if (result > 0)
    {
      ...
    }
  }
  catch ...
  {
    ...
  }
}

套接字很奇怪,要正确处理原始 TCP/IP 套接字非常困难。附带说明一下,我总是鼓励开发人员使用更标准的东西,例如 HTTP 或 WebSockets,但在这种情况下,您无法控制客户端,所以这不是一个选择。

具体来说,您的代码没有处理result == 0 的情况。如果客户端设备优雅地关闭了它们的套接字,您会看到 result0,立即循环返回并继续得到 result0 - 一个占用 CPU 的紧密循环。

当然,这是假设IsConnected 仍然是true。这也许是可能的......

您没有显示代码中IsConnected 的设置位置,但我怀疑它在发送心跳消息后的错误处理中。所以这就是为什么这可能无法按预期工作的原因......我怀疑客户端设备正在关闭其发送流(您的接收流),同时保持其接收流(您的发送流)打开。这是关闭套接字的一种方法,有时被认为“更有礼貌”,因为它允许另一端继续发送数据,即使该端已完成发送。 (这是从客户端设备的角度来看,所以“另一端”是您的代码,“这一端”是客户端设备)。

这是完全合法的套接字方式,因为每个连接的套接字都是两个流,而不是一个,每个都可以独立关闭。如果发生这种情况,您的心跳仍将无错误地发送和接收(并且可能只是被客户端设备静默丢弃),IsConnected 将保持为true,并且读取循环将变得同步并占用您的 CPU。

要解决此问题,请在您的读取循环中添加对 result == 0 的检查,并清理客户端,就像发送心跳失败一样。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-05
    • 2014-05-07
    • 2022-07-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多