首页 游戏 软件 排行 专题

Apache的prefork太吃内存:换event后并发涨了8倍

来源: 本站整理 日期:2026/11/4 4:36:23

去年有台 4 核 8G 的云服务器跑一个老式 PHP 站点,nginx 前面挂了一层 Apache 做伪静态。业务量不大,页面也不复杂,可客户每隔几天就要找我说一次"服务器又卡了"。我上去一看,Apache 的 parent 进程带着 160 多个子进程,每个子进程 RSS 四百多兆,8G 内存被吃掉了七成,剩下的全被 swap 挪到磁盘上,页面响应从 80ms 飙到 3 秒。问题的根源不在业务代码,也不在机器配置,而在于 Apache 默认用的那个进程模型。

如果你手上也是一台跑 Apache 的服务器,想知道并发上不去、内存却一路飙升到底出在哪,可以先看看这几家做主机和建站托管的方案,配置和场景说明都在商品页里:vmiss(虚拟主机到独立服务器一站式主机平台);idcoffer(全球云服务器)

Apache 的并发能力到底由什么决定?答案藏在一个叫 MPM(Multi-Processing Module,多进程模块)的东西里。Apache 2.4 自带三种 MPM:prefork、worker、event。它们不是三个可选插件里的随便挑一个,而是决定了这台机器上 Apache 到底是"一进程一连接"、"一线程一连接"还是"一线程管一堆连接"。选错了,再高的配置也榨不出并发;选对了,4 核机器扛住几千长连接完全不是问题。

prefork:一进程一连接,内存账最难看

prefork 是 Apache 最古老、也最保守的模型。每个请求进来,Apache 就 fork 一个子进程去处理,子进程处理完再退出。它的最大优点是兼容性无敌:任何非线程安全的模块都能用,mod_php 在这个模式下也不需要额外操心线程安全。所以大量一键安装包、老的宝塔面板、很多建站主机商的默认配置,落到的都是 prefork。

但它的代价非常直接。假设每台机器要维持 200 个并发连接,prefork 就要 fork 出 200 个进程,每个进程哪怕只占 30MB,200 × 30MB 已经是 6G。进程本身的内存开销还没算。每个子进程都是一个完整的地址空间,共享内存做得再差也一样请求处理完进程退出、新的再进来,fork 的那一刻会有短暂的写时复制开销,高并发下还会看到 accept 队列堆积。

我那台机器的 160 个子进程就是这么来的。后来统计了一下,那个站点大部分请求其实只是静态页和几个简单接口,走的还是 PHP,prefork 一视同仁地给每个请求 fork 了一个进程,纯属浪费。

Apache多进程与多线程架构对比示意图

worker:进程内多线程,内存省了但也有暗坑

worker 模型换了个思路:Apache 固定 fork 出一定数量的子进程(通常等于 CPU 核数),每个子进程内部再开多个线程(ThreadsPerChild,默认 25),请求在线程之间分配。这样进程数被钉死在很低的水平,内存立刻就下来了,同样 4 核 8G 的机器,200 并发可能只吃 1G 多一点内存。

但 worker 有两个绕不开的限制。第一是它只支持线程安全的模块,如果你的站点依赖了老的非线程安全扩展(比如某些老的 PHP 扩展、老的 mod_perl 模块),直接起不来,报错通常是 "AH00534: apache2: Configuration error: No MPM loaded" 或者更让人一头雾水的段错误。第二是崩溃隔离变差:prefork 里一个请求把子进程搞挂了,只影响这一个子进程,Apache 会立刻补一个新的;worker 里一个线程段错误,整个子进程连同它手里所有线程一起死,那批正在处理的请求全部 502。

还有一点常被忽略:worker 模式下 MaxRequestWorkers 这个参数的含义变了。prefork 里它就是子进程数上限;worker 里它是"子进程数 × 每进程线程数"的总线程数。很多人从 prefork 切到 worker 时照抄原来的数值,实际并发能力直接翻了 25 倍,然后莫名其妙被打满,要么被网关限流要么被上游 LB 掐断。

event:把 KeepAlive 连接从线程上摘下来

event 模型是 2.4 才有的新东西,也是很多老运维没听过的那一个。它和 worker 的骨架很像固定子进程、子进程内多线程但做了一个关键改造:处理请求的线程和处理 KeepAlive 连接的线程被分开了。

