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

SSE(服务端推送事件)本地正常但生产共享主机批量返回问题排查求助

问题根因

你的问题核心是共享主机前置的Nginx反向代理默认开启了proxy_buffering,会把后端返回的响应内容攒到缓冲区大小(默认通常为4KB)后才发给客户端,哪怕PHP侧已经主动flush了缓冲内容,Nginx层的缓冲不受PHP配置控制。


可行解决方案(无需修改Nginx配置)

1. 轻量缓冲区填充(推荐,改动最小)

不用填充800KB的冗余内容,仅需要填充刚好超过Nginx默认缓冲区大小的空白字符即可,SSE协议会自动忽略消息之外的空白内容,不会影响业务解析,开销极低:

// 优先尝试加这个Nginx专用Header,部分共享主机支持直接禁用当前请求的代理缓冲,生效则不需要填充内容
header('X-Accel-Buffering: no');
// 禁用压缩避免额外缓冲
header('Content-Encoding: none');

function send_message($id, $message, $progress) {
    $d = array('message' => $message , 'progress' => $progress);
      
    echo "id: $id\n";
    echo "data: " . json_encode($d) . "\n";
    echo "\n";
    // 若上面的X-Accel-Buffering头不生效,再加这行填充4KB空白跳过Nginx默认缓冲区
    echo str_pad('', 4096) . "\n";
      
    // 清空所有PHP层输出缓冲
    while(ob_get_level() > 0) {
        ob_end_flush();
    }
    flush();
}

2. 改用短轮询方案(兼容性最好)

如果完全不想用填充逻辑,可以把SSE的长连接逻辑改成短轮询,完全绕过反向代理缓冲限制:

  • 前端将EventSource替换为定时请求,每秒拉取一次最新进度
  • 后端将长任务进度存储在Session、临时文件或缓存中,拆分出任务启动、进度查询、任务停止三个独立接口

前端示例代码:

var pollTimer;
function startTask() {
    // 触发长任务启动
    fetch('long_process.php?action=start');
    // 每秒轮询进度
    pollTimer = setInterval(async () => {
        const resp = await fetch('long_process.php?action=getProgress');
        const result = await resp.json();
        addLog(result.message);
        if(result.progress >= 100) {
            addLog('处理完成,停止轮询');
            clearInterval(pollTimer);
            document.getElementById('progressor').value = 100;
            document.getElementById('percentage').innerHTML = '100%';
            return;
        }
        document.getElementById('progressor').value = result.progress;
        document.getElementById('percentage').innerHTML = result.progress + '%';
    }, 1000);
}
function stopTask() {
    clearInterval(pollTimer);
    fetch('long_process.php?action=stop');
    addLog('任务已中断');
}

3. XHR流式解析替代EventSource

使用XHR的onprogress事件实时读取增量响应内容,不需要等整个响应完成就可以解析SSE消息,兼容性比EventSource更好,也能一定程度绕过缓冲限制:

function startTask() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'long_process.php');
    let lastReadPos = 0;
    xhr.onprogress = () => {
        // 读取上次位置之后的新增响应内容
        const newContent = xhr.responseText.slice(lastReadPos);
        lastReadPos = xhr.responseText.length;
        // 按SSE协议解析消息
        const eventList = newContent.split('\n\n');
        eventList.forEach(eventStr => {
            if(!eventStr.trim()) return;
            let eventId, eventData;
            eventStr.split('\n').forEach(line => {
                if(line.startsWith('id: ')) eventId = line.slice(4);
                if(line.startsWith('data: ')) eventData = JSON.parse(line.slice(6));
            });
            if(!eventData) return;
            addLog(eventData.message);
            if(eventId === 'CLOSE') {
                addLog('收到关闭指令,断开连接');
                xhr.abort();
                document.getElementById('progressor').value = 100;
                document.getElementById('percentage').innerHTML = '100%';
                return;
            }
            document.getElementById('progressor').value = eventData.progress;
            document.getElementById('percentage').innerHTML = eventData.progress + '%';
        });
    };
    xhr.onerror = () => {
        addLog('出现错误');
        xhr.abort();
    };
    xhr.send();
}

配置差异排查方向

本地与共享主机的核心差异除了Nginx代理外,还可以排查以下配置项:

  • FastCGI缓冲:共享主机的FastCGI可能开启了fastcgi_buffering,会缓冲PHP输出的内容
  • 压缩配置:如果Nginx开启了对text/event-stream类型内容的gzip压缩,会攒够内容再压缩返回
  • 输出缓冲层级:共享主机的PHP可能开启了多层输出缓冲,单次ob_flush()仅能清空最上层缓冲

内容的提问来源于stack exchange,提问作者Juan Rangel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 00:36:03