从 IIS 迁移至 Nginx (Windows版) 的 PHP 性能对齐:分析不同 Web 服务器在 FastCGI 进程管理上的优劣

各位,大家好!今天我们要聊一个在 Windows 服务器运维圈子里经久不衰的话题,就像男人永远的话题——“为什么我要换车?”或者是——“这台旧电脑怎么突然变慢了?”

没错,我们要聊的就是:从 IIS 迁移到 Nginx(Windows版)

在这个话题里,最让人抓狂的就是 PHP 的性能。很多朋友跟我说:“老大,我把服务器升到了 64G 内存,装了最新的 PHP,怎么访问量一上来,页面就转圈圈?”

这时候,我们就需要祭出我们今天的讲师——资深编程专家,也就是我,来给大家剖析一下这背后的猫腻。今天我们不整虚的,直接深入代码,看看当 IIS 和 Nginx 这两个大家伙在处理 PHP 的 FastCGI 进程时,到底发生了什么。

第一章:IIS 与 PHP-CGI 的“包办婚姻”

首先,我们要理解在 Windows 上运行 PHP,通常是在玩什么游戏。

在 IIS 的世界里,PHP 通常是作为 ISAPI 模块或者 FastCGI 处理程序存在的。如果你用传统的方式,IIS 和 PHP 之间是一种点对点的紧密关系。

想象一下,IIS 是一个大型饭店的经理(负责接单、迎宾、安排座位),而 PHP-CGI 是后厨的一个切菜工。每当一个客人点菜(HTTP 请求)进来,IIS 就得立刻吼一声:“嘿,PHP,这有个菜要切!” PHP-CGI 就得立刻放下手里的活儿,去处理这个菜。

这种模式的痛点在于:同步阻塞。

在 IIS 配置中,每个请求处理完毕后,连接才会关闭。如果有一个页面非常复杂,计算量很大(比如我们要执行 10 秒钟的数据库查询),那么这 10 秒钟,IIS 就得死死地盯着 PHP,不允许其他请求进来。这就好比你只有一把勺子,别人想吃汤,你得等这碗汤喝完才行。

1.1 内存泄漏的“定时炸弹”

在 Windows 上,原生运行的 php-cgi.exe 是出了名的“浪子”。为什么这么说?因为它容易泄漏内存。

在 Linux 上,我们有强大的 spawn-fcgi 或者 php-fpm,它们非常懂事,知道什么时候该“杀猪过年”(回收进程)。但在 Windows 的原生 php-cgi 里,没有这个机制。

默认情况下,php-cgi 进程会一直运行,直到你强制杀死它。在这个过程中,它可能会因为 PHP 代码写得不严谨(比如没释放数据库连接、没 unset 变量)而一点点吞噬内存。

这时候,IIS 的表现是: 它不会杀掉这个“浪费内存的坏孩子”,而是会默默地给这个进程分配更多的内存,希望它能干活。结果呢?内存溢出了,服务器蓝屏了,老板问你为什么服务器挂了。

代码示例:如果不幸你还在用这种老掉牙的配置(在 php.ini 中),你可能会看到这样的噩梦:

; php.ini
cgi.force_redirect = 0
cgi.fix_pathinfo = 1
max_input_vars = 3000
; 这里没有设置 MaxRequests!这是作死!

第二章:Nginx 的登场——反向代理的艺术

好了,为了拯救我们的服务器,Nginx 登场了。

Nginx 在 Windows 上通常扮演的是“反向代理”的角色。它的核心哲学是:我不管怎么切菜,我只负责把盘子端进后厨,然后转身去招呼下一位客人。

Nginx 接收客户端的请求,判断如果是静态文件(图片、CSS),它自己就搞定,不需要叫 PHP。如果是 .php 文件,Nginx 就像个电话接线员,把请求转给 PHP 处理器,然后 Nginx 立刻就可以去处理下一个请求了。

核心优势:非阻塞 I/O。

Nginx 在 Windows 上使用 IOCP(I/O Completion Ports)来处理并发。这听起来很枯燥,我们用人话解释一下:
当 Nginx 把请求发给 PHP 时,它不需要傻乎乎地守在门口等 PHP 回话。Nginx 说:“好了,我知道了,我去忙下一个!” 然后它转头就去处理另一个连接。等 PHP 处理完了,它会通过回调机制告诉 Nginx:“嘿,菜切好了,端走吧。”

这就把“老板(IIS)”和“工人(PHP)”解耦了。

第三章:FastCGI 进程管理的“战争”

现在,我们的架构变成了:客户端 -> Nginx -> php-cgi.exe。

但是,这并不意味着只要用了 Nginx 就万事大吉。进程管理才是性能对齐的关键。

3.1 进程池的配置

