我们应该使用多个接受器套接字来接受大量连接吗?

时间:2017-07-09 21:49:26

标签: c linux multithreading sockets tcp

众所周知,SO_REUSEPORT允许多个套接字侦听相同的IP地址和端口组合,它每秒将请求增加 2到3次,并减少延迟(~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地址和端口组合。内核   然后负载平衡套接字上的传入连接。   ...

     

如图所示, reuseport将每秒请求数增加2   到3倍,并减少潜伏期和标准偏差   等待时间。

enter image description here

enter image description here

enter image description here

SO_REUSEPORT适用于大多数现代操作系统:Linux(kernel >= 3.9以来29 Apr 2013),免费/开放/ NetBSD,MacOS,iOS / watchOS / tvOS ,IBM AIX 7.2Oracle Solaris 11.1,Windows(只有SO_REUSEPORT在BSD中同时表现为SO_REUSEPORT + SO_REUSEADDR两个标志,并且可能在{{3 }:Android

  

Linux> = 3.9

     
      
  1. 此外,内核会执行一些特殊的魔法"对于在其他操作系统中找不到的SO_REUSEPORT套接字:   对于UDP套接字,它尝试均匀分配数据报,用于 TCP   侦听套接字,它会尝试分发传入的连接请求   (通过调用accept()接受的那些)均匀地跨越所有套接字   共享相同的地址和端口组合。因此一个应用程序   可以轻松地在多个子进程中打开相同的端口然后使用   SO_REUSEPORT以获得非常便宜的负载平衡
  2.   

同样众所周知,为了避免自旋锁定和高性能锁定,不应该是读取超过1个线程的套接字。即每个线程都应该处理自己的套接字以进行读/写。

  

POSIX.1-2001 / SUSv3 需要accept(),bind(),connect(),listen(),   socket(),send(),recv()等是线程安全的函数。它' s   标准中可能存在一些含糊之处   他们与线程的互动,但意图是他们的   多线程程序中的行为受标准管理。

  

与单线程相比,接收性能下降   程序。由UDP接收缓冲区上的锁争用引起的   侧。由于两个线程都使用相同的套接字描述符,因此它们   花费不成比例的时间争取锁定   UDP接收缓冲区。本文更详细地描述了这个问题。

  

诉K ERNEL ISOLATION

     

...

     

另一方面,当应用程序尝试读取数据时   从套接字,它执行一个类似的过程,其中   如下所示并在图3中从右到左表示:

     

1)从接收队列中取出一个或多个数据包,使用   相应的螺旋锁(绿色)。

     

2)将信息复制到用户空间内存。

     

3)释放数据包使用的内存。这个   可能会改变套接字的状态,所以有两种方法可以锁定   套接字可以发生:快速和慢速。在这两种情况下,数据包都是   从套接字取消链接,内存记帐统计信息更新和   根据所采用的锁定路径释放插座。

即。当许多线程访问同一个套接字时,由于等待一次自旋锁定,性能会下降。

我们有2个Xeon 32 HT-Core服务器,64个HT核心,2个10 Gbit以太网卡和Linux(内核3.9)。

我们使用RFS和XPS - 即在与应用程序线程(用户空间)相同的CPU-Core上处理相同的连接TCP / IP堆栈(内核空间)。

至少有3种方法可以接受连接以在多个线程上处理它:

  • 使用多个线程共享的一个接受器套接字,每个线程接受连接并对其进行处理
  • 在1个线程中使用一个接受器套接字,并且此线程通过使用线程安全队列推送接收到其他线程工作者的连接的套接字描述符
  • 使用多个接受器套接字,在每个线程中侦听相同的ip:port,1个单独的接受器套接字 ,然后接收连接的线程处理它(的recv /发送)

如果我们接受大量新的TCP连接,那么效率更高的方法是什么?

3 个答案:

答案 0 :(得分:10)

必须在生产中处理这样的场合,这是解决这个问题的好方法:

首先,设置一个线程来处理所有传入连接。修改关联性映射,以便此线程具有专用核心,使应用程序(甚至整个系统)中的其他线程不会尝试访问。 You can also modify your boot scripts so that certain cores are never automatically assigned to an execution unit unless that specific core is explicitly requested (i.e. isolcpus kernel boot parameters).

将该核心标记为未使用,and then explicitly request it in your code for the "listen to socket" thread via cpuset.

接下来,设置一个队列(理想情况下,优先级队列),优先考虑写入操作(i.e. "the second readers-writers problem).现在,根据您的需要设置多个工作线程。

此时,“传入连接”线程的目标应该是:

  • accept()传入连接。
  • 尽快将这些连接文件描述符(FD)传递给编写器优先级的队列结构。
  • 尽快回到accept()状态。

这将允许您尽快委派传入的连接。您的工作线程可以在到达时从共享队列中获取项目。也许值得拥有第二个高优先级线程,该线程从该队列中获取数据,并将其移动到辅助队列,从而节省“监听套接字”线程不必花费额外的周期来委派客户端FD。

这也会阻止“侦听套接字”线程和工作线程不必同时访问同一个队列,这样可以避免在最糟糕的情况下,例如缓慢的工作线程锁定队列时“听” socket“thread想要删除数据。即。

Incoming client connections

 ||
 || Listener thread - accept() connection.
 \/

Listener/Helper queue

 ||
 || Helper thread
 \/

Shared Worker queue

 ||
 || Worker thread #n
 \/

Worker-specific memory space. read() from client.

至于你提出的另外两个选择:

  

使用在多个线程和每个线程之间共享的一个接受器套接字   接受连接并处理它。

凌乱。线程必须以某种方式轮流发出accept()调用,这样做没有任何好处。您还将有一些额外的排序逻辑来处理哪个线程的“转向”。

  

使用许多听取相同ip:port,1个人的接受器套接字   每个线程中的acceptor套接字,以及接收的线程   然后连接处理它(recv / send)

Not the most portable option. I'd avoid it.此外,您可能需要使服务器进程使用多进程(即fork())而不是多线程,具体取决于操作系统,内核版本等。

答案 1 :(得分:0)

假设你有两个10Gbps的网络连接并且假设平均帧大小为500byby(对于没有交互式使用的服务器而言非常保守),每个网卡每秒大约有2M个数据包(我不相信你有更多而这意味着这意味着每微秒处理4个数据包。对于像您的配置中描述的cpu一样,这是一个非常慢的延迟。在这些前提下,我确保您的瓶颈将在网络中(以及您连接的交换机),而不是每个套接字上的自旋锁(在自旋锁上需要一些cpu周期才能解决),这远远超出了网络)。或者,我会在每个网卡上专门设置一个或两个线程(一个用于读取,另一个用于写入),并且在套接字锁定功能中不要多考虑。最可能的是您的瓶颈在于您在此配置后端的应用程序软件中。

即使遇到麻烦,也许最好对内核软件进行一些修改,而不是添加越来越多的处理器或考虑将自旋锁分配到不同的套接字中。或者更好的是,添加更多网卡以缓解瓶颈。

答案 2 :(得分:-4)

  

使用许多接受套接字,它们在每个线程中侦听相同的ip:port,1个单独的接受器套接字,然后接收连接的线程处理它(recv / send)

这在TCP中是不可能的。算了吧。

做别人做的事。一个接受线程,它为每个接受的套接字启动一个新线程,或者将它们发送到一个线程池。