WebSocket 限流的研究

2,180 阅读4分钟

关于 WebSocket,在四年前做实验室习题时第一次接触,当时基本上没怎么介绍 WebSocket,只贴了一份链接,而恰巧这次做了一些 WebSocket 上的工作,所以会从头开始介绍一下 WebSocket。

WebSocket 的原理

在 WebSocket 以前,Web 工作人员已经探索过好多种方法了,由于我没有生在那个时代,所以除了 ajax 轮询之外的方法,我完全都没有记住,不过 WebSocket 确实是站在了巨人的肩膀上,终于成为了一套事实方案,而其维护成本和原理也比 TCP 的 socket 链接要简单不少。

在 TCP 上,我们有经典的「三次握手」「四次挥手」,这里以三次握手来说,大致通话是:「喂,你在吗?」「我在」「好我知道了」,然后他们俩就建立了连接,这是一个比较简单的问题,但是如果我问你,三次握手中,每一次的报文分别是怎么样的,你又要如何解决 TCP 中经典的粘包问题——不经常进行网络编程而又没有进行面试复习的你可能就一时想不起来了。

在 WebSocket 中,模型大致与 TCP 相同:「都有握手」、「都有报文」、「都会超时断开」,但是相比之下,模型更加简单:

  1. 只进行一次握手,由 HTTP 协议握手,升级请求到 Websocket 协议
  2. 不会有粘包拆包问题
  3. 超时重连非常简单,而且主流库都会自行心跳维持

限流策略

在实际应用中,我们发现对于 Websocket 应用来说,断开重连表面上来说没什么问题,非常贴心,实际上,由于某些意外,我们的其中一个容器可能会挂掉,而此时会让与这个容器连接的所有 Websocket 都断开,同一时间,容器恢复,接下来源源不断的同时想要建立握手的请求就有可能再次打挂这个容器。

常用的单机限流策略有三种:

  1. 计数器
  2. 漏斗
  3. 令牌桶

令牌桶可以看做是漏斗的改进,详情可以看一下这个文章:谈谈服务限流算法的几种实现,在这里我们主要概括一些来说,令牌桶就是桶内定时生产令牌,请求必须拿到了令牌在执行,而计数器则是在一段时间内进行请求计数,超过一定值就拒绝,等待下一个时间周期。

两种方法实现起来都不是特别的困难,可以按照自己的业务场景来进行选择。

限流在 socket.io 中的实践

socket.io 提供了一些方法来在各个阶段挂载一些注入方法(middleware(伪)),常用的有:

  • server.use 尽管文档没写,但好像是可以用的,但是它处理 connection 后的请求,如果需要处理异常,可以 next(new Error('error')),这样会发起一个 error 的 emit。
  • socket.use
  • namespace.use

但是所有的方法都在建立握手后的 ws 协议中做一些中间处理,似乎并没有人提到握手阶段的处理,为此我 Google 和 stackoverflow 的半天,甚至翻完了 engine.io 的文档,直到最后才发现在 new Server(httpServer[, options]) 中有一个 allowRequest

A function that receives a given handshake or upgrade request as its first parameter, and can decide whether to continue or not. The second argument is a function that needs to be called with the decided information: fn(err, success), where success is a boolean value where false means that the request is rejected, and err is an error code.

用于专门处理握手阶段是否成功。

一个 allowRequest 的方法大概是这样的:

// req 就是 Node 中的 Request
// callback 接受两个参数,第一个虽然官方说是 error code, 实际上是 error message,第二个代表是否握手成功
allowRequest(req, callback) {
  // 限流
  if (限流满) { callback('限流满', false) }
  else { callback(null, true) }
}

总结

当然,这其实是一个非常简单的限流模型,很明显我们带出了两个问题:

  1. 之所以会出现这种问题,其实还是流量的分配不均,否则一台机器挂了,给其他机器分摊,这样的问题会少很多
  2. 限流本质上是单进程限流,但是瓶颈是容器级别的,限流却是进程级别的,这样似乎代表一个容器只能跑一个进程,完全没有充分利用机器配置。

针对这两个问题,或许下次有机会可以带来一篇「谈谈 Websocket 的均衡负载」。