为什么我的服务器发送事件成批到达?

Why are my server sent events arriving as a batch?

我有一个 Java 基于 8 / Spring4 的 Web 应用程序,它使用服务器发送事件 (SSE) 向 browser-based 客户端报告 long-running 进程的进度 运行 一些 Java 脚本和更新进度条。在我的开发环境和我们的开发服务器上,SSE 以 near-real-time 的形式到达客户端。我可以使用 Chrome 开发工具看到他们到达(连同他们的时间戳)并且进度条更新顺利。

但是,当我部署到我们的生产环境时,我观察到不同的行为。在 long-running 进程完成之前,事件不会到达浏览器。然后它们都突然到达(根据开发工具,所有事件的时间戳都在几百毫秒内)。进度条在此期间停留在 0%,然后很快跳到 100%。同时,我的服务器日志告诉我事件是定期生成和发送的。

这是相关的服务器端代码:

public class LongRunningProcess extends Thread {
    private SseEmitter emitter;
    public LongRunningProcess(SseEmitter emitter) {
        this.emitter = emitter;
    }
    public void run() {
        ...
        // Sample event, representing 10% progress
        SseEventBuilder event = SseEmitter.event();
        event.name("progress");
        event.data("{ \"progress\": 10 }"); // Hand-coded JSON
        emitter.send(event);
        ...
    }
}

@RestController
public class UploadController {
    @GetMapping("/start")
    public SseEmitter start() {
        SseEmitter emitter = new SseEmitter();
        LongRunningProcess process = new LongRunningProcess(emitter);
        process.start();
        return emitter;
    }
}

这是相关的 client-side Java脚本:

EventSource src = new EventSource("https://www.example.com/app/start");
src.addEventListener('progress', function(event) {
    // Process event.data and update progress bar accordingly
});

我相信我的代码相当典型,并且在 DEV 中工作得很好。但是,如果有人看到问题请告诉我。

问题可能与我们生产服务器的配置有关。 DEV 和 PROD 都是 运行 相同版本的 Tomcat。但是,其中一些是通过负载平衡器访问的(在这种情况下为 F5)。几乎所有这些都支持 CDN(在我们的例子中是 Akamai)。此设置的某些部分是否会导致 SSE 被缓冲(或排队或缓存),从而可能产生我所看到的结果?

根据基础结构配置想法,我在响应中观察到以下内容 headers。在开发环境中,我的浏览器收到:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: Keep-Alive
Content-Type: text/event-stream;charset=UTF-8
Keep-Alive: timeout=15, max=99
Pragma: no-cache
Server: Apache
Transfer-Encoding: chunked
Via: 1.1 example.com

这是我对事件流的期望。未知内容长度的分块响应。在生产环境中,我的浏览器收到了不同的东西:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Type: text/event-stream;charset=UTF-8
Content-Encoding: gzip
Content-Length: 318
Pragma: no-cache
Vary: Accept-Encoding

此处返回的内容具有已知长度并且被压缩。我认为事件流不应该发生这种情况。似乎有什么东西正在将我的事件流转换成单个文件。关于我如何弄清楚这是怎么回事的任何想法?

您是否尝试过其他浏览器?我正在尝试调试 SSE 在 iPhone 客户端上工作但在 MacOS/Safari 或 Firefox 上不工作的类似问题。

您的问题可能有解决方法 - 如果服务器发送 "Connection: close" 而不是保持活动状态,甚至关闭连接本身,客户端应在几秒钟后重新连接,然后服务器将发送当前进度条事件。

我猜关闭连接会清除导致问题的任何缓冲区。

这不是这个问题的确切解决方案,而是与 SSE、Spring 和压缩的使用有关。

就我而言,我有 ziplet CompressionFilter configured in my Spring application and it was closing the Http Response and causing SSE to fail. This seems to be related to an open issue in the ziplet project。我禁用了过滤器并在 application.properties (server.compression.enabled=true) 中启用了 Tomcat 压缩,它解决了 SSE 问题。

请注意,我没有更改默认的 compressionMinSize 设置,这可能与 SSE 流量未被压缩和通过有关。

经过大量调查才确定问题的原因是我们网络路径中的元素。所以上面的代码是正确的,可以安全使用。如果您发现 SSE 缓冲,您很可能想要检查关键网络元素的配置。

在我的例子中,它是 Akamai 作为我们的 CDN 并使用 F5 设备作为负载平衡器。事实上,两者都可以引入缓冲,这使得诊断问题变得非常困难。

Akamai 边缘服务器默认缓冲事件流。这可以通过使用 Akamai 的 advanced metadata and controlled via custom behaviours 来禁用。目前,这无法通过 Amakai 的门户直接控制,因此您需要让他们的工程师为您做一些工作。

F5 设备似乎也默认缓冲响应数据。幸运的是,这很容易更改,您可以通过设备的配置门户自行完成。对于有问题的虚拟设备,转到 Profile : Services : HTTP 并将 Response Chunking 的配置更改为 Preserve(在我们的例子中它已默认选择性).

进行这些更改后,我开始从我们的 PROD 服务器(而不仅仅是我们的 DEV 服务器)近乎实时地接收 SSE。

webpack 开发服务器在使用代理设置时也会缓冲服务器发送的事件。