高性能的UDP服务器,阻塞或非阻塞?C语言。

10

我一直在研究UDP的阻塞和非阻塞套接字,但很难理解它们之间的优劣。互联网上绝大多数评论似乎表明非阻塞更好,但并没有很具体地说明什么情况下使用非阻塞更好,至今我还没有找到什么参考资料能够说明在何种情况下使用阻塞更为可取。我希望社区能够在这个问题上提供一些见解。

为了让答案可以具体应用于我的问题集,下面简单介绍一下背景。我正在编写一个UDP服务器,在本地局域网上拥有40个连接,并且会不断流入数据。数据速率将平均达到250MB/s,峰值可达500+Mb/s,平均数据包大小约为1400字节。处理数据包的负载较轻,但由于消息数量巨大,效率和性能成为高优先级任务,以防止丢失数据包。

由于我没有找到与这个特定问题集类似的上下文信息,因此根据我所能获取的关于阻塞和非阻塞的信息,我不得不做出一些猜测。基本上,由于每个连接都将是几乎不断的数据包流,我认为使用阻塞套接字更可取,因为任何recv函数实际上被阻塞的时间将非常短暂,而使用事件驱动模型则会产生大量异步触发。我认为真正的问题在于管理40个线程的优先级,以确保每个线程都能获得其份额的CPU时间。我的想法和方法可能不正确,因此希望社区能够提供一些帮助。

~编辑~

虽然我担心线程设计如何影响/整合阻止/非阻止问题,但我真正关心的是从我的问题集的角度来看阻塞/非阻塞应该如何看待。如果线程确实成为问题,我可以选择线程池解决方案。

首先,想要感谢截至目前为止已经给出的回复。你们中的一些人提到,使用这么多套接字的单线程/套接字模型可能是一个不好的想法,我承认我自己对这个解决方案也有些犹豫。然而,在尼古拉的回复中的其中一个链接中,作者讨论了单线程/套接字模型,并链接到了一篇非常有趣的论文,我想在这里将它链接上,因为它打破了我关于线程与事件驱动模型之间区别的很多错误观念:为什么事件驱动是一个坏主意


你正在进行NetFlow收集,是吗? - emurano
4个回答

7

以下是一些链接,如果您还没有添加到书签中,不是答案,而是帮助:

C10K问题 by Dan Kegel,
高性能服务器架构 by Jeff Darcy,
高级投票API:epoll(4)kqueue(2)

编辑:

听起来很蠢,但我完全忘记了您正在使用UDP,所以...

由于UDP没有协议级连接,并且除非您必须在不同的端口上工作,否则您不需要40个服务器套接字。只需一个UDP“服务器”套接字即可为所有客户端提供服务。您可以在此一个套接字上阻塞,只需确保套接字接收缓冲区足够大以容纳流量峰值,并且不要花费太多时间处理每个读取。


嗨,谢谢您的回复尼古拉和感谢您提供这些链接,我现在会去查看它们。我提到的40个不同连接是一个IP的40个端口。所以,不幸的是,我正在处理需要40个不同套接字的不同端口。 - jim

2

我不知道阻塞或非阻塞有很大的性能优势;更多的是关于你的网络I/O事件循环想要做什么样的事情:

  • 如果你的网络I/O线程唯一要做的事情就是在单个套接字上监听传入的UDP数据包,那么阻塞I/O可能会很好用,并且编程也更容易。

  • 如果你的网络I/O线程需要处理多个套接字,那么阻塞I/O会成为问题,因为如果它在套接字A上阻塞,它将无法被唤醒以处理到达套接字B的数据,反之亦然。在这种情况下,非阻塞I/O变得更可取,因为你可以在select()或poll()中进行阻塞,只要任何一个被观察的套接字上有数据可用,它就会返回。

请注意,即使在非阻塞情况下,你也不想在数据包之间忙等待,因为在线程A中消耗CPU周期意味着它们将不可用于线程B,这会影响性能。因此,如果你没有在recv()中阻塞,请确保在select()或poll()中阻塞。


