更好的方法:在同一端口上有两个或更多个线程监听

4

亲爱的朋友们,

我目前正在开发一个网络服务器应用程序。我计划为每个服务器核心分配一个线程来处理客户端连接,以更好地利用服务器资源、获得更快的响应时间并避免阻塞等问题。

据我所知,有两种方法可以实现:

1)父进程打开监听套接字,每个线程监视它(使用epoll、kqueue等)以获取新的连接,并 accept() 它们;

2)每个线程在相同的端口和地址上打开自己的监听套接字(由于 SO_REUSEPORT 和 SO_REUSEADDR 的存在),监视它以获取新的连接并 accept() 它们。

我不确定它背后的工作原理,但我认为像 #2 这样做将把客户端连接分配到各个线程中去,由内核网络部分完成这项任务。这是正确的吗?

这两种方法之间是否有任何显著的区别?在一种方法中可能会出现问题而在另一种方法中却不会出现吗?采用某种特定的方法是否会获得更好的结果(更少的资源使用、更少的延迟等)?

我认为只有通过比较才能看出两种方法之间的差异,但是我的电脑无法处理大量的连接。因此,我倚靠您的经验和知识来回答这个问题。提前感谢您的帮助。

2个回答

2

1) 父进程打开监听套接字,每个线程监视它 (使用 epoll、kqueue 等) 来获取新连接并接受连接;

虽然可以这样做,但与仅有一个 accept() 线程和线程池相比,几乎没有太大的好处。而且在接受连接的线程之间会出现竞争,只有其中一个线程会赢得连接 ('thundering herd' 问题)。

2) 每个线程在相同的端口和地址上打开自己的监听套接字(SO_REUSEPORT 和 SO_REUSEADDR 的帮助下),监视它以获取新连接并 accept() 它们。

除非您在 Windows 上,否则不能使用 TCP 进行此操作,因为就我所知,语义未定义。


我对“雷鸣群”问题感到困惑,因为有些人说它已经在早期版本的Linux内核中得到了解决(没有提到BSD)。看起来使用信号量可以解决这个问题。关于第二个问题:如果不是允许在同一端口和地址上有多个监听套接字,那么SO_REUSEPORT和SO_REUSEADDR在Linux/BSD上的目的是什么? - Tiago.SR
1
代码中会出现雷鸣般的问题,而不是在Linux内核中。所有选择器都将触发;所有线程都将调用accept();但只有一个线程会得到-1和errno == EAGAIN。SO_REUSEPORT用于多播,而SO_REUSEADDR仅适用于TCP的一种情况,这不是它的用途。 - user207421
我读到的关于“雷鸣群”问题的解决方法是在Linux内核中进行了一些更改,导致只有一个进程/线程accept()被通知新连接。然而,我不确定它是否已经被实现,因此不会依赖它,即使我打算让我的应用程序在FreeBSD上运行。 - Tiago.SR
1
这适用于阻塞模式。您提到您正在使用 select()poll(),这意味着非阻塞模式。 - user207421
4
我不相信 @EJP 对第二个解决方案的回复是正确的。在 Linux 内核版本 >= 3.9 上,SO_REUSEPORT 确实允许多个线程在同一 IP 和端口上进行监听,即使是 TCP 也是如此。实际上,这就是它被引入的原因 - Jon Gjengset

1

这里有一个建议,当父线程开始监听请求时,只有在新连接到达并返回一个新的套接字后才创建新线程,现在您可以在任何线程中使用该套接字,不会出现任何问题。谢谢。


你的建议是关于对工作线程进行惰性初始化,还是为每个新到达的连接创建一个新线程?我不太明白。 - Tiago.SR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接