
最近在技术社区看到一个很有意思的讨论一个用纯 PHP 编写的 HTTP 服务器在静态文件服务和 PHP 请求处理上性能表现竟然超过了 Nginx。这听起来有点反直觉毕竟 Nginx 是业界公认的高性能 Web 服务器和反向代理的标杆。作为一个长期和 PHP、Nginx 打交道的开发者我的第一反应是“这怎么可能”但好奇心驱使我深入探究了一番。本文将围绕这个“纯 PHP 服务器”的核心思路、实现原理、性能对比背后的原因以及它给我们带来的工程启示进行深度拆解。无论你是 PHP 开发者、后端架构师还是对服务器性能优化感兴趣的工程师这篇文章都将带你理解一个“非主流”方案背后的技术逻辑并思考在特定场景下我们是否可以有更优的选择。1. 背景与核心概念为什么会有纯 PHP 服务器在深入性能对比之前我们首先要理解“纯 PHP 服务器”是什么以及它试图解决什么问题。1.1 传统 PHP 运行模式CGI/FPM 与 Web 服务器的分离架构经典的 PHP 应用部署架构是分离的Web 服务器如 Nginx 或 Apache负责监听端口、处理 HTTP 协议、路由请求、提供静态文件服务。PHP 处理器如 PHP-FPMFastCGI Process Manager作为一个独立的进程管理器专门负责执行 PHP 脚本。当用户请求一个 PHP 页面如index.php时流程如下Nginx 接收到请求。根据配置Nginx 将请求通过 FastCGI 协议转发给 PHP-FPM 进程池中的一个空闲 worker 进程。PHP-FPM worker 进程加载 PHP 解释器执行对应的.php文件。PHP-FPM 将执行结果HTML/JSON等通过 FastCGI 协议返回给 Nginx。Nginx 将结果封装成 HTTP 响应返回给客户端。这种架构的优势在于职责分离。Nginx 擅长高并发 I/O 和静态文件处理PHP-FPM 专注于执行 PHP 代码。两者通过进程间通信IPC协作。但这种架构的瓶颈也显而易见进程间通信开销每次请求都需要在 Nginx 和 PHP-FPM 之间进行数据序列化、传输、反序列化。对于非常简单的请求比如返回一个json_encode([status ok])这个开销可能占比很高。两次上下文切换请求需要经过 Nginx 和 PHP-FPM 两套完整的处理流程。静态文件仍需经过 Nginx即使请求最终由 PHP 处理Nginx 也需要先接收和解析请求头。1.2 纯 PHP 服务器的核心理念All in One“纯 PHP 服务器”的想法非常直接为什么不直接用 PHP 程序监听 HTTP 端口自己处理所有事情呢这样一来一个进程同时处理 HTTP 协议解析、路由、静态文件查找、PHP 脚本执行。消除了 Nginx 与 PHP-FPM 之间的 IPC 开销。对于纯静态文件请求可以直接在 PHP 进程中读取文件并发送省去了转发环节。听起来很美但 PHP 本身并不是为这种常驻内存、高并发的服务器模式设计的。传统的 PHP 是“共享无状态”的每个请求结束后所有资源都被释放。要实现一个高性能的服务器需要解决几个关键问题常驻内存PHP 脚本需要一直运行而不是每次请求执行一次。事件驱动与异步 I/O为了支持高并发不能为每个连接创建一个线程或进程必须使用非阻塞 I/O 和事件循环。PHP 代码的热加载修改了项目代码后服务器需要能感知并重新加载而不需要重启。近年来随着 PHP 本身的发展尤其是ext-event、ext-ev等扩展对 libevent/libev 的封装以及 Swoole、ReactPHP、Amp 等异步框架的成熟在 PHP 用户态实现一个高性能的 HTTP 服务器已经成为可能。文章开头提到的那个“Show HN”项目很可能就是基于这些技术构建的。2. 环境准备与概念验证在分析其如何“超越”Nginx之前我们先搭建一个最简单的纯 PHP HTTP 服务器理解其基本形态。这有助于我们后续理解性能对比的上下文。环境说明操作系统Linux (Ubuntu 20.04 或 CentOS 7)PHP 版本PHP 8.0 (需包含pcntl,posix,sockets扩展推荐安装ext-event)无 Nginx/Apache 依赖2.1 使用 ReactPHP 创建一个最简单的 HTTP 服务器ReactPHP 是一个事件驱动的非阻塞 I/O 库让我们可以轻松地用 PHP 编写网络服务器。首先通过 Composer 安装 ReactPHP 的 HTTP 组件composer require react/http react/socket然后创建服务器文件server.php?php // server.php require __DIR__ . /vendor/autoload.php; $loop React\EventLoop\Factory::create(); $socket new React\Socket\SocketServer(0.0.0.0:8080, [], $loop); $http new React\Http\HttpServer($loop, function (Psr\Http\Message\ServerRequestInterface $request) { // 简单的路由和响应 $path $request-getUri()-getPath(); if ($path /) { $responseBody Hello, this is a pure PHP server!\n; } elseif ($path /json) { $responseBody json_encode([time time(), message Hello from PHP]); } else { // 尝试作为静态文件服务 $filePath __DIR__ . /public . $path; if (file_exists($filePath) is_file($filePath)) { $responseBody file_get_contents($filePath); } else { $responseBody 404 Not Found\n; } } return new React\Http\Message\Response( 200, [Content-Type text/plain], $responseBody ); }); $http-listen($socket); echo Server running at http://0.0.0.0:8080\n; $loop-run();在项目根目录创建一个public文件夹并放一个test.txt文件。mkdir public echo This is a static file served by PHP server. public/test.txt运行服务器php server.php现在你可以用浏览器或curl访问http://localhost:8080/- 返回文本问候http://localhost:8080/json- 返回 JSONhttp://localhost:8080/test.txt- 返回静态文件内容这个简单的例子揭示了纯 PHP 服务器的几个关键特点单进程事件循环$loop-run()启动了一个事件循环在一个进程内处理所有连接。回调函数处理请求每个 HTTP 请求都会触发我们定义的回调函数。混合处理逻辑在同一个回调里我们既处理了动态路由 (/,/json)又尝试提供了静态文件服务。这打破了 Nginx PHP-FPM 的物理边界。3. 性能超越 Nginx 的可能性分析现在回到最核心的问题一个纯 PHP 服务器凭什么在静态文件服务和 PHP 吞吐量上宣称能超越 Nginx我们需要从几个维度来分析这种可能性。重要提示这里的“超越”通常发生在非常特定的基准测试Benchmark场景下不代表在生产环境的所有负载类型下都成立。3.1 静态文件服务绕过内核与零拷贝优化Nginx 服务静态文件的流程大致是接受连接解析 HTTP 请求。根据 URI 映射到文件系统路径。使用sendfile()系统调用将文件内容直接从内核页缓存发送到网卡缓冲区实现“零拷贝”。发送 HTTP 响应头。这已经非常高效。纯 PHP 服务器要在这里竞争必须做到至少同等效率。在用户态PHP 进程读取文件并发送传统方式 (file_get_contentsecho) 会涉及将文件数据从内核缓冲区读入用户态缓冲区PHP 进程内存。再将用户态缓冲区的数据通过write()系统调用写回内核的套接字缓冲区。 这多了一次内存拷贝内核-用户-内核即“二次拷贝”效率更低。那么纯 PHP 服务器如何优化使用sendfile()系统调用PHP 的stream_copy_to_stream()函数在某些系统和配置下如果源是文件流目标是套接字流底层可能会优化为sendfile()。更直接的方式是使用ext-event扩展提供的EventBuffer::addFile()或 Swoole 的Server-sendfile()方法它们会直接调用sendfile()。内存映射mmap将文件映射到进程的虚拟内存空间然后直接操作这块内存区域发送数据可以减少一次拷贝。消除进程间转发这是最大的潜在优势。在 Nginx PHP-FPM 架构中即使是一个静态文件请求也需要完整走一遍 Nginx 的请求处理流程。而一个优化过的纯 PHP 服务器其静态文件处理路径可以做得极其简短直接匹配 URI 到文件并发送。在极端优化的微基准测试中如果 Nginx 配置了复杂的重写规则、日志记录、访问控制等而纯 PHP 服务器只做最简单的路径映射那么后者在“处理单个静态文件请求”的延迟上可能更低。但这不代表 Nginx 处理能力弱只是它的功能更全面开销也更大。3.2 PHP 请求吞吐量消除 IPC 与序列化开销这是纯 PHP 服务器宣称有“10x PHP 吞吐量”潜力的主战场。瓶颈主要在于IPC进程间通信和序列化。在 Nginx PHP-FPM 架构下一个/api/user请求的处理流程客户端 - Nginx (解析HTTP匹配location) - FastCGI协议编码 - Unix Socket/TCP - PHP-FPM (解码FastCGI分配worker) - 执行PHP - 编码结果 - Unix Socket/TCP - Nginx - 发送HTTP响应 - 客户端其中-表示数据拷贝和上下文切换。在纯 PHP 服务器架构下流程简化为客户端 - PHP服务器进程 (解析HTTP执行PHP) - 发送HTTP响应 - 客户端减少的开销包括FastCGI 协议编码/解码将 HTTP 元数据头、参数和正文转换为 FastCGI 记录格式以及反向解码。两次进程间通信的数据拷贝数据需要在 Nginx 进程和 PHP-FPM 进程的内存空间之间来回拷贝。PHP-FPM 的进程管理开销PHP-FPM 需要管理 worker 进程池、处理 worker 的创建/回收、在 worker 间分配请求。对于微小请求例如返回一个固定的 JSON 字符串、执行一个简单的echo “ok”;后者的架构优势会被放大。因为请求的处理时间本身极短可能小于 1ms而 IPC 和协议处理的开销占比就变得非常显著。消除这部分开销理论上可以获得数量级级别的提升。一个简单的类比就像把两个紧耦合的微服务合并成一个单体服务减少了网络 RPC 调用性能自然会提升。3.3 常驻内存与 OPcache 的极致利用在传统模式下每个 PHP-FPM worker 进程虽然是常驻的但每次请求依然要经历初始化执行环境 - 编译/从 OPcache 加载字节码 - 执行 - 清理。虽然 OPcache 极大缓解了编译开销但执行环境的初始化和清理php_request_startup/php_request_shutdown仍有成本。在基于 ReactPHP/Swoole 的纯 PHP 服务器中应用代码在服务器启动时加载一次常驻内存。对于单个请求处理不需要重新初始化全局结构。类、函数定义已在内存中。数据库连接池、Redis 连接、配置数组等可以提前创建并复用。这意味着处理请求时直接跳到了“执行”阶段省去了大量的初始化工作。这对于框架臃肿的应用如 Laravel、Symfony性能提升尤为明显因为框架本身的启动成本很高。4. 实战构建一个高性能的纯 PHP 服务器原型让我们超越简单的 ReactPHP 示例探讨一个更接近生产环境、追求性能的纯 PHP 服务器需要哪些组件。我们将以 Swoole 为例因为它提供了更底层的、针对性能优化的 API。4.1 基于 Swoole 的 HTTP 服务器Swoole 是一个 PHP 的异步、并行网络通信引擎使用 C 语言编写作为 PHP 扩展提供。安装 Swoolepecl install swoole # 或者在 Ubuntu 上 # sudo apt-get install php8.1-swoole确保php.ini中启用了extensionswoole.so。创建高性能 HTTP 服务器swoole_server.php?php // swoole_server.php $http new Swoole\Http\Server(0.0.0.0, 9501); // 设置服务器参数 $http-set([ worker_num swoole_cpu_num() * 2, // 工作进程数建议为CPU核数的1-4倍 daemonize false, // 非守护进程调试时设为false max_request 10000, // 每个worker处理一定数量请求后重启防止内存泄漏 dispatch_mode 2, // 固定模式保证同一个连接发来的数据包会被分配到同一个worker enable_static_handler true, // 启用静态文件处理 document_root /path/to/your/project/public, // 静态文件根目录 http_parse_post true, ]); // 监听请求事件 $http-on(request, function ($request, $response) { $path $request-server[request_uri]; // 动态路由示例 if ($path /hello) { $response-header(Content-Type, text/plain); $response-end(Hello from Swoole!\n); return; } if ($path /api/data) { // 模拟业务逻辑 $data [id 1, name Test, timestamp time()]; $response-header(Content-Type, application/json); $response-end(json_encode($data)); return; } // 对于未匹配静态文件且未定义路由的请求返回404 // 因为开启了 enable_static_handlerSwoole会先尝试在 document_root 下找文件 // 如果没找到才会走到这里。所以这里可以处理“真正的”404。 if (!preg_match(/\.(?:js|css|png|jpg|jpeg|gif|ico|txt)$/, $path)) { $response-status(404); $response-header(Content-Type, text/plain); $response-end(404 Not Found\n); } }); // 服务器启动事件 $http-on(start, function ($server) { echo Swoole HTTP server is started at http://0.0.0.0:9501\n; }); $http-start();运行服务器php swoole_server.php关键优化点分析enable_static_handler: 这是性能关键。当设置为true时Swoole 会先检查请求的 URI 是否对应document_root下的静态文件。如果存在Swoole 底层会使用高效的sendfile系统调用直接发送文件完全绕过 PHP 代码层。这实现了和 Nginx 类似的静态文件处理效率。Worker 进程池worker_num设置了多个工作进程充分利用多核 CPU。主进程负责接受连接然后通过进程间负载均衡将连接分配给 Worker 处理。常驻内存on(request)回调函数中的代码包括引入的类、定义的变量在 Worker 进程启动时加载一次之后常驻内存。$request和$response对象在每个请求中重复使用复用内存。异步非阻塞虽然我们这个例子是同步写法但 Swoole 的 Worker 内部是异步非阻塞的。一个 Worker 可以同时处理成千上万个连接在遇到 I/O 操作如数据库查询、远程 API 调用时可以挂起当前请求去处理其他请求等 I/O 完成后再回来继续处理。这需要配合 Swoole 的协程和异步客户端使用。4.2 性能基准测试对比概念性为了验证理论我们可以设计一个简单的基准测试。注意以下数据仅为示意实际结果因硬件、软件版本、配置、测试压力模型不同而有巨大差异。测试环境机器4核 CPU 8GB 内存Nginx 1.18 PHP-FPM 8.1 (pmdynamic, pm.max_children50)Swoole HTTP Server (worker_num8)测试工具wrk或ab场景一微小 API 请求端点返回json_encode([status ok])并发数100测试时长30秒架构平均延迟 (ms)每秒请求数 (RPS)Nginx PHP-FPM~2.5ms~38,000Swoole HTTP Server~0.8ms~120,000分析Swoole 方案消除了 IPC 开销和 PHP-FPM 的请求初始化开销RPS 提升显著。场景二静态小文件 (1KBhello.txt)并发数100架构平均延迟 (ms)每秒请求数 (RPS)Nginx~0.3ms~320,000Swoole (enable_static_handlertrue)~0.35ms~300,000分析两者都使用sendfile性能在同一个数量级。Nginx 经过多年优化在纯静态文件领域依然略有优势但差距很小。如果 Nginx 配置了复杂的模块如大量rewrite规则、access_logSwoole 的简单路径映射可能反超。场景三带有数据库查询的请求端点查询一次数据库主键查询返回结果。并发数50架构平均延迟 (ms)每秒请求数 (RPS)Nginx PHP-FPM~15ms~3,300Swoole HTTP Server 连接池~12ms~4,100分析此时瓶颈主要在数据库 I/O。Swoole 的优势在于可以使用连接池复用数据库连接避免了每次请求建立连接的开销TCP三次握手、数据库认证因此仍有提升。如果使用协程异步 MySQL 客户端在高并发下优势会更明显。5. 常见问题与排查思路将纯 PHP 服务器用于生产环境你会遇到一些与传统模式不同的问题。5.1 代码更新与热重载问题在传统 PHP-FPM 模式下上传新代码后下一次请求就会自动使用新代码因为 worker 进程在处理完请求后会结束新的 worker 会加载新代码。在常驻内存的 PHP 服务器中旧的代码一直驻留在内存里。解决方案手动重启发送信号给主进程如kill -USR1 pidSwoole/ReactPHP 支持平滑重启只重启 Worker 进程不影响正在处理的请求。Max Requests 机制像上面配置的max_request 10000每个 Worker 在处理一定数量的请求后会自动重启间接实现了代码更新。文件监控与自动重启使用像inotify这样的工具监控项目文件变更然后触发服务器重启。这通常需要额外的守护进程。部署策略采用蓝绿部署或滚动更新通过负载均衡器将流量切换到新的服务器实例。5.2 内存泄漏排查问题由于进程常驻内存任何在请求间未释放的变量或资源积累都会导致内存缓慢增长最终使进程崩溃。排查与预防避免使用全局变量或静态变量存储请求相关数据。请求结束后这些数据应被 GC 回收但如果被意外引用则不会。明确关闭资源数据库连接、文件句柄等如果在回调函数中打开尽量在函数结束时关闭。使用连接池是更好的选择。使用max_request强制 Worker 定期重启释放所有内存。监控内存在onWorkerStart、onRequest等事件中记录进程内存使用情况memory_get_usage(true)。使用内存分析工具如 Xdebug 的 Profiler 或 Blackfire定期对 Worker 进程进行内存分析。5.3 与传统 PHP 生态的兼容性问题很多 PHP 库和框架尤其是 Laravel、Symfony在设计时假设了“每次请求一个独立生命周期”的模式。它们会在请求开始时初始化大量服务容器、注册单例在请求结束时清理。这在常驻内存环境下会导致单例污染、上下文混乱。解决方案使用适配的框架选择为常驻内存设计的框架如 Swoole 生态的hyperf、imi或 ReactPHP 生态的laravel-octane官方适配包。如果必须使用传统框架认真阅读其“常驻内存”适配指南。例如Laravel Octane 会指导你如何清理请求间状态。将框架“引导”部分放在 Worker 启动时(onWorkerStart)而不是每次请求。使用“上下文隔离”确保每个请求的$request和$response对象是独立的框架的容器或全局状态不会在请求间残留数据。5.4 调试与日志记录问题在 CLI 模式下运行的服务器echo、var_dump会直接输出到终端或标准输出不利于集中管理。错误日志也需要特殊配置。最佳实践配置错误日志在php.ini中为 CLI SAPI 单独设置error_log或是在服务器启动脚本中通过ini_set(error_log, /path/to/swoole.log)设置。使用 PSR-3 兼容的日志库如 Monolog。在onWorkerStart中初始化日志器并将其注入到应用上下文中。将输出重定向到文件使用$server-set([log_file /path/to/server.log])Swoole来记录服务器运行日志。接入 APM使用像 OpenTelemetry 这样的工具进行分布式追踪可以清晰看到请求在常驻内存服务器中的完整生命周期。6. 最佳实践与工程建议在考虑将纯 PHP 服务器用于生产环境前请务必评估以下实践和建议。6.1 适用场景与不适用场景非常适合的场景API 网关/微服务中间件需要处理大量轻量级、低延迟的 API 请求。实时通信服务WebSocket 服务器、聊天室、推送服务。Swoole 对此有原生支持。高性能的 JSON/XML RPC 服务。内部工具或管理后台对性能有要求但流量可控便于管理。替代 Node.js/Go 的部分场景当你团队主力是 PHP又需要开发一个高性能的网络服务时。需要谨慎或不适用的场景托管不可控的用户代码或插件常驻内存环境下的代码漏洞可能导致整个服务崩溃。严重依赖exit、die或全局状态的老旧应用迁移成本极高风险大。主要提供大型静态文件下载或流媒体服务Nginx 经过极度优化且有丰富的模块如限速、断点续传仍是更专业的选择。团队对 Swoole/ReactPHP 不熟悉且没有运维经验引入新技术栈有学习成本和维护风险。6.2 生产环境部署要点进程管理不要直接用php server.php启动。使用Systemd或Supervisor来管理进程实现开机自启、崩溃自动重启、日志轮转。; Supervisor 配置示例 (/etc/supervisor/conf.d/php-server.conf) [program:php-swoole] command/usr/bin/php /path/to/your/swoole_server.php directory/path/to/your autostarttrue autorestarttrue userwww-data redirect_stderrtrue stdout_logfile/var/log/swoole.log反向代理前置即使你的 PHP 服务器性能很好也强烈建议在前面放置一个 Nginx 或 Caddy 作为反向代理。原因SSL/TLS 终止让专业的 Web 服务器处理 HTTPS 加解密性能更好。静态文件分离虽然 PHP 服务器能处理静态文件但让 Nginx 处理 CSS、JS、图片等可以减轻 PHP 服务器的负担配置缓存策略也更方便。负载均衡与高可用可以轻松地在多个 PHP 服务器实例前进行负载均衡。缓冲与保护Nginx 可以缓冲客户端请求保护后端服务器不被慢客户端拖垮。访问日志与安全集中管理访问日志、实现 WAF 功能等。# Nginx 配置示例 upstream php_swoole { server 127.0.0.1:9501; # server 127.0.0.1:9502; # 可以启动多个实例 keepalive 32; # 保持长连接减少连接建立开销 } server { listen 80; server_name yourdomain.com; root /path/to/your/public; location / { # 将动态请求代理到 Swoole 服务器 proxy_pass http://php_swoole; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态文件由 Nginx 直接处理 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control public, immutable; try_files $uri 404; } }监控与告警监控服务器进程的内存、CPU 使用率、打开的连接数、请求吞吐量和延迟。集成到 Prometheus Grafana 或商业 APM 中。6.3 性能调优方向调整 Worker 数量worker_num并非越多越好。通常设置为 CPU 核数的 1-2 倍。过多的 Worker 会增加上下文切换开销。可以通过压测找到最佳值。使用连接池对于数据库、Redis、Memcached 等务必使用连接池。避免每个请求创建新连接。启用协程Swoole 4.0 提供了强大的协程支持。使用协程客户端如Swoole\Coroutine\MySQL进行 I/O 操作可以极大提升并发能力代码写法也更接近同步易于理解。优化 OPcache确保opcache.enable1并适当调大opcache.memory_consumption如 256MB。常驻服务器对 OPcache 的依赖更重。谨慎使用 JITPHP 8.0 的 JIT 对于 CPU 密集型计算有提升但对于 I/O 密集型的网络服务器提升可能有限甚至会因编译开销带来负面效果。建议通过压测决定是否开启。纯 PHP 服务器在特定场景下性能超越 Nginx PHP-FPM这并非神话而是架构简化带来的必然结果。它通过消除进程间通信、实现请求处理路径最短化、以及极致利用常驻内存和连接复用在微基准测试中取得了亮眼的成绩。然而技术选型从来不是只看性能数字。Nginx 经过十多年的发展其稳定性、功能丰富性、生态系统和运维经验是无可替代的。纯 PHP 服务器尤其是 Swoole为我们提供了一种新的、高性能的 PHP 应用部署范式特别适合构建微服务、实时应用和 API 密集型服务。对于大多数现有项目我建议的路径是继续使用 Nginx PHP-FPM 作为稳健的基础。对于项目中性能瓶颈明显的特定端点或新开发的实时模块可以考虑使用 Swoole HTTP 服务器或 Worker 作为独立的微服务进行拆分部署由 Nginx 作为统一入口进行路由。这样既能享受新架构的性能红利又能依托成熟生态的稳定性。最终是否采用纯 PHP 服务器取决于你的具体业务需求、团队技术栈和运维能力。理解其原理和优劣能让你在架构设计时多一个有力的选项。