基于TcpListener的应用程序,不能很好地扩展

时间:2014-02-25 11:37:05

标签: .net multithreading asynchronous async-await tcplistener

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

我设置了性能计数器来测量输入和输出的消息和字节数,以及连接的插槽数量。

我创建了一个启动1400异步TCPClient的测试应用程序,并且每100-500ms发送一条1Kb消息。客户端在开始时有10-1000毫秒的随机等待启动,因此他们不会尝试同时连接所有客户端。我运作良好,我可以在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.YieldTask.Run方法,他们没有帮助。

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

当连接超过~2000个客户端时,我在客户端看到的例外是两个。大约1500年我开始看到例外,但客户最终连接。超过1500我看到很多连接/断开连接:

  

“远程主机强行关闭现有连接”   (System.Net.Sockets.SocketException)A   捕获到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)。

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

2 个答案:

答案 0 :(得分:5)

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

我学到了一些教训:

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

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

使用Task.Yield

调用Task.Run或yield

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

ConfigureAwait(假)

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

字节[]

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

异步并不意味着fast

异步允许很好地扩展,但它有成本。仅仅因为有可用的异步操作并不意味着你应该使用它。假设在获得实际响应之前需要等待一段时间,请使用异步编程。如果您确定数据存在或响应速度很快,请同步进行。

支持同步和异步是乏味的

你必须两次实现这些方法,没有从同步代码重新使用异步的防弹方法。

答案 1 :(得分:0)

嗯,首先,你在一个线程上运行所有东西,所以更改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端口,包括所有初始化。我在localhost上运行它,因此每个客户端连接实际上需要两个端口(一个用于服务器,一个用于客户端),因此它比客户端在另一台机器上时要快一些。在任何情况下,很明显我的案例中的问题是 TCP端口耗尽。