【问题标题】:Receiving byte stream using TCPClient consuming too much CPU使用 TCPClient 接收字节流消耗过多 CPU
【发布时间】:2022-01-12 22:05:03
【问题描述】:

我有一个使用TcpListener 从第三方系统接收大字节流的服务。不幸的是,我没有选择更改协议或WCF 之类的选项。

当多个系统同时发送数据时,我的服务器上的 CPU 使用率飙升至近 90%,并导致其他服务出现问题。在对我的服务进行概要分析后,看起来 CPU 正在用于将字节数组从 NetworkStream 读取到 MemoryStream 中。这是我的服务器代码示例

public void StartListening(CancellationToken token) {
    var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, port);
    listener.Start();

    while (!token.IsCancellationRequested) {
        var socket = listener.AcceptSocket();
        TcpClient tcpClient = new TcpClient(socket);
        var stream = tcpClient.GetStream();

        Task.Run(()=> ReadStream(stream, token));
    }
}

private void ReadStream(NetworkStream stream, CancellationToken token){
    int offset = 0;
    int size = EXPECTED_FILE_SIZE;
    var inStream = new MemoryStream(size);
    while (size > 0 && !token.IsCancellationRequested) {
        try {
            int readin = stream.Read(inStream.GetBuffer(), offset, size);
            size -= readin;
            offset += readin;
        }
        catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }    
    }
    //Do something with the memory stream
}

我编写了一个测试客户端,它会在每次 CPU 达到峰值时一次向服务发送多个字节流。我已经尝试了一些服务器端的方法来解决这个问题(包括在网络流上使用BeginReadEndRead)。唯一似乎有帮助的是当我从客户端发送更大的字节流块时,但我无法控制发送数据的第 3 方系统。

我认为接受所有套接字连接可能会起作用,但会限制“ReadStream”任务的数量,但我不知道接受套接字然后不从中读取是否有任何不利影响一会儿。

【问题讨论】:

  • 不确定实际问题,因为代码本身看起来正确,我唯一担心的是如果EXPECTED_FILE_SIZE 大于实际传输的值怎么办?您的代码将陷入无限循环。
  • 您正在使用取消令牌,但同步读取。问题:为什么要使用同步读取? Socket IO 几乎是异步读取的完美候选者。另外,你是这里的客户端还是服务器?如果您是服务器:Kestrel 和 Pipelines 是为这种情况构建的,这使得编写大规模并发服务器变得轻而易举。 Pipelines 还旨在为您处理后台缓冲区等,因此您最终不会将所有内容复制到 MemoryStream。我有一个包含 4 部分的博客,在这里:blog.marcgravell.com/2018/07/pipe-dreams-part-1.html
  • 如果您是服务器,那么:使用每连接线程回答“为什么我会导致 CPU 出血?”的问题。异步是编写可扩展服务器的关键。特别是,您当前的代码正在占用线程池,这是一个糟糕的主意,原因有很多
  • 这是服务器端代码。感谢您提供有关 Kestrel 的提示,这项服务越来越希望被重写。至于同步,我没有编写原始代码,但我确实尝试将 Read 调用转换为它们的 BeginRead 和 EndRead 等效项(使用 FromAsync 而不是 Task.Run),但并没有太大改善
  • 您到处都缺少async await,所以这不足为奇。循环中的Task.Run 不会异步生成。

标签: c# tcplistener


【解决方案1】:

我个人会尝试通过为每个在完成后终止的连接创建一个单独的 BackgroundWorker 来解决它。这会让你更好地利用 CPU 线程,让操作系统调度程序来处理它。

也可能是 CPU 的功能不够强大,无法多次处理如此多的传入数据。

否则,我将使用网关获取数据并将其发送到 RabbitMQ 队列,并以较低的速度处理该数据。

【讨论】:

  • 每个连接一个线程的可扩展性不大。服务器端有更好的方法。
  • @JeroenvanLangen 在这种情况下,您可以使用负载均衡器,假设您可以拥有多个实例,例如 NGINX。尽管如此,我还是建议使用多个工作人员或异步编程。不过,我想了解更多关于架构的信息。
  • 不,使用负载均衡器无法解决 cpu 线程阻塞问题。
【解决方案2】:

与其连续读取流,不如使用内置的异步回调,这样它只在需要时调用,您可以执行类似的操作

public void StartListening(CancellationToken token)
{
    var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, port);
    listener.Start();

    while (!token.IsCancellationRequested)
    {
        var socket = listener.AcceptSocket();
        TcpClient tcpClient = new TcpClient(socket);
        stream = tcpClient.GetStream(); //you need to have the stream declared at the start of the program

        //set it up with th receive callback called RecieveCallback
        //the 4096 is just so that any data more than that won't be read but it is highly unlickely for a peice of data above 4096 to appear
        stream.BeginRead(4096, 0, 4096, ReceiveCallback, null);

        Task.Run(() => ReadStream(stream, token));
    }
}
//then we setup the RecieveCallback event
private void ReceiveCallback(IAsyncResult _result)
{
    try
    {
        int _byteLength = stream.EndRead(_result); //find how much data it is
        if (_byteLength <= 0)
        {
            //if this is true you are disconnected
            return;
        }

        byte[] _data = new byte[_byteLength];
        Array.Copy(4096, _data, _byteLength);

        DoSomethingWith(_data)
        stream.BeginRead(4096, 0, 4096, ReceiveCallback, null); //start listening again
    }
    catch
    {
        //disconnect
    }
}

顺便说一句,运行代码 sn-p 不起作用,这是我写出看起来不错的代码的唯一方法

【讨论】:

  • 异步并不快。它更具可扩展性。我也更喜欢使用异步,但这取决于有多少客户端。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-06-04
  • 2016-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-19
  • 2015-09-13
相关资源
最近更新 更多