核心结论:Windows 下 Apache(mpm_winnt)出现两个进程是「1 个父控制进程 + 1 个子工作进程」的正常设计,并非故障;真正的问题是子工作进程内存持续累积到约 1.7G。本机为 XAMPP 默认 mod_php 嵌入式模式,PHP 解释器跑在 Apache 进程内部,请求结束后内存不归还操作系统,叠加子进程永不回收(MaxConnectionsPerChild = 0),导致内存只涨不跌。
一、问题现象
- 任务管理器中同时存在两个 Apache(
httpd.exe)进程。 - 其中一个进程占用内存约 7MB,另一个进程占用内存约 1.7G,整机内存占用偏高。
- 环境为 Windows + XAMPP + Apache 2.4,PHP 以模块方式嵌入(mod_php)。
二、为什么会有两个 Apache 进程
Windows 版 Apache 使用 mpm_winnt 多处理模块,其进程模型固定为「父进程 + 子进程」两层,这是设计如此,不是重复启动或异常:
| 进程 | 职责 | 内存表现 |
|---|---|---|
| 父控制进程 | 监听端口、管理子进程生命周期、在子进程异常时重启它,本身不处理网页请求。 | 占用很小,约 7MB。 |
| 子工作进程 | 所有网页请求、PHP 脚本、加载的模块都在该进程内运行。 | 内存随请求持续累积,1.7G 的占用来自这个进程。 |
因此,两个进程本身无需处理;优化目标是控制子进程的内存累积与回收。
三、子进程内存持续上涨的原因
3.1 mod_php 嵌入式:内存不归还操作系统(最高发)
当前配置中 PHP 以模块方式直接加载进 Apache 子进程。PHP 解释器与 Apache 共享同一进程地址空间,脚本执行结束后,申请的内存通常只在进程内部复用,不会整体归还给操作系统。一旦代码存在静态变量、缓存未释放、循环读取大文件、数据库连接未释放等情况,进程 RSS 就会只涨不跌。
需要注意:php.ini 中的 memory_limit 只限制单次脚本可使用的最大内存,无法阻止 Apache 进程整体内存持续上涨。
3.2 子进程永不自动回收
MPM 配置不在主 httpd.conf,而由 Include conf/extra/httpd-mpm.conf 引入。mpm_winnt 默认 MaxConnectionsPerChild 0,表示子进程处理任意数量请求后都不重启,内存长期累积、无法被强制释放。
3.3 常驻模块与第三方插件
- Apache 加载的各模块常驻在子进程内,基础内存随模块数量增加。
- 配置末尾加载了云锁过滤模块
agent_apache_filter_2.4.dll,该模块同样注入子进程,会跟随一起占用内存,必要时可临时注释观察内存上涨速度。 - 开启了 mod_http2(
Protocols h2 http/1.1),在 Windows mpm_winnt 下部分场景会放大内存占用,访问量不大时可先关闭测试。
3.4 大请求、爬虫与导出类脚本
报表导出、一次性读取大文件、大数据量查询、高频爬虫访问,会一次性申请大量内存;即使脚本跑完,这部分内存也很难在进程内被回收利用。
3.5 OPcache 缓存占用
PHP OPcache 将大量脚本缓存进内存,这部分缓存同样计入 Apache 子进程内存;缓存配置过大时会进一步抬高基础占用。
四、排查步骤(按顺序执行)
- 确认 PHP 运行模式:查看
httpd.conf与httpd-xampp.conf是否存在LoadModule php*这类加载 PHP 模块的行。存在即为 mod_php 嵌入式模式;当前proxy_fcgi_module处于注释状态,说明尚未使用 PHP-FPM。 - 重启 Apache 观察基线:重启服务后查看子进程内存是否回落到几十 MB。重启后缓慢上涨,说明是内存累积或泄漏;访问某个页面后立刻暴涨,则定位该页面的 PHP 代码。
- 查看活跃请求:配置已加载
status_module,可开启 server-status 页面,观察长时间挂住的请求,定位有问题的 URL 或脚本。
五、处理方案
方案 A:调整 MPM 自动回收(治标,最快见效)
打开 C:/xampp/apache/conf/extra/httpd-mpm.conf,找到 mpm_winnt 段:
<IfModule mpm_winnt_module>
ThreadsPerChild 150
MaxConnectionsPerChild 0
</IfModule>
修改前(默认)
改为:
<IfModule mpm_winnt_module>
ThreadsPerChild 100
MaxConnectionsPerChild 3000
</IfModule>
修改后(建议)
- MaxConnectionsPerChild 3000:子进程处理 3000 次请求后,Apache 优雅地新建子进程、关闭旧进程,强制释放全部内存。
- ThreadsPerChild 100:并发线程数,小网站 100 足够;线程过大反而会加快内存上涨。
数值参考:访问量不大用 2000~3000,访问量大可调到 5000;不要小于 1000,避免频繁重启造成卡顿。保存后重启 Apache。该方案只是自动回收内存,不能修复 PHP 代码中的内存泄漏。
方案 B:迁移到 PHP-FPM(治本,强烈推荐)
将 PHP 从 Apache 进程中剥离,由独立的 PHP-FPM 进程池运行,Apache 只负责静态文件、重写、SSL 与反向代理。这样 PHP 内存泄漏被隔离在 FPM 进程内,不会把 Apache 拖垮;FPM 自身也有进程回收机制。
- 在
httpd.conf中取消注释代理模块:LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
- 在
httpd-xampp.conf中注释掉加载 mod_php 的行(LoadModule php8_module等)。 - 在虚拟主机配置中,将 PHP 请求通过 proxy_fcgi 转发给 PHP-FPM 监听端口。
- 使用 NTS 版 PHP 启动 php-fpm,并配置独立的进程池与回收参数。
方案 C:代码层面优化
- 检查全局静态数组、无限追加的缓存、一次性读入内存的大文件。
- 导出与大数据查询改为分页、流式输出。
- 控制 OPcache 内存大小,不要配置过大。
方案 D:精简 Apache 模块
注释掉所有不使用的 LoadModule,减少常驻内存。当前配置中,不使用 WebDAV 可关闭 dav_lock_module,不使用服务器信息页可关闭 info_module。
六、结合当前配置的额外优化点
| 配置项 | 现状 | 建议 |
|---|---|---|
| 云锁过滤模块 | LoadModule agent_apache_filter "...agent_apache_filter_2.4.dll",注入子进程常驻。 |
临时注释该行重启 Apache,对比内存上涨速度,判断是否由该插件加剧占用。 |
| mod_http2 | 已加载并启用 Protocols h2 http/1.1。 |
访问量不大时可注释 http2_module 与 Protocols 行测试内存表现。 |
| 日志级别 | LogLevel warn。 |
保持 warn,不要改为 debug,避免日志与额外开销暴涨。 |
七、建议执行顺序
- 先修改
httpd-mpm.conf的MaxConnectionsPerChild 3000,重启 Apache,观察几天内存走势。 - 观察内存是否在接近阈值后自动回落;若每次都涨到上限才回落,说明是 PHP 累积内存,代码存在泄漏。
- 长期方案:迁移 PHP-FPM,从架构上隔离 PHP 内存,避免 Apache 进程被 PHP 内存拖死。
若放任子进程内存持续累积,最终可能占满物理内存,导致系统卡顿、网站超时。