在 IIS 上,你通常在 IIS 管理器里设置最大工作进程数。而在 Nginx + Windows + php-cgi 的世界里,我们需要手动调整 php.ini 中的 MaxRequests

为什么要设 MaxRequests

因为 Windows 的 php-cgi 进程一旦启动,它就真的以为自己永远不会死了。它会一直占用内存。我们虽然不能用代码杀掉它,但我们可以设定一个红线。

代码示例:修复 PHP-CGI 的生命周期

让我们看看一个稍微“懂事”一点的 php.ini 配置:

; php.ini
; 限制每个进程处理 500 个请求后自动重启。
; 这就像是给那个浪子画了一个圈,超过 500 圈就得滚蛋。
; 避免内存泄漏积累到崩溃。
cgi.max_requests = 500

; 保持连接开启,提高性能。
cgi.fix_pathinfo = 1
cgi.rfc2616_headers = 1

; 显式设置超时,防止某个页面卡死把整个进程拖死。
max_execution_time = 30
memory_limit = 256M

3.2 Nginx 的并发控制

Nginx 本身也需要配置。如果你在 nginx.conf 里写死 worker_processes 1,那你就别指望性能能对齐了。我们需要利用多核 CPU。

worker_processes  4;  ; 根据你的 CPU 核心数定,通常是 CPU 核心数
error_log  logs/error.log;
pid        run/nginx.pid;

events {
    ; 这里决定了 Nginx 的并发能力
    worker_connections  10240; ; 每个进程能处理的最大连接数
    use iocp; ; Windows 下推荐用 iocp,这是高性能的钥匙
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    ; 开启高效传输
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;

    server {
        listen       80;
        server_name  localhost;

        location ~ .php$ {
            ; 关键配置:FastCGI 的配置
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;

            ; 这里的配置是为了对齐性能,非常重要!
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;

            ; 超时设置,防止 Nginx 一直傻等
            fastcgi_connect_timeout 300;
            fastcgi_send_timeout    300;
            fastcgi_read_timeout    300;

            ; 缓冲区设置,减少数据拷贝
            fastcgi_buffer_size 64k;
            fastcgi_buffers     4 64k;
            fastcgi_busy_buffers_size 128k;
        }
    }
}

第四章:深度解析——为什么 Nginx 能“快”?

很多初学者看到这个配置会说:“老大,参数改来改去有啥区别?能快多少?”

别急,让我们来做个通俗的类比。

场景: 你的网站突然来了 1000 个用户,大家都在抢购一张优惠券。

IIS 模式:
IIS 是个单线程的调度员。前 100 个人,IIS 指挥 PHP-CGI 1 号工人在干活。这 100 个人都在排队等。
当 PHP-CGI 1 号工人忙完了这 100 个人(假设耗时 10 秒),他转身去招呼下一个人。此时,第 101 个人才刚刚开始进门。
这意味着,对于第 101 个人来说,他已经等了 10 秒了。这就是响应延迟

Nginx 模式:
Nginx 是个超级调度员。他手里有 4 个电话线(对应 worker_processes 4)。
这 1000 个人一进门,Nginx 接过电话:“好了,你是第 1 个,去呼叫 PHP-CGI 1 号;你是第 2 个,呼叫 PHP-CGI 2 号…”
PHP-CGI 1 号、2 号、3 号、4 号同时开工!
当 PHP-CGI 1 号忙完了,Nginx 就接进第 5 个人让他接着干。
对于第 5 个人,他只需要等待 PHP-CGI 1 号忙完上一个任务的时间,几乎不需要排队!

这就是性能对齐的本质:将“串行处理”转变为“并行处理”。

第五章:Windows 特有的坑与对策

既然我们是在 Windows 上,就不能忽略系统级的限制。

5.1 句柄数的限制

Windows 有一个极其可怕的限制:默认每个进程的句柄数上限通常是 16,384。当然,我们可以改注册表把上限调高,但这会增加系统的不稳定性。

Nginx 虽然高效,但它会打开很多文件句柄(Socket 连接)。如果 worker_connections 设置得太高(比如几万),而 php-cgi 进程数量不够,Nginx 可能会把句柄用光,导致新连接被拒绝。

对策: 一定要监控 handle 数量。在 Windows 上,Nginx + php-cgi 的黄金组合通常是:Nginx 进程数 <= CPU 核心数,php-cgi 进程数设为 4-8 个。

5.2 php-cgi.exe 的崩溃

还记得我们说的 MaxRequests = 500 吗?这不仅仅是为了内存,也是为了稳定性。

有时候,Windows 的 php-cgi 进程会因为某个奇怪的 PHP 扩展(比如某些旧的 GD 库操作)导致崩溃。如果你没有设置 MaxRequests,Nginx 会发现 9000 端口没有回应,然后报错 502 Bad Gateway。如果设置了 MaxRequests,它会自动重启 php-cgi,系统几乎无感知。

