C++ 套接字服务器 - 无法使 CPU 饱和

Posted

技术标签:

【中文标题】C++ 套接字服务器 - 无法使 CPU 饱和【英文标题】:C++ Socket Server - Unable to saturate CPU 【发布时间】:2010-11-17 02:40:33 【问题描述】:

我使用 boost::asio 在 C++ 中开发了一个迷你 HTTP 服务器,现在我正在使用多个客户端对其进行负载测试,但我一直无法接近 CPU 饱和。我在一个 Amazon EC2 实例上进行测试,一个 cpu 的使用率约为 50%,另一个 cpu 的使用率为 20%,其余两个处于空闲状态(根据 htop)。

详情:

服务器每个内核启动一个线程 接收、解析、处理请求并写出响应 请求是针对从内存中读取的数据(本测试只读) 我正在使用两台机器“加载”服务器,每台机器运行一个 java 应用程序,运行 25 个线程,发送请求 我看到大约 230 个请求/秒的吞吐量(这是 应用程序 请求,由许多 HTTP 请求组成)

那么,我应该看什么来改善这个结果?鉴于 CPU 大部分时间处于空闲状态,我想利用额外的容量来获得更高的吞吐量,比如 800 个请求/秒或其他什么。

我的想法:

请求非常小,通常在几毫秒内完成,我可以修改客户端以发送/组合更大的请求(可能使用批处理) 我可以修改 HTTP 服务器以使用 Select 设计模式,这里合适吗? 我可以做一些分析来尝试了解瓶颈是什么

【问题讨论】:

公平地假设您在服务器上有一个 1Gbps 端口?您的请求和响应大小是多少(在线)? 服务器网络端口的带宽利用率是多少(我假设是1Gbps) 测试在 EC2 上运行,我相信它使用千兆位。 Bmon 报告了 3MiB(我相信是兆位)的 TX 速率和 2.5Mib 的 RX 速率。许多请求/响应的大小都很小(只有 100 字节),但有些响应高达 1mb,请求可能高达 0.25mb 您的客户有什么负担?如果每个核心只有 1 个线程,并且不使用 io 多路复用(选择/轮询或类似),您将不会获得太多并发性 - 并且线程可能会花费大量时间进行 i/o。 每台客户端机器运行一个进程,运行 25 个线程 【参考方案1】:

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

请注意,这是 boost::asio 如何使用 Linux 内核工具的限制,不一定是 Linux 内核本身。在使用边缘触发事件时,epoll 系统调用确实支持多线程,但要正确处理(无需过度锁定)可能非常棘手。

顺便说一句,我一直在这方面做了一些工作(将完全多线程的边缘触发 epoll 事件循环与用户调度的线程/光纤相结合)并在 nginetd 项目下提供了一些代码。

【讨论】:

(+1) cmeer 我有一篇关于 boost::asio 在 windows 和 linux 上的性能的未答复的帖子。如果您已经阅读了 asio 的大部分内容,请来回答我的帖子:P 我真的很担心这个全局锁。这并不是看起来那么大的问题。瓶颈只会出现在高吞吐量场景中。但是,当 asio 在 epoll 模式(linux)下运行时,它会在发出 async_* 调用时抢先尝试写入或读取。在高输入场景中,套接字通常会准备好读取,让async_read 完全跳过 epoll。您不能要求比这更好的网络性能了。 我不认为是这样。是的,看起来 epoll reactor 在 run() 函数的整个持续时间内都有一个作用域锁,但是在调用 epoll_wait 之前它被暂时释放(“lock.unlock();”)并在 epoll_wait 返回后再次锁定(“lock.unlock();”)。锁();”)。不过,不知道为什么这样做而不是两个作用域锁。 您可以使用 BOOST_ASIO_DISABLE_THREADS 宏解除锁定。如果只有一个线程在使用 io_service,那么这样做应该是安全的。 我很困惑,很想知道全局锁的影响。它真的可以将多线程应用程序退化为单线程应用程序吗?【参考方案2】:

当您使用 EC2 时,所有赌注都已取消。

使用真正的硬件尝试一下,然后您可能会看到发生了什么。尝试在虚拟机中进行性能测试基本上是不可能的。

我还没有弄清楚 EC2 有什么用,如果有人知道,请告诉我。

【讨论】:

这个系统将部署在EC2中,所以在真实硬件上测试系统的性能我认为没有帮助。 Mark 的观点是正确的:对于性能分析,请使用真机,或者至少是更可控的环境。随心所欲地部署到 EC2,但要了解您在 VM 映像中运行,这意味着您的“空闲”CPU 可能只是因为盒子上的其他租户在一段时间内获得了所有 CPU。这使得分析变得困难。 由于在任何给定时间点都有几十万(我上次听说)EC2 实例在运行,我想很多人都知道它有什么用处。你应该问问自己他们知道什么你不知道。 +1 指出在 VM 中进行性能测试是不可能的。特别是对于网络场景 - 您必须使用物理盒、物理交换机进行测试并能够监控 QoS。完成后 - 您可以推送到 EC2。然后,当您遇到 CPU/RAM 使用问题时 - 您可以确定是 EC2/Rackspace 有问题。【参考方案3】:

来自您对网络利用率的 cmets, 您似乎没有太多的网络活动。

