C++套接字服务器 - 无法充分利用CPU

30
我使用boost::asio在C++中开发了一个小型HTTP服务器,现在正在用多个客户端进行负载测试,但是我无法使CPU接近饱和。我在Amazon EC2实例上进行测试,一个CPU的使用率约为50%,另一个CPU的使用率为20%,其余两个CPU处于空闲状态(根据htop)。
详情:
- 服务器每核启动一个线程 - 接收请求,解析、处理并写出响应 - 请求数据只是从内存中读取(仅限此测试) - 我使用两台机器进行"负载",每个机器运行一个Java应用程序,运行25个线程,发送请求。 - 我看到大约230个请求/秒吞吐量(这是由许多HTTP请求组成的“应用程序”请求)
那么,我该关注什么来改进这个结果?考虑到CPU大部分时间都处于空闲状态,我想利用额外的容量来获得更高的吞吐量,比如800个请求/秒或其他。
我想过的一些想法:
- 请求非常小,并且通常在几毫秒内完成,可以修改客户端以发送/组合更大的请求(可能使用批处理)。 - 我可以修改HTTP服务器以使用Select设计模式,在这里是否适用? - 我可以进行一些分析以了解瓶颈所在。

可以假设您的服务器有1Gbps端口吗?您的请求和响应大小(在传输中)是多少? - nik
2
服务器网络端口(我假设为1Gbps)的带宽利用率是多少? - nik
1
测试正在 EC2 上运行,我相信使用千兆位。 Bmon 报告大约为 3MiB(我相信是兆比特)的 TX 速率和 2.5Mib 的 RX 速率。许多请求/响应大小很小(只有100字节),但某些响应高达1MB,请求可能高达0.25MB。 - waterlooalex
你的客户端负载如何?如果每个核心只有1个线程,而且没有使用IO多路复用(select/poll或类似工具),则并发性不高,线程很可能会花费大量时间进行I/O操作。 - nos
1
每个客户端机器都在运行一个进程,并且有 25 个线程正在运行。 - waterlooalex
一个4核CPU上为单个进程分配25个线程?这太过于过度了,特别是在Linux下。 - unixman83
6个回答

46

boost::asio并不像您所希望的那样友好,因为在boost/asio/detail/epoll_reactor.hpp中的epoll代码周围有一个大锁,这意味着每次仅有一个线程可以调用内核的epoll系统调用。对于非常小的请求,这会产生很大的影响(意味着您只能看到大约单线程的性能表现)。

请注意,这是boost::asio如何使用Linux内核设施的限制,而不一定是Linux内核本身的限制。当使用边缘触发事件时,epoll系统调用确实支持多个线程,但是正确地进行操作(而不需要过多的锁定)可能会非常棘手。

顺便说一句,我一直在这个领域进行一些工作(将完全多线程的边缘触发的epoll事件循环与用户计划的线程/纤程结合起来),并在nginetd项目下提供了一些代码。


1
(+1) cmeer 我有一个关于boost::asio在Windows和Linux上性能的未回答的帖子。如果你已经阅读了大部分asio,请来回答我的帖子 :P - Hassan Syed
3
我非常担心这个全局锁。实际上它并不像看起来那么严重。瓶颈只会出现在高吞吐量的情况下。但是,当asio运行在epoll模式(Linux)时,在发出“异步_ *”调用时,它会主动尝试写入或读取。在高输入场景中,套接字通常准备好读取,让async_read完全跳过epoll。对于网络性能而言,你不能再要求更好了。 - deft_code
3
我不认为是这种情况。是的,看起来 epoll reactor 在 run() 函数的整个持续时间内都有一个作用域锁,但在调用 epoll_wait 之前会暂时释放 ("lock.unlock();") ,并在 epoll_wait 返回后再次加锁 ("lock.lock();")。不确定为什么要这样做,而不是使用两个作用域锁。 - Alex B
3
如果只有一个线程在使用io_service,您可以使用BOOST_ASIO_DISABLE_THREADS宏来解除锁定。这样做应该是安全的,但请确保不会影响其他线程。 - Joshua Chia
1
2021年有对此做出任何更改吗? - InQusitive
显示剩余7条评论

12

由于您正在使用EC2,所有赌注都已经失效了。

尝试使用真实的硬件进行测试,那么您可能能够看到发生了什么。在虚拟机上进行性能测试基本上是不可能的。

我还没有想出EC2有什么用处,如果有人知道,请告诉我。


这个系统将要部署在EC2上,因此我认为在真实硬件上测试系统的性能并不会有所帮助。 - waterlooalex
9
马克的观点是正确的:要进行性能分析,请使用真实机器,或者至少在更加可控的环境下进行。你可以将部署到 EC2 上,但请理解你正在运行在一个虚拟机镜像中,这意味着你的“空闲”CPU可能只是因为其他租户在一段时间内获得了所有的CPU。这会使得性能分析变得困难。 - janm
8
目前每时每刻都有数十万(据我所知)的EC2实例在运行,因此很多人都理解它的用途。你应该问问自己,他们知道什么,而你不知道。 - Eloff
1
指出在虚拟机中进行性能测试是不可能的,特别是对于网络场景 - 您必须使用物理盒、物理交换机进行测试,并能够监视QoS。一旦完成测试,您可以推送到EC2。然后,当您遇到CPU / RAM使用问题时,您可以确定是EC2 / Rackspace的问题。 - quixver