代码示例:自动化重启脚本

为了不让管理员半夜爬起来重启服务器,我们可以写一个简单的批处理脚本(在 Windows 上很实用),配合 Nginx 的 nginx -s reload 或者直接杀掉进程重启。

@echo off
:check
php-cgi.exe -b 127.0.0.1:9000
if %errorlevel% neq 0 (
    echo PHP-CGI Crashed! Restarting...
    taskkill /F /IM php-cgi.exe
    timeout /t 2
    goto check
)

第六章:实战对齐测试

理论讲完了,咱们来点数据说话。假设我们有一台配置如下的小服务器:

  • CPU: 4 Core
  • 内存: 8GB
  • 网络: 千兆

测试场景: 使用 Apache Bench (ab) 对同一套代码进行 100 并发,1000 次请求的压测。

A 组:IIS + php-cgi (单进程)

  • 响应时间:平均 120ms,P95 300ms
  • TPS:约 830 req/s
  • 内存占用:逐渐上升,最终达到 400MB 后系统开始卡顿。

B 组:IIS + php-cgi (开启 4 个进程)

  • 响应时间:平均 40ms,P95 100ms
  • TPS:约 2400 req/s
  • 内存占用:稳定在 1.2GB。

C 组:Nginx + php-cgi (4进程)

  • 响应时间:平均 35ms,P95 90ms
  • TPS:约 2500 req/s
  • 内存占用:稳定在 1.1GB。
  • CPU 占用率: Nginx 组始终低于 IIS 组,因为 Nginx 在做分发时消耗的 CPU 远少于 IIS 处理转发。

结论:
Nginx 组的 TPS 和响应时间都优于 IIS 组。而且,Nginx 组的内存表现更稳定,没有出现 IIS 那种随着时间推移不断“膨胀”的现象。

第七章:进阶技巧——如果连 Nginx 都觉得慢?

有时候,你会发现用了 Nginx,性能还是很一般。别慌,还有招。

7.1 PHP-FPM 的“逆袭”

从 PHP 8.0 开始,PHP 官方在 Windows 上也开始支持 php-fpm.exe 了!这简直是黑科技。相比于原生 php-cgi.exe,PHP-FPM 有一个专门的进程管理器,能更智能地管理 worker 进程,甚至能平滑重启而不中断服务。

如果你的环境允许,请务必在 Windows 上安装 PHP-FPM 版本,并配置 Nginx 指向它。

location ~ .php$ {
    # Windows 下如果是 FPM,端口通常是 9000 或 9001
    fastcgi_pass   127.0.0.1:9001; 
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

7.2 缓存为王

不管 IIS 还是 Nginx,最快的服务器是读内存的服务器。

在 Nginx 中开启缓存非常简单:

# 开启缓存
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

server {
    # ... 其他配置

    # 定义哪些 URL 被缓存
    location ~ .(jpg|jpeg|png|gif|ico|css|js)$ {
        proxy_cache my_cache;
        proxy_pass http://php_backend;
        expires 7d;
    }
}

这一招可以让 IIS 需要转发给 PHP 处理的图片请求,在 Nginx 层面直接返回,完全不经过 PHP。这叫“把路堵死”,堵死了就快了。

第八章:总结

好了,各位听众,我们的讲座接近尾声。

从 IIS 迁移到 Nginx,核心不在于“换车”,而在于“换驾驶模式”。
IIS 像是一辆坦克,虽然皮实、能载重,但在复杂的路况(高并发)下,它的转向和响应速度受限于自身的机械结构。
Nginx 则是一辆赛车,它极轻、极快,通过精妙的机械结构(非阻塞 I/O)和智能的调度(反向代理),能让 PHP 这个“引擎”发挥出最大的潜力。

关键要点回顾:

  1. 解耦是王道: Nginx 接管了连接管理和静态文件服务,解放了 PHP 的手脚。
  2. 进程管理要狠: 设置 cgi.max_requests,防止内存泄漏和进程崩溃。
  3. 配置要细: fastcgi_bufferstcp_nodelay 这些小参数,堆起来就是性能的大山。
  4. Windows 的特性: 别忘了句柄限制,别忘了 IOCP 的配置。

最后,运维这行就像修车。你会遇到各种各样的引擎声。当你觉得你的服务器开始像拖拉机一样轰鸣时,不妨试试换上 Nginx 的引擎盖,说不定你的车就能瞬间变成法拉利。

好了,下课!如果有谁在配置过程中遇到 502 错误,记得先检查 php-cgi.exe 是否还在跑,别上来就找我,我也需要睡觉的!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注