TCP套接字5分钟后CPU/内存使用率高



我正在构建一个接受传入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;
}

Mananger代码:

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();
}
}

当它们保持空闲超过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的情况。如果客户端设备优雅地关闭了它们的套接字,您会看到0result,立即循环并不断获得0result——这是一个占用CPU的紧密循环。

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

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

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

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

最新更新