PHP Server Sent Event中While循环致服务器过载的解决方案咨询
解决SSE连接频繁重连与服务器过载问题
核心问题分析
你当前的代码存在两个关键问题:
- 无意义的轮询循环:
while(true)每秒执行一次,每个客户端连接会长期占用一个PHP-FPM进程,用户量上升后会快速耗尽服务器进程池,导致过载。 - 频繁心跳与重连:每秒发送心跳+默认3秒重连机制,既浪费带宽,又让连接频繁启停,增加服务器连接开销。
优化方案
1. 改用阻塞式数据监听(替代主动轮询)
不要主动每秒检查数据,而是通过消息队列/事件监听的方式,只有当真正有新数据时才唤醒进程发送消息,空闲时进程处于阻塞状态,大幅降低CPU和进程占用。
以Redis Pub/Sub为例(适合大多数实时更新场景),修改后的代码如下:
<?php session_start(); session_write_close(); ignore_user_abort(true); header("Content-Type: text/event-stream"); header("Cache-Control: no-cache"); header("Access-Control-Allow-Origin: *"); // 处理Last-Event-ID,恢复断点 $lastEventId = floatval($_SERVER["HTTP_LAST_EVENT_ID"] ?? $_GET["lastEventId"] ?? 0); // IE兼容的2KB填充 echo ":" . str_repeat(" ", 2048) . "\n"; // 把默认重试时间拉长到30秒,避免客户端频繁重连 echo "retry: 30000\n"; // 连接Redis(根据你的实际配置调整) $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 订阅数据更新频道 $redis->subscribe(['work_updates'], function ($redis, $channel, $message) use (&$lastEventId) { // 检查客户端是否已断开 if (connection_aborted()) { $redis->unsubscribe(); exit(); } // 发送新数据 $latestEventId = $lastEventId + 1; echo "id: {$latestEventId}\n"; echo "data: {$message}\n\n"; ob_flush(); flush(); $lastEventId = $latestEventId; }); // 心跳机制:30秒无数据时发送心跳,维持连接 $heartbeatInterval = 30; $lastHeartbeat = time(); while (true) { if (connection_aborted()) { exit(); } if (time() - $lastHeartbeat >= $heartbeatInterval) { echo ": heartbeat\n\n"; ob_flush(); flush(); $lastHeartbeat = time(); } // 让出CPU资源,避免空循环占用过高 usleep(100000); } ?>
2. 调整PHP-FPM配置(如果继续用FPM)
每个SSE长连接会占用一个FPM进程,需要调整FPM的进程数上限:
- 修改
php-fpm.conf中的pm.max_children,根据服务器CPU核心数和内存调整(比如8核服务器可设为60-100) - 同时调整
pm.start_servers、pm.min_spare_servers、pm.max_spare_servers,确保有足够的空闲进程处理新连接
3. 高并发场景:改用异步框架
如果你的用户量较大,PHP-FPM的进程模型会成为瓶颈,推荐使用Swoole/Workerman这类异步非阻塞框架,单个进程可处理数千个长连接,大幅提升服务器承载能力。
Swoole版SSE示例:
<?php $http = new Swoole\Http\Server("0.0.0.0", 9501); $http->on('request', function ($request, $response) { if ($request->server['path_info'] !== '/sse') { $response->end('Not Found'); return; } // 设置SSE响应头 $response->header('Content-Type', 'text/event-stream'); $response->header('Cache-Control', 'no-cache'); $response->header('Access-Control-Allow-Origin', '*'); // 处理Last-Event-ID $lastEventId = floatval($request->header['last-event-id'] ?? $request->get['lastEventId'] ?? 0); $response->write(":" . str_repeat(" ", 2048) . "\n"); $response->write("retry: 30000\n"); // 模拟监听数据更新(实际场景替换为Redis订阅/数据库事件) $timer = Swoole\Timer::tick(5000, function () use (&$lastEventId, $response) { $latestEventId = $lastEventId + 1; $response->write("id: {$latestEventId}\n"); $response->write("data: 实时更新内容 ({$latestEventId})\n\n"); $lastEventId = $latestEventId; }); // 客户端断开时清理定时器 $response->on('close', function () use ($timer) { Swoole\Timer::clear($timer); }); }); $http->start();
关键优化点总结
- 用阻塞式监听替代主动轮询,减少空循环的资源消耗
- 拉长心跳间隔(30秒以上),降低不必要的输出
- 客户端断开时及时释放进程/资源,避免僵尸进程
- 高并发场景下切换到异步框架,提升连接处理能力
内容的提问来源于stack exchange,提问作者Thomas D.
相关产品推荐
相关产品推荐

