众所周知,SO_REUSEPORT允许多个套接字在相同的IP地址和端口组合上进行监听 ,每秒增加2到3次 ,并减less延迟(〜30%)和延迟(8次)的标准差: https ://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
NGINX版本1.9.1引入了一个新特性,可以使用SO_REUSEPORT套接字选项,这个选项可以在很多操作系统的新版本中使用,包括DragonFly BSD和Linux(内核版本3.9和更高版本) 。 此套接字选项允许多个套接字侦听相同的IP地址和端口组合 。 内核然后通过套接字负载平衡传入的连接。 …
如图所示, 重新使用端口每秒增加2到3次请求,并减less延迟和延迟的标准偏差。
SO_REUSEPORT
可用于大多数现代操作系统 :Linux(自2013年4月29日起 内核> = 3.9 ),Free / Open / NetBSD,MacOS,iOS / watchOS / tvOS, IBM AIX 7.2 , Oracle Solaris 11.1 ,Windows(仅为SO_REUSEPORT
, 2个标志SO_REUSEPORT
+ SO_REUSEADDR
在BSD中),并可能在Android上 : https : //stackoverflow.com/a/14388707/1558037
Linux> = 3.9
- 此外,内核对
SO_REUSEPORT
套接字执行一些在其他操作系统中找不到的“特殊魔力”:对于UDP套接字,它试图平均地分配数据报,对于TCP侦听套接字,它试图分发传入的连接请求accept()
)平均分配到所有共享相同地址和端口组合的套接字。 因此,应用程序可以轻松地在多个subprocess中打开相同的端口,然后使用SO_REUSEPORT
获得非常便宜的负载平衡 。
另外,为了避免旋转锁的locking并实现高性能,不应该有读取多于一个线的插槽。 即每个线程应该处理自己的套接字进行读/写。
accept()
是同一个套接字描述符的线程安全函数,所以它应该被锁保护 – 所以锁争用降低了性能 : http : //unix.derkeiler.com/Newsgroups/comp.unix.programmer/2007-06/ msg00246.html POSIX.1-2001 / SUSv3 需要accept(),bind(),connect(),listen(),socket(),send(),recv()等为线程安全函数。 关于它们与线程的交互,标准中可能存在一些含糊之处,但是其意图是它们在multithreading程序中的行为是由标准来pipe理的。
与单线程程序相比,接收性能下降。 这是由UDP接收缓冲区上的锁争用引起的 。 由于两个线程都使用相同的套接字描述符,所以他们花费了大量的时间争取UDP接收缓冲区的locking。 本文更详细地描述了这个问题。
V. K ERNEL隔离
….
另一方面, 当应用程序试图从套接字读取数据时 ,它执行一个类似的过程,如下所示,从右到左如图3所示:
1) 使用相应的自旋锁 (绿色)从接收队列中取出一个或多个数据包。
2)将信息复制到用户空间内存。
3)释放数据包使用的内存。 这可能会改变套接字的状态,所以可能会发生两种locking套接字的方法:快速和慢速。 在这两种情况下,数据包都与套接字断开连接,“记忆计数”统计信息将更新,并根据所采用的lockingpath释放套接字。
也就是说,当许multithreading访问相同的套接字时,由于等待一个自旋锁,性能会降低。
我们有2个Xeon 32 HT-Cores服务器,共64个HT内核,两个10 Gbit以太网卡和Linux(内核3.9)。
我们使用RFS和XPS – 即在同一个CPU-Core上为应用程序线程(用户空间)处理相同连接的TCP / IP堆栈(内核空间)。
至less有三种方法可以接受连接,以便在多个线程中进行处理:
ip:port
,1个单独的acceptor套接字,接收连接的线程将处理它(recv / send) 什么是更有效的方式,如果我们接受很多新的TCP连接?
在生产中必须处理这样的场合,这是解决这个问题的好方法:
首先,设置一个线程来处理所有传入的连接。 修改亲和性映射,以便该线程拥有一个专用核心,应用程序(甚至整个系统)中的其他线程都不会尝试访问。 您还可以修改引导脚本,以便某些内核永远不会自动分配给执行单元,除非特定的内核被明确请求(即isolcpus
内核引导参数)。
将该内核标记为未使用, 然后在您的代码中通过cpuset
显式请求“侦听套接字”线程。
接下来,建立一个队列(理想情况下,一个优先级队列),优先写入操作(即“第二个读写器问题)。现在,设置很多工作线程,你认为合理。
在这一点上,“传入连接”线程的目标应该是:
accept()
传入的连接。 accept()
状态。 这将允许您尽快委派传入连接。 您的工作线程可以在到达时从共享队列中获取项目。 也许值得拥有第二个高优先级的线程来抓取来自这个队列的数据,并将其移动到第二个队列中,从而使得“监听套接字”线程不必花费委托客户端FD的额外周期。
这也会阻止“监听套接字”线程和工作线程同时访问相同的队列,这将使您免受最糟糕的情况,比如缓慢的工作线程在“监听套接字”线程时锁定队列想要放下数据。 即
Incoming client connections || || listner thread - accept() connection. \/ listner/Helper queue || || Helper thread \/ Shared Worker queue || || Worker thread #n \/ Worker-specific memory space. read() from client.
至于你提出的其他两个选项:
使用一个在许多线程之间共享的接受者套接字,并且每个线程接受连接并处理它。
乱。 线程将不得不轮流发出accept()
调用,这样做没有任何好处。 你还将有一些额外的顺序逻辑来处理哪个线程的“转向”。
使用许多接收者套接字,它们在每个线程中监听相同的ip:port,1个单独的接受者套接字,接收连接的线程将处理它(recv / send)
不是最便携的选择。 我会避免它。 此外,您可能需要使服务器进程使用多进程(即fork()
)而不是多线程,具体取决于操作系统,内核版本等。
假设你有两个10Gbps的网络连接,并假设一个500byte的平均帧大小(这是非常保守的一个服务器没有交互使用),你将有每个网卡每秒约2Mpackets(我不相信你有超过这个)这意味着每微秒处理4个数据包。 这对于你的配置中描述的CPU来说是非常慢的延迟。 在这些地方,我要确保你的瓶颈将会在网络(和你连接的交换机)中,而不是在每个socket上的自旋锁(在自旋锁上需要一些CPU周期来解决,这远远超出了由网络)。 不管怎么说,我会在每张网卡上专用一两个线程(一个用于读取,另外一个用于写入),并且不要在套接字锁定功能中多加考虑。 最有可能的是你的瓶颈是你在这个配置的后端应用软件。
即使在遇到麻烦的情况下,也许最好对内核软件进行一些修改,而不是增加越来越多的处理器,或者考虑将自旋锁分布到不同的套接字中。 或者甚至更好,增加更多的网卡来消除瓶颈。
使用许多接收者套接字,它们在每个线程中监听相同的ip:port,1个单独的接受者套接字,接收连接的线程将处理它(recv / send)
在TCP中这是不可能的。 算了吧。
做别人做的事。 一个接受线程,它根据接受的套接字启动一个新的线程,或者将它们分派给一个线程池。