Ale*_*lex 12 c sockets linux multithreading 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倍,并减少了延迟和延迟的标准偏差.
SO_REUSEPORT
适用于大多数现代操作系统:Linux(自2013年4月29日起内核> = 3.9),Free/Open/NetBSD,MacOS,iOS/watchOS/tvOS,IBM AIX 7.2,Oracle Solaris 11.1,Windows(仅表现为2个标志)在BSD上一起+ ,可能在Android上:https: //stackoverflow.com/a/14388707/1558037SO_REUSEPORT
SO_REUSEPORT
SO_REUSEADDR
Linux> = 3.9
- 此外,内核为
SO_REUSEPORT
其他操作系统中找不到的套接字执行一些"特殊魔法" :对于UDP套接字,它尝试均匀分布数据报,对于TCP侦听套接字,它会尝试分发传入的连接请求 (通过调用接受的请求accept()
)均匀地跨越共享相同地址和端口组合的所有套接字.因此,应用程序可以轻松地在多个子进程中打开相同的端口,然后使用它SO_REUSEPORT
来获得非常便宜的负载平衡.
同样众所周知,为了避免自旋锁定和高性能锁定,不应该有超过1个线程的套接字.即每个线程都应该处理自己的套接字以进行读/写.
accept()
对于相同的套接字描述符是线程安全的函数,因此它应该被锁定 - 因此锁争用会降低性能:http://unix.derkeiler.com/Newsgroups/comp.unix.programmer/2007-06/msg00246.htmlPOSIX.1-2001/SUSv3 需要accept(),bind(),connect(),listen(),socket(),send(),recv()等作为线程安全函数.标准中可能存在关于它们与线程交互的一些含糊之处,但其意图是它们在多线程程序中的行为受标准控制.
与单线程程序相比,接收性能下降.这是由UDP接收缓冲区的锁争用引起的 .由于两个线程都使用相同的套接字描述符,因此它们花费了不成比例的时间来争夺UDP接收缓冲区周围的锁定.本文更详细地描述了该问题.
V. 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种方法可以接受连接以在多个线程上处理它:
ip:port
接收器套接字的接收器套接字,然后接收连接的线程处理它(recv/send)什么是更有效的方法,如果我们接受许多新的TCP连接?
Clo*_*oud 10
在生产中不得不处理这样的场合,这是解决这个问题的好方法:
首先,设置一个线程来处理所有传入连接.修改关联性映射,以便此线程具有专用核心,使应用程序(甚至整个系统)中的其他线程不会尝试访问.您还可以修改引导脚本,以便某些内核永远不会自动分配给执行单元,除非明确请求该特定内核(即isolcpus
内核引导参数).
将该核心标记为未使用,然后在您的代码中明确请求"侦听套接字"线程cpuset
.
接下来,设置一个队列(理想情况下,优先级队列),优先考虑写操作(即"第二个读者 - 编写者问题).现在,设置许多工作线程,你认为合理.
此时,"传入连接"线程的目标应该是:
accept()
传入连接.accept()
尽快回到自己的状态.这将允许您尽快委派传入连接.您的工作线程可以在到达时从共享队列中获取项目.可能还值得拥有第二个高优先级线程,该线程从该队列中获取数据,并将其移动到辅助队列,从而使"监听套接字"线程不必花费额外的周期来委派客户端FD.
这也可以防止"监听套接字"线程和工作线程不必同时访问同一个队列,这样可以避免在最糟糕的情况下,比如"侦听套接字"线程时锁定队列的慢工作线程想要删除数据.即
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.
Run Code Online (Sandbox Code Playgroud)
至于你提出的另外两个选择:
使用在多个线程之间共享的一个接受器套接字,每个线程接受连接并处理它.
乱.线程必须以某种方式轮流发出accept()
调用,这样做没有任何好处.您还将有一些额外的排序逻辑来处理哪个线程的"转向".
使用许多接受器套接字,它们在每个线程中监听相同的ip:port,1个单独的接受器套接字,然后接收连接的线程处理它(recv/send)
不是最便携的选择.我会避免它.此外,您可能需要使服务器进程使用多进程(即fork()
)而不是多线程,具体取决于操作系统,内核版本等.
归档时间: |
|
查看次数: |
1775 次 |
最近记录: |