3 + 2.5 MiB/sec50Mbps 球场附近(与您的 1Gbps 端口相比)。

我会说您遇到以下两个问题之一,

    工作负载不足(来自客户的低请求率) 在服务器中阻塞(干扰响应生成)

查看cmeerw 的笔记和您的 CPU 利用率数据 (闲置50% + 20% + 0% + 0%) 这似乎很可能是您的服务器实现的限制。 我第二次cmeerw 的回答(+1)。

【讨论】:

他正在亚马逊的 EC2 云计算集群上运行测试。很难排除 EC2 性能不佳的可能。【参考方案4】:

对于这种简单的异步请求,每秒 230 个请求似乎非常低。因此,使用多线程可能是过早的优化 - 让它正常工作并在单个线程中进行调整,看看你是否仍然需要它们。只是摆脱不必要的锁定可能会让事情跟上进度。

This article 有一些关于 2003 年左右 Web 服务器式性能的 I/O 策略的详细信息和讨论。有人知道最新的吗?

【讨论】:

请记住,每秒 230 个请求是由许多实际 HTTP 请求组成的“应用程序请求”。 没有太多的锁定可以摆脱,在我的代码中没有,但是正如 cmeerw 指出的 boost::asio 做了一些内部锁定。 HTTP 服务器执行纯 CPU 受限的工作,因此不使用额外的内核将是一种昂贵的浪费 如果目标只是使 CPU 饱和,则在一个线程中完成工作,并让其他三个计算 PI 或其他东西。拥有多个用户级线程不会使操作系统和 IO 硬件更容易或更快地读取和写入网络数据包。线程和内核用于计算工作,如果您不做任何事情,它们就不可能为您带来任何好处,并且可能会与系统正在做的其他事情发生争用。 除非,显然不是。最佳解决方案可能是一个线程进行 I/O 和 2 或 3 个解析等等。但这很可能是过早的优化,直到您可以正确地异步调度您的 IO,以便您使一个 CPU 内核或网络饱和。 我明白你在说什么。好吧,我将使用 1 个线程启动服务器作为快速测试,看看会发生什么。【参考方案5】:

ASIO 适用于中小型任务,但不太擅长利用底层系统的强大功能。原始套接字调用,甚至 Windows 上的 IOCP 都不是,但如果您有经验,您将永远比 ASIO 更好。无论哪种方式,所有这些方法都有很多开销,而 ASIO 则更多。

为了它的价值。在我的自定义 HTTP 上使用原始套接字调用可以使用 4 核 I7 每秒处理 800K 动态请求。它从 RAM 提供服务,这是您需要达到该级别性能的地方。在这种性能水平上,网络驱动程序和操作系统消耗了大约 40% 的 CPU。使用 ASIO,我每秒可以获得大约 50 到 100K 的请求,它的性能变化很大,并且主要受我的应用程序的约束。 @cmeerw 的帖子主要解释了原因。

提高性能的一种方法是实施 UDP 代理。拦截 HTTP 请求,然后通过 UDP 将它们路由到后端 UDP-HTTP 服务器,您可以绕过操作系统堆栈中的大量 TCP 开销。您还可以让前端通过 UDP 自己通过管道,这不应该太难自己做。 HTTP-UDP 代理的一个优点是它允许您使用任何好的前端而无需修改,并且您可以随意更换它们而不会产生任何影响。你只需要更多的服务器来实现它。对我的示例进行的此修改将操作系统 CPU 使用率降低到 10%,这使我在该单个后端上的每秒请求数增加到刚刚超过一百万。并且 FWIW 您应该始终为任何高性能站点设置前端 - 后端,因为前端可以缓存数据而不会减慢更重要的动态请求后端。

未来似乎是编写自己的驱动程序来实现自己的网络堆栈,这样您就可以尽可能接近请求并在那里实现自己的协议。这可能不是大多数程序员想要听到的,因为它更复杂。就我而言,我将能够多使用 40% 的 CPU 并达到每秒超​​过 100 万个动态请求。 UDP 代理方法可以让您接近最佳性能而无需这样做,但是您将需要更多服务器 - 尽管如果您每秒执行这么多请求,您通常需要多个网卡和多个前端来处理带宽,因此拥有里面有几个轻量级 UDP 代理没什么大不了的。

希望其中的一些内容对您有用。

【讨论】:

想展示一个示例或工作项目?没有它,这与无关紧要的谈话一样有用。不是想贬低你,但这里需要一些具体的代码..【参考方案6】:

您有多少个 io_service 实例? Boost asio 有一个example,它为每个 CPU 创建一个 io_service 并以 RoundRobin 的方式使用它们。

您仍然可以创建四个线程并为每个 CPU 分配一个,但每个线程可以轮询自己的 io_service。

【讨论】:

以上是关于C++ 套接字服务器 - 无法使 CPU 饱和的主要内容,如果未能解决你的问题,请参考以下文章

具有多个连接的 C++ 服务器套接字

s3eSocket 果酱无法创建套接字

C++:无法让套接字、线程和结构工作

包含互斥锁 C++ 时的套接字问题

Node js服务器和c ++客户端套接字io连接,无法发出或读取数据

Windows 下 C++ 和 C# 中的套接字无法正常工作