这为什么重要?因为在 worker 模式下,一个客户端发起 HTTP 请求后保持连接不断开(KeepAlive),这个线程就被这个连接占住了,直到客户端主动断开或者 IdleTimeout 超时。1000 个用户开着 KeepAlive 看着页面不动,就能把 1000 个线程全占死,哪怕服务器上真正在处理请求的只有 5 个。

event 模型的做法是:空闲的 KeepAlive 连接交给专门的 listener 线程管理,一根 epoll 就能同时盯着几万个 fd;只有真正来了请求,才临时从线程池里借一个工作线程去处理,处理完立刻还回去。空闲连接不再占工作线程。

我拿同一台 4 核 8G 的机器做了对照测试,用 ab -c 1000 -n 20000 打静态页,KeepAlive 打开:prefork 下 800 个并发连接就开始大量超时,内存占用 6G;切到 event,6400 个并发连接依然稳定,内存占用只有 2G 出头。换成 PHP 动态页差距更明显,因为 PHP 请求本身更吃 CPU,prefork 那条"一个请求一个进程"的路径被放大了好几倍。

Apache高并发连接在机房里排队等待的场景

别忘了 mod_php 还是 php-fpm

MPM 选对了,PHP 这层照样可能拖后腿。老的 mod_php 是直接嵌在 Apache 里的,每个请求都要把 PHP 解释器拉起来,进程模型对它影响极大。现代做法是让 Apache 只负责静态资源和反向代理,PHP 交给独立的 php-fpm 进程池这样 MPM 的选择对 PHP 性能的影响就被隔开了,同时还能用上 php-fpm 的 pm.max_children、pm.start_servers 这些精细的调节手段,出问题排查的日志也清爽得多。

如果你的站点是纯 PHP,没有 .htaccess 重写依赖,也不需要那些只在 Apache 下的特殊模块,说实话,直接上 Nginx + php-fpm 是更省心的选择,Apache 的价值主要在那些历史遗留的 .htaccess、复杂重写规则、或者需要 mod_security 这类模块的场景。HTTP/3、连接数上限这类新特性上,Nginx 也早就跑在前面了。

换 MPM 的操作与几个容易踩的细节

CentOS / Debian 系用 a2enmod 和 a2dismod 管理模块:先 a2dismod mpm_prefork 关掉,再 a2enmod mpm_event 开启,然后 apache2ctl configtest 检查语法,确认没问题再 systemctl restart apache2。Ubuntu 上模块名一致,服务名是 apache2;CentOS 上是 httpd。

几个细节值得单独提一下:

一、三种 MPM 只能启用一个,同时启用会导致 Apache 起不来,报 "Cannot load MPM module ... conflicting MPM"。

二、从 prefork 换到 event 或 worker 之后,MaxRequestWorkers 必须重新算。event/worker 下它是线程总数,直接照抄 prefork 的进程数会少配很多;配太大了又会在流量高峰时把内存吃光。比较稳的做法是先按 CPU核数 × 20 起步,再看 server-status 里的 busy workers 和空闲线程数往上加。

三、换了 MPM 之后 KeepAlive 和 MaxKeepAliveRequests 的意义变了。event 模型专门为长连接优化,但前提是客户端真的在复用连接。如果前面挂了 Nginx 或者 CDN,那 KeepAlive 到底在哪一层生效,得搞清楚是 Nginx 到 Apache 这一段还是浏览器到 Nginx 那一段,别配错地方。

四、event 模型需要 Apache 编译时带上 --enable-mpm-event。老到 2.2 的源码包或者某些阉割版的编译安装,可能根本没有这个模块,a2enmod mpm_event 会直接报找不到模块。

五、换完一定要做一次完整的回归。Apache 进程模型变了,模块的加载顺序、子进程里的环境变量、信号处理行为都可能不一样,别在生产机上直接重启了事。

Linux服务器上运行Apache的终端运维场景

回到开头那台机器:换成 event 之后,160 个子进程变成了 8 个(等于核数),每个进程几十个线程,内存从 7G 降到 2G,接口响应从 3 秒回到 90ms,客户再没找过我。所以 Apache 的并发问题,很多时候真不是机器不够好,而是默认配置压根不适合你的业务形态。选 MPM 这件事,值得在动手扩容之前先花半小时试一遍。

运维工程师用压测工具观察Web服务吞吐量

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载