【问题标题】:TcpListener based application that does not scale up well基于 TcpListener 的应用程序不能很好地扩展
【发布时间】:2014-03-27 14:37:55
【问题描述】:

我有一个基于TCPListener 的 ECHO 服务器应用程序。它接受客户端、读取数据并返回相同的数据。我使用框架提供的 XXXAsync 方法使用 async/await 方法开发了它。

我已经设置了性能计数器来测量有多少消息和字节进出,以及有多少连接的套接字。

我创建了一个测试应用程序,它启动 1400 个异步TCPClient,并每 100-500ms 发送一条 1Kb 的消息。客户端在开始时有 10-1000 毫秒之间的随机等待开始,因此他们不会尝试同时连接所有客户端。我工作得很好,我可以在 PerfMonitor 中看到 1400 已连接,以良好的速度发送消息。我从另一台计算机运行客户端应用程序。服务器的 CPU 和内存使用量很少,它是 Intel Core i7 和 8Gb RAM。客户端似乎更忙,它是具有 4Gb RAM 的 i5,但仍然没有 25%。

问题是如果我启动另一个客户端应用程序。客户端中的连接开始失败。我没有看到每秒消息的大幅增加(或多或少增加了 20%),但我看到连接的客户端数量大约是 1900-2100,而不是预期的 2800。性能略有下降,图表显示每秒最大和最小消息之间的差异比以前更大。

不过,CPU 使用率甚至不到 40%,内存使用率仍然很少。我试图增加客户端和服务器中的线程数或池线程数:

ThreadPool.SetMaxThreads(5000, 5000);
ThreadPool.SetMinThreads(2000, 2000);

在服务器中,循环接受连接:

while(true)
{
    var client = await _server.AcceptTcpClientAsync();
    HandleClientAsync(client);
}

HandleClientAsync 函数返回一个Task,但正如您所见,循环不等待处理,只是继续接受另一个客户端。那个处理函数是这样的:

public async Task HandleClientAsync(TcpClient client)
{    
    while(ws.Connected && !_cancellation.IsCancellationRequested)
    {
        var msg = await ReadMessageAsync(client);
        await WriteMessageAsync(client, msg);
    }
}

这两个函数只是异步读写流。

我已经看到我可以启动 TCPListener 指示 backlog 金额,但默认值是多少?

为什么应用程序在达到最大 CPU 之前无法扩展?

找出实际问题的方法和工具是什么?

更新

我尝试了Task.YieldTask.Run 方法,但它们没有帮助。

服务器和客户端在同一台计算机上本地运行时也会发生这种情况。每秒增加客户端或消息的数量,实际上会降低服务吞吐量。 600 个客户端每 100 毫秒发送一条消息,比 1000 个客户端每 100 毫秒发送一条消息产生更多的吞吐量。

连接超过 2000 个客户端时,我在客户端上看到的异常是两个。大约 1500 我在开始时看到了异常,但客户端最终连接了。超过 1500 我看到很多连接/断开连接:

“一个现有的连接被远程主机强行关闭” (System.Net.Sockets.SocketException) 一个 System.Net.Sockets.SocketException 被捕获:“现有连接 被远程主机强行关闭”

“无法将数据写入传输连接:一个现有的 连接被远程主机强行关闭。” (System.IO.IOException) 抛出 System.IO.IOException:“无法 将数据写入传输连接:现有连接是 被远程主机强行关闭。”

更新 2

我已经设置了一个非常 simple project with server and client using async/await 并且它可以按预期扩展。

我遇到可扩展性问题的项目是this WebSocket server,即使它使用相同的方法,显然也有一些东西引起了争用。有一个console application hosting the component 和一个generate load 的控制台应用程序(尽管它至少需要Windows 8)。

请注意,我不是要直接解决问题的答案,而是要找出导致争用的原因的技术或方法。

【问题讨论】:

  • 好吧,请务必查清楚!在他们那一刻,我们只知道“某处出现错误”。
  • 除了确切的错误信息,请告诉我们您是否(以及如何)明确使用线程池,Task.RunTask.Factory.StartNew 等。
  • @vtortola,我的意思是使用Task.Run。这就是我的意思:stackoverflow.com/a/21018042/1768303。也许你应该展示你的HandleClientAsync 的样子。
  • @vtortola:您绝对确定需要使用TCP/IP ?因为HandleClientAsync 有几个问题:它使用Connected,它在没有同时定期写入的情况下读取,在没有同时连续读取的情况下写入。 TCP/IP 与编写汇编语言没有什么不同。在克林贡。有什么方法可以改用 WebAPI 和/或 SignalR?
  • @vtortola:我强烈建议你选择另一个项目来学习async/await。学习 TCP/IP 本身就是一项艰巨的任务。不,阻塞不是我的意思;使用读/写循环,您将面临半开问题。

标签: .net multithreading asynchronous async-await tcplistener


【解决方案1】:

我已经成功地扩展到 6,000 个并发连接,并且每秒处理大约 24,000 条消息,从没有机器的机器连接(没有 localhost 测试)并且仅使用大约 80 个物理线程。

我学到了一些教训:

增加线程池大小让事情变得更糟

除非你知道自己在做什么,否则不要这样做。

使用 Task.Yield 调用 Task.Run 或 yield

确保您释放调用线程参与该方法的其余部分。

配置等待(假)

如果您确信自己不在单线程同步上下文中,则从您的可执行应用程序中,这允许任何线程获取延续,而不是专门等待开始变得空闲的线程。

字节[]

