十年过去了,服务器发送事件仍然未能达到生产环境的要求。这对我来说是一个教训,对你来说也是一个警告!

2025-06-07

十年过去了,服务器发送事件仍然未能达到生产环境的要求。这对我来说是一个教训,对你来说也是一个警告!

TL;DR

  • 我曾经遇到过一起与 SSE 相关的事件,给客户带来了很大的困扰,下面我会详细说明
  • 服务器发送事件是一种长期建立并推荐的从服务器送到客户端的方法
  • 许多文章都宣称它们的优点,例如自动重新连接,包括追赶错过的消息,比套接字更容易、更可靠等
  • 在规范和细则中隐藏着一些东西,使得它们在不受控制的环境中(必须普遍可用的应用程序和公共网站)完全不可靠
  • 不要指望你的活动能很快在某些公司或老旧的网络上传递

 着火了!着火了!

我讨厌那种傻乎乎的感觉,你知道是怎么回事,我发布了一个软件版本,自动和手动测试都说没问题,可扩展性测试也说没问题。然后10天后,一位客户经理说“XYZ公司”抱怨软件登录“很慢”。

好吧,你觉得登录慢吗?我们来看看。不,一点也不慢,没有过度负载,所有服务器都运行良好。嗯……

客户反映登录仍然“非常慢”。好吧,最后我想问“有多慢?”—— 20分钟——哇哦——20分钟登录不算“慢”,20分钟根本就是烂透了。

我们到处都检查过了,一切正常。他们的账户也正常。肯定是网络问题,果然如此。

自推出以来,我们一直使用服务器发送事件向客户端发送通知,但最近我们将其用于更多场景——基本上,我们向服务器发送请求,服务器会立即返回已入队信息,然后结果稍后会通过服务器事件推送。这极大地简化了我们的流程,使其速度更快,可扩展性更强。但问题是,对于少数客户端来说,我们的事件从未被成功推送过。

“我们的活动从未向某些客户交付过”——哦糟糕,架构失败,偶尔出现错误的原因突然升级为优先级 1,我的内衣着火了,搞砸了。

事情是这样的——我们的服务器和客户端计算机之间出了点问题,导致事件被永久保存。它之所以能正常工作,是因为它每隔几分钟就会重新连接一次,而 SSE 的“可靠性”意味着我们能收到被吞掉的消息。

一群开发人员和 DevOps 开始在网上搜索到底发生了什么,我们忘记了什么,以及我们需要设置哪些设置才能正常工作。答案是坏消息:我们无能为力!

SSE 的问题

问题在于:SSE 会向客户端打开一个没有内容长度的流,并在数据包可用时将其发送。然而,问题在于,它使用了传输编码,这只能保证传输到链中下一个节点的方式。

任何位于服务器和客户端设备之间连接的老旧代理都可以合法地存储所有这些数据包,并等待流关闭后再转发它们。它会这样做,因为它没有看到 Content-Length,于是会想——我的客户端想知道它有多大。也许代码早于不需要 Content-Length 的 text/event-stream,谁知道呢,但它们就在那里,会偷走你的午餐钱。

是的,这完全符合规范且合法,而且没有可以发送的标头来禁用它。你说“我想全部发送”,链中的下一个节点就会覆盖它,并说“我想先分块发送,直到全部发送完成”。

当然,你可以在 NGINX 上禁用它(距离你的服务器只有一跳),但谁知道是什么原因导致你的应用崩溃呢?归根结底,如果你无法控制网络基础设施,你就无法依赖 SSE。

真倒霉。

推送至客户端

好的,基本上有 4 种方法可以从服务器获取“未启动”的请求:

方法 描述 评论
WebSockets 客户端和服务器之间的双向通信——两端各有一个打开的“套接字”来接收信息。 套接字在工作时很棒,但让它们稳定地工作却很困难,我们可以使用像socket.io这样的库来帮助我们,当套接字不可用时,它会使用许多技术(如长轮询)。
  • 由于某些环境中的各种复杂的握手和操作中断(尤其是负载平衡),需要图书馆的帮助才能在现实世界中工作。
  • 像 socket.io 这样的库使用长轮询来获取早期连接并尝试升级。
  • 在很多现实情况下升级都会失败,因此您只能依赖长轮询的性能。
  • 您需要自行处理丢失的消息和重新连接。
服务器发送事件 从服务器到客户端的单向推送通信,始终保持连接开放。
  • 处理断开/重新连接和错过的消息
  • 无需特殊握手
  • 网络代理和其他硬件/软件可以完全破坏它。
长轮询 客户端会打开与服务器的连接,服务器会等待收到消息后再发送。客户端会立即打开新的连接。感觉就像随时可以从服务器推送消息一样。
  • 始终有效,但如果没有数据开始发送,某些代理可能会快速关闭连接 - 增加性能开销但不会导致失败。
  • 需要频繁重新建立连接以及建立标头的成本等。
  • 连接特定资源和缓存更加困难,因为每个新消息块都可以路由到没有粘性会话的不同服务器。
轮询 简单方法,客户端定期请求事件。
  • 延迟 - 我们只有在询问时才会收到请求,因此不能依赖它进行高速消息传递(例如聊天等)。
  • 即使没有任何数据,我们也会发出请求——这可能会对性能产生重大影响

这是我们最初决定使用 SSE 时参考的一篇文章

结论

我们现在已经重写了我们的层,使用长轮询,这样我们就能在我们的软件运行的正常环境(互联网以及一些非常老旧的企业和工业网络)中保持稳定的性能。它确实有效。我希望我之前就知道 SSE 的局限性——但那天很晚才在规范中找到了一段。

2025 年更新

我现在使用 SSE,但等待客户端的确认;这仅在连接开始时发生;如果我没有收到确认,我会在去抖动超时后关闭服务器端的流,这就像一个长轮询解决方案,以防流消息无法成功。

文章来源:https://dev.to/miketalbot/server-sent-events-are-still-not-production-ready-after-a-decade-a-lesson-for-me-a-warning-for-you-2gie
PREV
可视化地形的最佳工具
NEXT
不使用 .map 的 React 列表