我有一个基于TCPListener
的ECHO服务器应用程序。它接受客户端,读取数据,并返回相同的数据。我使用框架提供的XXXAsync
方法,使用async/await方法开发了它。
我已经设置了性能计数器,以测量有多少消息和字节进出,以及有多少连接的套接字。
我创建了一个测试应用程序,该应用程序启动1400异步TCPClient
,并每100-500ms发送1Kb消息。客户端在开始时会有10-1000ms的随机等待时间,因此它们不会尝试同时连接所有客户端。我运行良好,在PerfMonitor中可以看到1400已连接,并且以很高的速率发送消息。我从另一台计算机上运行客户端应用程序。服务器的CPU和内存使用量很少,它是具有8Gb RAM的Intel Core i7。客户端似乎更忙,它是具有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.Yield
和Task.Run
方法,但它们没有帮助。当服务器和客户端在同一台计算机上本地运行时,也会发生这种情况。每秒增加客户端或消息的数量实际上减少了服务吞吐量。每100毫秒发送一条消息的600个客户端比每100毫秒发送一条消息的1000个客户端产生更多的吞吐量。
连接超过2000个客户端时,我在客户端上看到的异常是两个。大约有1500个时,我在一开始就看到了异常,但是客户端终于连接了。超过1500个,我看到很多连接/断开连接:
更新2
我已经设置了一个非常simple project with server and client using async/await,它可以按预期缩放。
我遇到可伸缩性问题的项目是this WebSocket server,即使使用相同的方法,显然也有引起争用的问题。有一个console application hosting the component和generate load的控制台应用程序(尽管它至少需要Windows 8)。
请注意,我并不是在寻求直接解决问题的答案,而是寻求找出导致该争执的原因的技术或方法。
最佳答案
我设法成功扩展到6,000个并发连接,而没有问题,并且每秒处理无机器(无localhost测试)的连接大约24,000条消息(仅使用80个物理线程)。
我学到了一些教训:
增加线程池大小会使情况更糟
除非您知道自己在做什么,否则不要做。
调用Task.Run或使用Task.Yield屈服
为确保您不会从调用该方法的其余部分中释放线程。
ConfigureAwait(false)
如果您确信自己不在单线程同步上下文中,则可以从可执行应用程序中使用任何线程来接续,而不必专门等待开始变为空闲的线程。
字节[]
内存分析器显示该应用在创建Byte[]
实例上花费了过多的内存和时间。因此,我设计了几种策略来重用可用策略,或者只是“就地”工作,而不是创建新策略并进行复制。 GC性能计数器(特别是“GC中的%时间”,大约为55%)发出警告,指出某些问题不正确。另外,我使用BitArray
实例检查字节中的位,这也导致了一些内存开销,因此我将它们替换为按位操作,并对其进行了改进。后来我发现WCF使用Byte[]
池来解决此问题。
异步并不意味着fast
异步可以很好地扩展,但是要付出代价。仅仅因为有可用的异步操作并不意味着您应该使用它。假设您需要花一些时间才能获得实际的响应,请使用异步编程。如果您确定有数据或响应很快,请同步进行。
支持同步和异步乏味
您必须实现两次该方法,没有从同步代码重新启动异步的防弹方法。
关于.net - 基于TcpListener的应用程序无法很好地扩展,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/22013072/