内存分析器显示该应用在创建 Byte[] 实例时花费了过多的内存和时间。所以我设计了几种策略来重用可用的策略,或者只是“就地”工作而不是创建新策略并复制。 GC 性能计数器(特别是“% time in GC”,大约为 55%)发出了一些不正确的警报。另外,我使用BitArray 实例来检查字节中的位,这也导致了一些内存开销,所以我用按位操作替换它们并且它得到了改进。后来我发现 WCF 使用 Byte[] 池来处理这个问题。

异步不代表fast

异步可以很好地扩展,但它是有代价的。仅仅因为有可用的异步操作并不意味着您应该使用它。如果您认为在获得实际响应之前需要等待一段时间,请使用异步编程。如果您确定数据在那里或响应会很快,请同步进行。

支持同步和异步很繁琐

你必须实现这些方法两次,没有从同步代码中重新组合异步的万无一失的方法。

【讨论】:

  • +1 进行研究,如果您尝试this optimization,我会很感兴趣。
  • 这个周末我会尝试一下。谢谢。
【解决方案2】:

嗯,首先,您在一个线程上运行所有内容,因此更改 ThreadPool 不会产生任何影响。

编辑:正如 Noseration 指出的那样,这实际上不是真的。虽然 IOCP 和异步套接字本身实际上并不需要额外的线程来处理 I/O 请求,但 .NET 中的默认实现却需要。完成事件在ThreadPool 线程上处理,您有责任提供自己的TaskScheduler,或将事件排队并在消费者线程上手动处理。我将留下其余的答案,因为它仍然相关(并且线程切换在这里不是性能问题,如答案后面所述)。另请注意,UI 应用程序中的默认 TaskScheduler 通常确实使用同步上下文,例如。 winforms,完成事件将在 UI 线程上处理。在任何情况下,在问题上抛出比 CPU 内核更多的线程都无济于事

但是,这不一定是坏事。 I/O 绑定操作不会从在单独的线程上运行中受益,事实上,这样做非常低效。这正是 async 和 IOCP 的用途,所以请继续使用它。

如果您开始获得大量 CPU 使用率,那就是您希望使事情并行的地方,而不是简单的异步。尽管如此,使用await 在一个线程上接收消息应该没问题。处理多线程总是很棘手,对于不同的情况有很多方法。在实践中,您通常不希望拥有比可用处理器内核更多的线程 - 如果它们正在竞争 I/O,请使用 async。如果他们在竞争 CPU,那么当线程数超过 CPU 可以并行处理的线程时,情况只会变得更糟。

请注意,由于您在一个线程上运行,因此您的一个处理器内核很可能以 100% 的速度运行,而其余的则什么也不做。您可以在任务管理器中轻松验证这一点。

另外,请注意,您一次可以打开的 TCP 连接数量非常有限。每个连接都必须在客户端和服务器上都有自己的端口。客户端 Windows 的默认值在 1000-4000 端口的某处。对于服务器(也不是您的负载测试客户端)来说,这并不算多。

如果您同时打开和关闭连接,情况会变得更糟,因为 TCP 端口保证会打开一段时间(断开连接后最多四分钟)。这是因为在同一端口上打开新的 TCP 连接可能意味着旧连接的数据可能会到达新连接,这将非常非常糟糕。

请添加更多信息。 ReadMessageAsyncWriteMessageAsync 有什么作用?性能影响是否可能是由 GC 引起的?您是否尝试过分析 CPU 和内存?您确定您实际上没有用所有这些 TCP 消息耗尽网络带宽吗?您是否检查过您是否遇到 TCP 端口耗尽或高丢包情况?

更新:我编写了一个测试服务器和客户端,当使用异步套接字时,它们可以在一秒钟内耗尽可用的 TCP 端口,包括所有初始化。我在本地主机上运行它,所以每个客户端连接实际上需要两个端口(一个用于服务器,一个用于客户端),所以它比客户端在不同机器上时要快一些。无论如何,很明显我的问题是 TCP 端口耗尽。

【讨论】:

  • CPU 和内存使用率很低,在Task Mgr 中很少达到服务器中的30%,通常在15% 左右,1400 个客户端,内存小于100Mb;我不记得 PerfMonitor 中的确切数字。两台机器,服务器和客户端在一个 54Mb 的无线网络中,根据任务管理器,每秒消耗大约 1Mb。条件不理想,但我认为不是原因。
  • @vtortola 是的,但是四核机器上 30% 的 CPU 仍然可能意味着您实际使用的一个核心是 100%。您能否检查每个核心的 cpu 使用情况以确保不是这种情况?
  • 我不记得见过 8 个逻辑 CPU 中的一个已满,但我会再次检查。
  • @Luaan,再次不正确。 OP 始终运行至少一个线程,其中核心while (true) 循环运行并接受与AcceptTcpClientAsync 的连接。现在想象一个待处理的ReadMessageAsync 已在随机 IOCP 线程上完成。 await ReadMessageAsync 之后的 OP 代码继续在该线程上执行。 WriteMessageAsync 里面的任何东西都将在那里执行。到目前为止,我们有 两个线程。突然,另一个待处理的ReadMessageAsync 完成:三个线程,同时进行。等等……
  • @Noseratio 哦,男孩。再次被抽象的具体实现所愚弄。默认任务调度程序,没有同步上下文,哎呀。你是对的,OP 实际上是在很多很多线程上执行代码。我认为 是辩论“专家初学者”的人。抱歉 :) 我会更新我的答案以反映这一点。
猜你喜欢
  • 2018-06-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-24
  • 1970-01-01
  • 2011-11-18
  • 1970-01-01
相关资源
最近更新 更多