prefork 与 PPC 模式区别、Java 中间件是否使用、Apache 256 并发配置详解
这篇笔记专门拆三个问题:
- prefork 和 PPC 到底啥区别?为什么说它俩都"不行"?
- 现在 Java 用的中间件里,有哪些是 prefork 模式?(剧透:几乎没有,这里有个常见误解)
- "默认最大 256 个并发连接"指的是哪个中间件、哪个配置?能调大吗?
关联文档:[[fork之后为什么需要IPC-从文件描述符继承说起]]、[[进程间通信方式详解-IPC]]——prefork 的痛点本质上就是 fork 之后父子进程通信的问题。
一、先把两个概念摆正:PPC vs prefork
这俩都是多进程并发模型,思路一脉相承,区别只在"什么时候 fork"。
1. PPC = Process Per Connection(每连接一进程)
每来一个连接,主进程现场fork一个子进程专门伺候这个连接,连接断开子进程就退出。
客户端A 连进来 ──> 主进程 fork ──> 子进程A 处理 ──> 处理完销毁 客户端B 连进来 ──> 主进程 fork ──> 子进程B 处理 ──> 处理完销毁 (每个连接都重复一次 fork + 销毁的完整流程)致命缺点:
- fork 太贵:Linux 下
fork()虽然有写时复制(COW)优化,但每次连接都要走一遍"复制进程地址空间描述、分配 PID、建页表",高并发下扛不住。 - 父子进程通信复杂:子进程处理结果要回传父进程,得靠 IPC(管道、共享内存、信号量……),见 [[进程间通信方式详解-IPC]]。
- 并发数有限:每个进程占独立内存,进程数上不去。
2. prefork = 预先 fork(进程池)
主进程启动时一次性预先 fork 出一池子进程(N 个 worker),它们就位后阻塞等待。来连接了,从池子里挑一个空闲进程去处理,处理完不销毁,回池子继续等下一个。
启动时:主进程 ──fork×N──> [worker1, worker2, ... workerN] 全部就位、阻塞等待 客户端A 连进来 ──> worker1 接走处理 ──> 处理完回池子继续等 客户端B 连进来 ──> worker2 接走处理 ──> 处理完回池子继续等 (不再每次 fork,复用进程)比 PPC 好在哪:省掉了"每连接一次 fork"的开销,进程复用。
为啥还是不行(和 PPC 一样的痼疾):
- 仍然是多进程模型,每个 worker 独立内存空间,内存占用大(一个进程几十 MB 是常态,几百个就是几 GB)。
- 父子/兄弟进程之间通信仍然要靠 IPC,复杂度没降。
- 并发数 = 进程数,受机器内存和 PID 上限卡死,扛不住几万连接的现代场景。
一句话:prefork 是 PPC 的"进程池优化版",但本质还是多进程,所以原文说它"和 PPC 一样,存在父子进程通信复杂、支持的并发连接数量有限的问题"。
3. 对比速查表
| 维度 | PPC(每连接一进程) | prefork(预先 fork) |
|---|---|---|
| fork 时机 | 每个连接来时现场 fork | 启动时一次性 fork 一池子 |
| 进程是否复用 | 否,用完即销毁 | 是,处理完回池子等下一个 |
| fork 开销 | 每连接一次,高 | 摊到启动期,连接期几乎无 |
| 内存占用 | 高(峰值与并发数成正比) | 高(固定 N 个进程常驻) |
| 进程间通信 | 需要 IPC | 仍需要 IPC |
| 并发上限 | 受进程数限制 | 受进程数(池大小)限制 |
| 典型代表 | 早期 Web 服务器 | Apache MPM prefork |
二、现在 Java 的中间件,有哪些是 prefork 模式?
直接结论:几乎没有 Java 中间件采用 prefork(多进程)模型。
这一点很容易被原文带偏——原文讲 Apache prefork,而 Apache HTTP Server 是C 语言写的,不是 Java 中间件。很多人把"Apache"和"Java 中间件"混为一谈,但它们是两回事。下面拆开说。
1. Java 中间件几乎都用"线程模型",不是"多进程模型"
JVM 启动代价极高,而且fork()在 JVM 里有天然的坑:fork 出的子进程会复制父进程整个堆和线程状态,但其他线程在 fork 瞬间的状态是未定义的,极易死锁/损坏。所以 Java 生态压根不走多进程并发,而是:
| 中间件 | 并发模型 | 说明 |
|---|---|---|
| Tomcat | 线程池(thread-per-request)+ NIO | 一个请求一个线程,线程从池里取,NIO Connector 用 Selector 多路复用降线程数。不是 prefork |
| Jetty | 线程池 + NIO/异步 | 同上,线程模型 |
| Undertow | XNIO + 线程池 | 基于 NIO 的线程模型 |
| Netty | Reactor 主从线程组 + NIO | 事件驱动线程模型,单/少量线程扛大量连接 |
这些都是线程级并发,进程只有一个 JVM。和 prefork 的"多进程 + IPC"是两条技术路线。
2. 那 Tomcat 的 APR/native 模式算 prefork 吗?
不算。Tomcat 的 APR/native Connector 底层确实调用了 Apache Portable Runtime(和 Apache httpd 同源的 C 库),但它用的是APR 的线程 + poll 机制,跑的还是 JVM 进程内的线程,不是 fork 出一堆进程。别被"APR"这个名字和 Apache 的关系误导。
3. 真正"prefork-like"的典型代表是谁?
不是 Java,是PHP-FPM:
PHP-FPM 启动时预 fork 一池子 worker 进程(pm = static/dynamic) 每个 worker 是独立进程,处理完一个请求回池子等下一个这和 prefork 的思路几乎一模一样。所以 prefork 这种模型的现代继承者主要在 PHP-FPM,而不是 Java 中间件。原因是 PHP 解释器本身没有线程安全的可靠实现、且请求间要彻底隔离,只能靠多进程;而 Java 天生多线程,没必要走这条路。
4. 那 nginx 呢?
nginx 启动时也会fork出多个 worker 进程——但这不是 prefork。nginx 的每个 worker 是事件驱动(epoll)多路复用,一个 worker 进程能扛成千上万的连接,是"少量进程 + 每进程大量连接",和 prefork 的"一个连接一个进程"完全相反。别看到"预先 fork 多个进程"就归到 prefork。
三、“默认最大 256 个并发连接”——哪个中间件、哪个配置、能调大吗?
1. 指的是谁
指的是Apache HTTP Server(httpd)的 MPM prefork 模式。原文那句"默认情况下最大支持 256 个并发连接"说的就是它。
⚠️ 再强调一次:Apache httpd 是C 写的 Web 服务器,常作为 Java 应用(Tomcat/Spring Boot)前面的反向代理或静态资源服务器,但它本身不是 Java 中间件。
2. 哪个配置项
配置项名字随 Apache 版本变过:
| Apache 版本 | 配置项 | 默认值 | 作用 |
|---|---|---|---|
| 2.2 及更早 | MaxClients | 256 | 同时能存在的 prefork worker 进程上限 = 并发连接上限 |
| 2.4 起 | MaxRequestWorkers | 256 | 同上,只是改了名(语义更准:处理请求的工作者上限) |
也就是说"256"这个默认数,2.2 叫
MaxClients,2.4 叫MaxRequestWorkers,本质都是"prefork 模式下最多多少个 worker 进程同时干活"。
3. 配置文件在哪
随发行版不同,位置不一样,常见三处:
# Debian / Ubuntu(Apache 2.4)/etc/apache2/mods-available/mpm_prefork.conf# RHEL / CentOS / Rocky/etc/httpd/conf.modules.d/00-mpm.conf# 加载 mpm_prefork 模块/etc/httpd/conf.d/...# 或在主配置里覆写# 源码编译安装 / 通用主配置/usr/local/apache2/conf/httpd.conf /usr/local/apache2/conf/extra/httpd-mpm.conf一个典型的mpm_prefork.conf长这样:
<IfModule mpm_prefork_module> StartServers 5 # 启动时预 fork 的进程数 MinSpareServers 5 # 最少空闲进程数 MaxSpareServers 10 # 最多空闲进程数 MaxRequestWorkers 256 # ← 就是这个:并发上限(2.4) MaxConnectionsPerChild 0 # 每个进程处理多少请求后回收,0=不回收 </IfModule> # Apache 2.2 里把 MaxRequestWorkers 换成 MaxClients,并多一个 ServerLimit4. 能调大吗?能,但有坑
能调大,直接改MaxRequestWorkers(2.2 的MaxClients)即可,但有三道坎必须一起考虑:
坎 1:ServerLimit必须跟着调(Apache 2.4 的硬上限)
MaxRequestWorkers不能超过ServerLimit。ServerLimit默认256,所以你光改MaxRequestWorkers=1000是没用的——必须先把ServerLimit也抬上去:
<IfModule mpm_prefork_module> ServerLimit 1000 # 必须先抬这个 MaxRequestWorkers 1000 # 才能生效 StartServers 10 MinSpareServers 10 MaxSpareServers 50 MaxConnectionsPerChild 1000 </IfModule>Apache 2.4 的
ServerLimit理论上限是200000(2.2 同理),但实际没人设这么大,见坎 2。
坎 2:内存是真正的天花板(最容易翻车)
prefork 每个 worker 是独立进程,每个进程都要把 Apache 加载的模块、PHP 等都占一份内存。粗略估算:
单进程内存 ≈ 10 MB(纯静态)~ 50 MB+(加载 PHP/大量模块) 设每进程 30 MB 256 进程 → ~7.5 GB 1000 进程 → ~30 GB 5000 进程 → ~150 GB ← 内存直接爆所以调多大不是拍脑袋,要先算:
可用内存 / 单进程内存 = 你能设的上限否则进程数一上去,系统开始换页(swap)甚至 OOM Killer,性能断崖式下跌,比不调还惨。
坎 3:prefork 本就不该扛高并发
即便内存够,prefork 一个连接一个进程的设计,到几千并发就吃力。真有高并发需求,正确做法是换模型,而不是把 prefork 调到天上去:
- 同样是 Apache,切到MPM event或MPM worker:worker/event 是"多进程 + 每进程多线程",event 还支持 keepalive 连接异步化,单台轻松上万并发。
- 或者直接上nginx(事件驱动,worker 进程数 = CPU 核数,一个 worker 扛几万连接)。
结论:256 能调大,但 prefork 这个模型的天花板就在"几千"量级;调到几千还行,再往上请换模型(MPM event / worker / nginx),别硬刚。
5. 怎么验证改没改生效
# 看当前加载的是哪个 MPMapache2ctl-V# Debian/Ubuntu,看 Server MPM 字段httpd-V# RHEL/CentOS# 看当前实际生效的 prefork 配置apache2ctl-M|grepmpm# 确认加载了 mpm_preforkapache2ctl-t-DDUMP_MODULES# 列模块# 改完先测语法、再 graceful 重载apache2ctl configtest systemctl reload apache2# 或 apachectl graceful四、一图收尾:几个概念别再混
并发模型谱系(简化) ├─ 多进程 │ ├─ PPC 每连接一进程(现场 fork) ← 老 │ ├─ prefork 预 fork 进程池 ← Apache MPM prefork / PHP-FPM │ └─ nginx 多进程 + 每进程 epoll 多路复用 ← 不是 prefork! └─ 多线程 / 事件驱动 ├─ Tomcat/Jetty/Undertow 线程池 + NIO ← Java 中间件基本在这条线 ├─ Netty Reactor + NIO └─ Apache MPM event/worker 多进程+多线程 ← Apache 的高并发版五、一句话回答最初的三个问题
- 区别:prefork 是 PPC 的进程池优化版——把"每连接现场 fork"改成"启动时预 fork 一池子复用",省掉每次 fork 的开销;但本质都是多进程,内存占用大、靠 IPC 通信、并发受进程数限制的痼疾没变。
- Java 中间件:几乎没有用 prefork 的。Tomcat/Jetty/Undertow/Netty 全是线程 + NIO模型;APR Connector 也是线程不是进程。prefork 的现代典型代表是PHP-FPM,不在 Java 侧。
- 256 并发:指Apache HTTP Server 的 prefork MPM,配置项是 2.4 的
MaxRequestWorkers(2.2 的MaxClients),默认 256。能调大,但必须同时抬ServerLimit,并且要按"可用内存 / 单进程内存"算上限;几千以内可玩,再高就该换 MPM event/worker 或 nginx。