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
相关产品推荐
相关产品推荐

