编程

Apache 的性能瓶颈与 Nginx 的性能优势

1148 2023-03-25 16:12:36

每一名后端开发可能都知道 Nginx 比 Apache 性能强,但是为什么强,强在哪里,接下来我们动手实验解答这个问题。

Nginx 利用了新的 Linux kernel API

Nginx 利用了 Linux 内核引入的 epoll 事件驱动 API,大幅降低了海量 TCP 连接下的 CPU 负载,提升了单个系统的 TCP 响应容量,这是 Nginx 性能更好的本质原因:时代在进步。

每一篇技术文章都会说,Nginx 的 epoll 比 Apache 的 select 性能高,但为什么,却几乎没人解释。下面我来尝试解释。

epoll 简单解释

众所周知,epoll 是一种高性能事件驱动 IO 多路复用机制,那他和 select 这种原始 IO 多路复用机制比有什么优势呢?简单来说,就是:转守为攻。

epoll 化被动为主动,以前需要两次遍历才能实现的网络数据包和线程的匹配,现在通过事件驱动的方式主动献上指针,性能暴增。这就像云原生时代的 Prometheus 监控:化主动上传为被动查询,大幅提升单个采集节点的性能上限,成功解决了监控领域的高并发性能问题。

在 5K 个 TCP 连接的情况下,每收到一个数据包,Nginx 找到对应线程的速度比 Apache 高了两个数量级,即便是 event 模式下的 Apache,性能依然远低于 Nginx,因为 Nginx 就是专门为“反向代理”设计的,而 Apache 本质是个 web 应用容器,无法做到纯粹的事件驱动,性能自然无法和 Nginx 相比。

Apache 的原始并发模型

Apache 支持三种进程模型:prefork、worker 和 event,在此我们简单讨论一下这三种模式的优缺点。

  1. prefork 是进程模式,需要消耗更多的内存,每次接到一段新的数据,需要使用 select 模型,遍历 TCP连接数 x 进程数 这么多次才能找到匹配的进程,在数千个 TCP 连接下,光是寻找线程就需要消耗掉一个 CPU 核心,单机性能达到极限,无法利用更多的 CPU 资源
  2. worker 是线程模式,依旧使用 select 模型来遍历 TCP 请求和线程,性能上限和 prefork 一致,区别是内存消耗量有了一些降低,初始 TCP 承载能力稍好,请求数突然增加的场景下,开新线程的速度反而比 prefork 更慢,且基础延迟比 prefork 模式更高
  3. event 模式采用和 Nginx 一致的 epoll 模型承载,理论上表现和 Nginx 一致,但由于 Apache 大概率是和 mod_php(插件)模式的 PHP 一起部署,再加上 PHP 阻塞运行的特性,性能和上面两兄弟并无明显区别。因此即便是 event 模式下的 Apache,性能依然远低于 Nginx。

接下来我们使用 jmeter 测试一下 prefork、worker、event 三种模式的性能。

压力测试

测试环境

客户端:

  • i5-10400 6 核 12 线程
  • 32GB 内存
  • 千兆有线网络
  • 软件环境
    • macOS
    • Java 19.0.1

服务端:

  • 物理服务器 E5-2682V4 2.5GHz 16 核 32 线程 * 2 (阿里云 5 代 ECS 同款 CPU)256GB RAM
  • 虚拟机 64 vCPU (赋予了虚拟机所有母机的 CPU 资源)
  • 虚拟机内存 32GB
  • 软件环境
    • CentOS Stream release 9
    • kernel 5.14.0-200.el9.x86_64
    • Apache/2.4.53
    • Nginx/1.20.1
    • PHP 8.0.26
  • PHP 环境:
    • Laravel 9.19
    • 给默认路由增加 sleep 500ms 的代码,模拟数据库、Redis、RPC、cURL微服务等场景
    • 执行 php artisan optimize 后测试

测试代码

Route::get('/', function () {
	usleep(500000);
	return view('welcome');
});
  • Apache prefork 模式配置
<IfModule mpm_prefork_module>
    StartServers         100
    MinSpareServers      5
    MaxSpareServers      100
    MaxRequestWorkers    500
    MaxRequestsPerChild  100000
</IfModule>
  • php-fpm 配置
pm = static
pm.max_children = 500

实验设计

我们将测试三种配置下的性能表现差异:

  1. Apache 标准模式:prefork + mod_php 插件式运行 PHP
  2. Nginx + php-fpm 专用解释器
  3. Nginx 作为 HTTP 反向代理服务器后接 Apache prefork 模式 + mod_php 插件式运行 PHP

请求计划

  1. 客户端新线程开启后,每隔 5 秒发送一个请求
  2. jmeter 用 50 秒开 5000 个线程,持续压测 100 秒,最大请求 QPS 为 1000

为什么这么设计?

