我知道这个话题已经被问了很多次,我几乎读了所有的帖子和评论,但我仍然没有找到答案。
我正在开发一个高性能网络库,必须具有TCP服务器和客户端,能够接受30000多个连接,并且吞吐量必须尽可能高。
我非常清楚我必须使用async
方法,我已经实现了所有我找到并测试过的解决方案。
在我的基准测试中,只使用了最小的代码来避免范围内的任何开销,我使用了分析工具来最小化CPU负载,没有更多的简单优化空间,在接收套接字上,缓冲数据总是被读取、计数和丢弃以避免套接字缓冲区完全填满。
情况非常简单,一个TCP套接字侦听本地主机,另一个TCP套接字连接到侦听套接字(从同一程序,在同一台机器上),然后一个无限循环开始使用客户端套接字向服务器套接字发送256kB大小的数据包。
一个1000ms间隔的计时器将字节计数器从两个套接字打印到控制台,以使带宽可见,然后为下一个测量重置它们。
我意识到数据包大小的最佳值是256kB,套接字缓冲区大小为64kB,以获得最大吞吐量。
使用async/await
类型的方法,我可以达到
~370MB/s (~3.2gbps) on Windows, ~680MB/s (~5.8gbps) on Linux with mono
使用
BeginReceive/EndReceive/BeginSend/EndSend
类型的方法,我可以实现以下操作:~580MB/s (~5.0gbps) on Windows, ~9GB/s (~77.3gbps) on Linux with mono
使用SocketAsyncEventArgs/ReceiveAsync/SendAsync
类型的方法,我可以实现以下功能:
~1.4GB/s (~12gbps) on Windows, ~1.1GB/s (~9.4gbps) on Linux with mono
以下是需要翻译的内容:
问题如下:
async/await
方法是最慢的,所以我不会使用它们BeginReceive/EndReceive
方法与BeginAccept/EndAccept
方法一起启动了新的异步线程,在Linux/mono下每个套接字实例都非常缓慢(当ThreadPool
中没有更多线程时,mono会启动新线程,但创建25个连接实例大约需要5分钟,创建50个连接是不可能的(程序在约30个连接后停止响应)。- 更改
ThreadPool
大小根本没有帮助,我也不会改变它(这只是一个调试移动) - 到目前为止,最好的解决方案是
SocketAsyncEventArgs
,在Windows上可以获得最高吞吐量,但在Linux/mono上比Windows慢,以前则相反。
我已经使用iperf对我的Windows和Linux机器进行了基准测试,
Windows machine produced ~1GB/s (~8.58gbps), Linux machine produced ~8.5GB/s (~73.0gbps)
奇怪的是,
iperf
可能会比我的应用程序产生更弱的结果,但在 Linux 上,它要高得多。首先,我想知道这些结果是否正常,或者我能否通过不同的解决方案获得更好的结果?
如果我决定使用
BeginReceive/EndReceive
方法(它们在 Linux/mono 上产生了相对较高的结果),那么我该如何解决线程问题,以使连接实例创建快速,并消除创建多个实例后的停滞状态?我将继续进行进一步的基准测试,并在有新的结果时分享。
=============================== 更新 ===============================
我承诺提供代码片段,但经过多小时的实验后,总体代码有点混乱,所以我只会分享我的经验,以帮助其他人。
我不得不意识到,在 Windows 7 下,环回设备很慢,无法获得比 1GB/s 更高的结果,无论是使用 iperf 还是 NTttcp,只有 Windows 8 和更新版本才拥有快速环回。因此,在我可以在更新版本上进行测试之前,我不再关心 Windows 结果。应该通过 SIO_LOOPBACK_FAST_PATH 通过 Socket.IOControl 启用,但它在 Windows 7 上会抛出异常。
结果表明,基于完成事件的 SocketAsyncEventArgs 实现是最强大的解决方案,不仅在 Windows 上,在 Linux/Mono 上也是如此。创建几千个客户端实例从未混乱过线程池,程序不会像我上面提到的那样突然停止。这种实现非常适合线程。
将 10 个连接创建到侦听套接字,并从
ThreadPool
中的 10 个单独的线程将数据提供给客户端,可以在 Windows 上产生约 2GB/s 的数据流量,在 Linux/Mono 上产生约 6GB/s 的数据流量。增加客户端连接数并没有改善整体吞吐量,但总流量分布在连接之间,这可能是因为即使使用 5、10 或 200 个客户端,CPU 负载在所有核心/线程上都达到了 100%。
我认为整体性能还不错,每个客户端可以产生大约500mbit/s的流量。当然,这是在本地连接中测量的数据,在网络中的实际情况可能会有所不同。
唯一需要分享的观察结果是:尝试使用Socket的输入/输出缓冲区大小以及程序读/写缓冲区大小/循环周期对性能影响很大,而且在Windows和Linux/Mono上影响非常不同。
在Windows上,最佳性能是使用128kB的Socket接收缓冲区、32kB的Socket发送缓冲区、16kB的程序读取缓冲区和64kB的程序写入缓冲区。
在Linux上,之前的设置导致性能非常差,但是512kB的Socket接收和发送缓冲区、256kB的程序读取缓冲区和128kB的程序写入缓冲区大小表现最佳。
现在我的唯一问题是,如果我尝试创建10000个连接套接字,那么在大约7005个之后,它就停止创建实例了,没有抛出任何异常,程序运行时好像没有任何问题,但是我不知道它如何退出特定的for循环而没有break,但它确实退出了。
关于我提到的任何内容,任何帮助都将不胜感激!
ValueTask
是完全异步的,没有线程相关的内容(不像Task
),而且它是一个结构体,所以没有GC开销(只有一点堆栈开销),此外,它使用Memory<byte>
进行方法重写,因此与Task
基本重写相比应该会获得更好的性能。 - John