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

NGINX+PHP环境下SSE EventStream分块异常及响应头缺失问题

SSE EventStream在NGINX+PHP环境流式传输异常排查与解决

问题现象

本地Apache+PHP环境中,SSE EventStream可正常逐词流式传输大文本,但部署到NGINX+PHP服务器后出现以下异常:

  • 初始延迟约为代码设置的单词延迟的10倍
  • 流以大分块形式返回,而非预期的逐词推送
  • 响应头中缺失Transfer-encoding: chunked和X-Accel-Buffering: no

现有PHP代码

public function sse($delay = 50)
{
    // Sample paragraph of text
    $paragraph = "This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec elit sit amet a. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec elit sit amet a";

    // Split the paragraph into an array of words
    $words = explode(' ', $paragraph);

    // Set up headers for streaming
    header('X-Accel-Buffering: no');
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('Connection: keep-alive');

    ini_set('zlib.output_compression', 0);
    ini_set('implicit_flush', 1);
    // This function discards the contents of the topmost output buffer and turns off this output buffering.
    @ob_end_clean();


    for ($i = 0; $i < ob_get_level(); $i++) { //for each open buffer
        @ob_end_flush(); //close it
    }

    @ob_implicit_flush(1); //turn implicit flush on 

    // Emit "word" event for each word
    $id = 1;
    foreach ($words as $word) {
      echo "id: $id".PHP_EOL;
        echo "event: word".PHP_EOL;
        echo "data: $word".PHP_EOL;
        echo PHP_EOL;
        @ob_flush();
        flush();
        usleep($delay*1000); // Delay between each word (in microseconds)
        $id++;
    }
    @ob_flush();
    flush();
}

当前异常响应头

Cache-Control: no-cache;
Pragma: no-cache;
Content-Type: text/event-stream;charset=UTF-8;
...

已尝试的NGINX配置(部分生效)

proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
proxy_buffering off;
proxy_cache off;
fastcgi_buffering off;

仅proxy_http_version 1.1配置有效:未设置时会一次性返回全部结果,设置后可流式传输但仍存在延迟及大分块问题。


解决方案

1. 调整NGINX核心配置(PHP-FPM场景)

针对PHP-FPM部署,需彻底关闭NGINX侧的缓冲,同时强制传递必要响应头。修改站点配置的location ~ \.php$块:

location ~ \.php$ {
    # 基础PHP-FPM配置(根据实际环境调整)
    fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # SSE流式传输关键配置
    fastcgi_buffering off;
    fastcgi_cache off;
    fastcgi_keep_conn on; # 维持长连接,避免重连损耗
    chunked_transfer_encoding on;

    # 强制添加缺失响应头(若PHP设置未生效)
    add_header X-Accel-Buffering no always;
}

若为反向代理场景(NGINX代理后端PHP服务),需将上述fastcgi_*配置替换为proxy_*配置,同时保留proxy_http_version 1.1和proxy_buffering off。

2. 优化PHP代码,确保缓冲彻底关闭

原代码的缓冲处理存在冗余,且可能受全局PHP配置影响,修改后如下:

public function sse($delay = 50)
{
    $paragraph = "This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec elit sit amet a. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. This is a sample paragraph of text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec elit sit amet a";
    $words = explode(' ', $paragraph);

    // 先禁用所有输出缓冲相关配置
    ini_set('output_buffering', '0');
    ini_set('zlib.output_compression', '0');
    ini_set('implicit_flush', '1');
    ob_implicit_flush(1);

    // 彻底清理所有层级的输出缓冲
    while (ob_get_level() > 0) {
        ob_end_clean();
    }

    // 设置SSE响应头(确保在缓冲清理后设置,避免头被缓冲)
    header('X-Accel-Buffering: no');
    header('Content-Type: text/event-stream; charset=utf-8');
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('Connection: keep-alive');

    $id = 1;
    foreach ($words as $word) {
        // 使用"\n"替代PHP_EOL,避免Windows环境下的\r\n导致分块异常
        echo "id: {$id}\n";
        echo "event: word\n";
        echo "data: {$word}\n";
        echo "\n";
        
        // 强制刷新输出
        @ob_flush();
        flush();
        
        // 若仍存在10倍延迟,检查服务器负载,可尝试用sleep(0.05)替代usleep
        usleep($delay * 1000);
        $id++;
    }
    
    // 最后一次刷新,确保所有内容发送完毕
    @ob_flush();
    flush();
}

3. 验证配置生效

重启NGINX和PHP-FPM服务后,检查响应头是否包含Transfer-encoding: chunked和X-Accel-Buffering: no,同时观察客户端是否逐词接收SSE事件,初始延迟是否恢复至设置的50ms左右。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 17:07:17