3
另外,如果您想要尽量减少丢失的UDP数据包,我建议采取以下措施:(1) 在UDP接收线程中尽可能少地执行任务。理想情况下,只需接收数据包,然后将它们添加到由另一个线程处理的队列中,并尽快返回select()/recv();(2) 将接收线程的优先级设置得高于处理线程的优先级;(3) 使用setsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...)将UDP套接字的接收缓冲区设置得尽可能大。 - Jeremy Friesner
如果线程可以调用recv(),为什么不能调用select()或poll()?在单个fd上使用这些是一种常见的习惯用法,以确保可以调用recv()而不会阻塞或控制允许的超时。如果poll()在所需的时间内未检测到可读取的数据,我们就不会调用recv()。 - kriss
你是指考虑不检查哪个fd引发了读取事件吗?那样做是不好的。 - kriss
当阻塞套接字被选择为可读时,在该套接字上调用recv()一次是安全的(除了在Linux下,由于“有趣”的内核行为,即使第一次调用也可能仍然会阻塞,叹息)。然而,比必要更频繁地重新执行整个事件循环是不可取的,因为你(通常)想做的是不断调用recv()直到套接字没有更多数据可提供给你,然后再回到select()poll()中休眠。但是对于阻塞套接字,如果你尝试这样做,你很可能会在recv()内部阻塞,这将是不好的。 - Jeremy Friesner
无论如何,如果您不想在select()poll()之外阻塞,那么“安全”的做法是将套接字设置为非阻塞;否则,在您没有意图阻塞的上下文中仍然存在阻塞的风险。 - Jeremy Friesner
显示剩余7条评论

1

我不确定为40个套接字使用40个线程是一个好主意... 当你只有少量的套接字时,使用每个套接字一个线程是有意义的,但是拥有那么多线程只会导致线程饥饿、死锁和丢失数据包。

至于阻塞与非阻塞,记住阻塞可能相对昂贵... 尽管在我看来它更容易处理。异步触发器等可能比阻塞/唤醒线程更快。


虽然死锁不会成为问题,因为每个连接将独立处理,但丢失数据包和线程饥饿可能是一个问题。我更关心为什么阻塞更加昂贵,特别是在这个例子中的应用。如果调用很少被阻塞,我不确定这比一连串的事件通知更好。 - jim

1
  • 当使用阻塞 IO 时,在程序中的某个点上,您应该有一个轮询或选择等待数据在任何一个文件句柄(在您的情况下套接字)上。因为如果您在任何 fh 上读取而不确保数据已准备好,它将阻塞并且程序将停止管理其他套接字。为了避免这种情况,以及使程序简单,使用阻塞 IO 的程序通常为每个套接字/fh 编写一条线程,从而避免了对 poll 或 select 的需求。

  • 如果您使用非阻塞 IO,则程序将只运行并检查数据到达情况。不需要进行轮询或选择。该程序仍然可以相当简单,并且也没有必要为此特定目的使用线程。

我认为最有效的方法是使用 poll 或 select 来同时管理多个 IO(可以是分配给线程的所有文件句柄的子集,如果您愿意)。与没有 poll 或 select 的非阻塞 IO 相比,这种方法更高效,因为该方法基本上尝试在大多数时间内无用地读取每个套接字,并且这具有成本。这三种方法中最糟糕的方法是使用每个线程一个 fh 的阻塞 IO,因为线程管理的高成本与读取返回 WOULD BLOCK 或 poll 的成本相比。

这样说,非阻塞IO还有另一个优点:您的程序可能除了IO之外还有计算任务,而当被阻塞等待IO时,您无法执行它。这可能会导致使用轮询/选择与非阻塞IO一起使用或者使用小的专用线程来处理IO,而其他线程则用于处理更加计算密集型的部分或程序。
在某些情况下,您可能也没有选择。我曾经不得不等待通过NFS挂载的文件句柄中的数据。在这种情况下,尝试设置非阻塞IO是无用的,因为NFS层在内部使用阻塞IO...
您还可以考虑使用异步IO。它非常高效,使您的程序变成“事件驱动”。这在Windows系统中非常常见,我还没有查看Linux异步IO的当前开发状态。上次我检查时,有些人正在努力将异步IO添加到内核API中,但我不知道它是否稳定或已达到主流内核。

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