3
从你对网络利用率的评论来看,你似乎没有太多的网络活动。3 + 2.5 MiB /秒大约是50Mbps(与你的1Gbps端口相比)。我认为你可能有以下两个问题之一:
1.工作量不足(客户端请求速率低)
- 服务器阻塞(干扰响应生成)
根据cmeerw的注释和你的CPU利用率数字(在50%+20%+0%+0%处空闲),看起来最可能是你的服务器实现的限制。我支持cmeerw的答案(+1)。

1
他正在Amazon的EC2云计算集群上运行测试。很难排除EC2性能不佳的可能性。 - unixman83

3
230个请求/秒似乎对于这样简单的异步请求来说很低。因此,使用多个线程可能是过早进行的优化——先在单个线程中使其正常工作并进行调整,然后再看看是否仍然需要它们。只要消除不必要的锁定可能就能提高速度。 这篇文章详细讨论了2003年左右的Web服务器性能的I/O策略。有没有更近期的资料呢?

请注意,每秒230个请求是“应用程序请求”,由许多实际的HTTP请求组成。 - waterlooalex
没有太多需要消除的锁,我的代码中没有,但正如cmeerw所指出的那样,boost::asio会进行一些内部锁定。HTTP服务器仅执行CPU绑定工作,因此不使用其他核心将是一种昂贵的浪费。 - waterlooalex
2
如果目标只是为了使CPU饱和,那么在一个线程中完成工作,并让其他三个线程计算PI或其他内容。拥有多个用户级线程不会使操作系统和IO硬件更轻松或更快地读写网络数据包。线程和核心用于计算工作,如果您没有进行任何计算工作,则它们不可能为您带来任何好处,并且可能与系统正在执行的其他任务产生竞争。 - soru
除非有明显的证明,否则这并不是最优解。最佳方案可能是一个线程进行I/O操作,另外两个或三个线程用于解析等操作。但在能够正确异步调度I/O操作以使CPU核饱和或网络饱和之前,这很可能是过早的优化。 - soru
我明白你的意思。好的,我会启动服务器,用一个线程进行快速测试,看看有什么结果。 - waterlooalex

2
ASIO适用于小到中等任务,但不太擅长发挥底层系统的性能。原始套接字调用或甚至Windows上的IOCP也无法做到这一点,但是如果您有经验,那么总是比ASIO更好。无论如何,所有这些方法都具有很多开销,只是ASIO更多。
值得一提的是,使用基本套接字调用的自定义HTTP可以每秒服务800K个动态请求,使用4个内核的I7处理器。它从RAM中提供服务,这也是您需要达到该性能水平的地方。在这种性能水平下,网络驱动程序和操作系统占用了大约40%的CPU。使用ASIO,我可以获得大约50到100K的请求每秒,它的性能相当不稳定,而且大部分绑定在我的应用程序中。@cmeerw发布的帖子主要解释了原因。
提高性能的一种方法是通过实现UDP代理来实现。拦截HTTP请求,然后将它们通过UDP路由到您的后端UDP-HTTP服务器,您可以避免操作系统堆栈中的许多TCP开销。您还可以拥有自己的前端,它们自己通过UDP进行管道传输,这应该不太难自己完成。 HTTP-UDP代理的优点是允许您使用任何好的前端而无需修改,并且可以在不影响的情况下随时更换它们。您只需要实现几个额外的服务器即可。对我的示例进行的此修改将操作系统CPU使用率降低到10%,这将使单个后端的请求每秒数增加到略高于一百万次。顺便说一句,对于任何高性能站点,您应始终具有前端 - 后端设置,因为前端可以缓存数据而不会减慢更重要的动态请求后端。
未来似乎是编写自己的驱动程序,以实现自己的网络堆栈,以便您可以尽可能接近请求并在那里实现自己的协议。这可能不是大多数程序员想要听到的,因为它更加复杂。在我的情况下,我将能够使用更多的40%CPU,并将动态请求数提高到1百万次/秒以上。 UDP代理方法可以让您接近最优性能而无需执行此操作,但是您将需要更多的服务器-虽然如果您执行此类每秒请求量,通常需要多个网络卡和多个前端来处理带宽,因此在其中拥有几个轻型UDP代理并不是太大的问题。
希望其中的一些内容对您有用。

2
能否展示一个例子或工作项目?没有它,这就和无关紧要的话一样毫无用处。不是贬低你,但这里需要一些具体的代码。 - Abhinav Gauniyal

0
你有多少个io_service实例?Boost asio有一个示例,它会为每个CPU创建一个io_service,并以RoundRobin的方式使用它们。
你仍然可以创建四个线程,并将每个线程分配给一个CPU,但每个线程可以在自己的io_service上轮询。

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