You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP Server Sent Event中While循环致服务器过载的解决方案咨询

解决SSE连接频繁重连与服务器过载问题

核心问题分析

你当前的代码存在两个关键问题:

  1. 无意义的轮询循环:while(true)每秒执行一次,每个客户端连接会长期占用一个PHP-FPM进程,用户量上升后会快速耗尽服务器进程池,导致过载。
  2. 频繁心跳与重连:每秒发送心跳+默认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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 14:30:07