单独对比 Nginx 和 Apache 性能的文章很多,数据结果也大同小异,无非是 Nginx 的 QPS 更高,但是 “为什么是这样?” 却没人回答,我本次的实验设计就是要回答这个问题。

Apache 标准模式:prefork + mod_php

nginx + php-fpm

nginx 反向代理 Apache 标准模式

结果分析

我们可以很明显地看出,Apache + prefork 的问题在于它对数千个 TCP 连接的处理能力不足。

  1. Nginx + fpm 一共发出了 59146 个请求,成功了五万个
  2. Nginx + Apache 一共发出了 56464 个请求,成功了五万两千个,比 fpm 还多一些
  3. fpm 模式最大 QPS 为 800 但比较稳定,Nginx + Apache 最大 QPS 1000 但不够稳定
  4. 至于 Apache 标准模式,显然它的技术架构不足以处理 5000 个 TCP 连接下 1000 QPS 的状况,QPS 低且不稳定,错误率高达 43%

结论:

  1. Nginx 处理海量用户的海量 TCP 连接的能力超群
  2. 提升 Apache 性能只需要在前面加一个 Nginx 作为 HTTP 反向代理即可,因为此时 Apache 只需要处理好和 Nginx 之间的少量 TCP 连接,性能损耗较小
  3. php-fpm 和 mod_php 在执行 PHP 的时候没有任何性能差异

Nginx epoll 和 Apache prefork 模型相比,优势劣势如下:

优势

  1. Nginx 每个 worker 进程可以 handle 上千个 TCP 连接,消耗很少的内存和 CPU 资源,可以让单台服务器承接比 Apache 多两个数量级的用户量,相比于 Apache 单机 5K 的 TCP 连接数上限(对应的是 2000 个在线用户),是一个巨大的进步
  2. Nginx 对 TCP 的复用使它非常善于应对海量客户端的直接连接,根据我的实测,在 HTTP 高并发下 Nginx 的活跃 TCP 连接数可以做到 Apache 的五分之一,而且用户量越高,复用效果越好
  3. 在架构上,基于 FastCGI 网络协议进行架构扩展,也可以更轻易地利用多台物理服务器的并行计算能力,提升整个系统的性能上限

劣势

  1. 在低负载下,Nginx 事件驱动的特性使得每个请求的响应时长比 Apache prefork 模式略长一些(14ms vs 9ms)

我的真实经验

新冠时期的爱情机遇

疫情初期,我司的私域电商业务兴起于草莽之间,在 3 月中旬才上班的情况下,半个月的 GMV 超过了前面一年,4 月就完成了全年目标,电商系统性能压力陡增。

当时我们的电商系统是购买的一个 PHP 单体系统,天生不具有扩展性,外加业务模式是团购秒杀,可要了亲命了。客户端为微信小程序,服务端主要提供两种业务:开团瞬间的海量 HTTP API 请求,以及每一个页面都非常消耗资源的订单管理后台。

当时我面临的第一个问题是数据库顶不住,我找到请求数最高的接口:商品详情,为它增加了一层保持时长为一分钟的 Redis 缓存,开团瞬间数据库的压力降低了很多。

而且幸运的是当时阿里云刚刚将 PolarDB 商用几个月,我用它顶住了开团三分钟内涌入的大约 4000 名用户,但是当我把虚拟机升级到 16 核 32G 内存的时候出现了一个非常诡异的现象:

  1. CPU 和内存占用率分别只有 8% 和 6%
  2. 但大量的新用户就是无法建立 TCP 连接,首次连接的客户端表现为长时间的等待
  3. 如果运气好在等待一段时间后成功进去了,那访问便会一直如丝般顺滑,每个接口的返回时间都非常短

这是为什么呢?这是因为新用户无法和服务器建立 TCP 连接!

默认情况下,CentOS 7.9 的最大文件打开数(ulimit)为 1024,一切皆文件,每个 TCP 连接也是一个文件,所以也被锁定在了 1024 个。一般大家都会把这个数字设置为 65535,但是我观察到,这台虚拟机此时的 TCP 连接数只能跑到 5-6K 之间,远远达不到用户的需求,无论是采用 prefork、worker 还是 event 都是这样。而原因就是我们上面实测过的:此时 Apache 花费了一颗核心的全部时间片来进行数据包和线程的匹配,已经忙不过来了。

后来,我在这台机器上安装了一个 Nginx,反向代理全部的用户请求再发送给 Apache:请求一下子舒畅了,而且 Nginx 使用的最大活跃 TCP 连接数量也只有 1K,就完全满足了三分钟 4000 用户的需求。

还记得我们的目标吗?一百万 QPS

在 2022 年的主流云服务器硬件上,经过 OPCache 性能优化的 PHP 应用,只需要 2 vCore 便可以达到 5K 的单机 Apache TCP 上限,此时 QPS 在 200 左右,单纯提升核心数量无法让这个数字大幅增加。而通过使用 Nginx,我们可以将单系统的 QPS 上限从 200 提升到 1000。