如何在kubernetes中实现分布式可扩展的WebSocket服务架构

How to implement a distributed and auto-scalable WebSocket server architecture on Kubernetes一文中虽然解决是WebSocket长连接问题,但可以为其他长连接负载均衡场景提供参考价值

WebRTC 是一套开放web标准,用于在客户端之间建立(端到端方式的)直接通信。WebRTC signaling 是WebRTC协议的前置步骤,它依赖signaling server在需要建立WebRTC连接的客户端之间转发协商协议。客户端和signaling server之间的连接通常使用WebSockets

signaling server保存了客户端的信息,其工作模式如下:

  • 使用HTTP库启动一个WebSocket服务,用于监听客户端的注册(即后可以与其他客户端建立WebSocket连接)请求
  • 维护一个内存关系结构(如哈希或字典),将clientId与其WebSocket进行映射
  • 当接收到发起端的WebSocket消息(当然,必须指定clientId)时,会在map中查找接收端的注册信息,然后通过WebSocket将数据转发给接收端。

伪代码实现如下:

// Global variable that maps each clientId to its associated Websocket.
clientIdToWebsocketMap = new Map() function main() {
// Start the server and listen to registration requests.
server = new WebsocketServer() server.listen("/register", registerHandler)
} function registerHandler(request) {
// For example, the client can send their clientId as a query parameter.
clientId = request.queryParams.get("clientId") websocket = request.acceptAndConvertToWebsocket()
websocket.onReceiveMessage = onReceiveMessageHandler clientIdToWebsocketMap.insert(clientId, websocket)
} function onReceiveMessageHandler(message) {
// We assume the message parsing logic to extract the recipientId from the message is defined elsewhere.
recipientId, body = parseMessage(message)
clientIdToWebsocketMap.get(recipientId).send(body)
}

上面例子仅描述了一个signaling server,但单台signaling server的能力有限,当需要多实例时,就会遇到kubernetes中的长连接负载均衡问题。在讨论如何解决该问题之前,需要明确连个目标:

  1. 分布式约束:系统必须保证发送方的消息能够被正确转发到期望的接收方,即使二者并没有注册到相同的实例上。
  2. 均衡约束:系统在实例增加或减少的情况下必须保证负载均衡。

经典的解决方式

使用pub/sub broker来解决分布式约束

网上的大部分方式都推荐使用一个Pub/Sub broker来实现实例间的交互,如下:

这种方式可以解决分布式约束问题,但有两个关键限制:

  1. 每个signaling实例都会读取其他实例发布的消息,这会导致读取的消息数量是实例数的平方,但平均只有1/N 的消息是有效的(即被接收方所在的实例接收到),大部分消息都会被丢弃。
  2. 有可能还需要对pub/sub broker实现自动缩放功能,复杂且增加了开支。

解决均衡约束

大部分默认的负载均衡算法为round-robin,但这种方式适用于HTTP短连接,不能在自动扩缩容情况下均衡WebSocket连接。另外有一种least-connected算法,可以将WebSocket连接请求分配给具有最少active连接的实例。这种方式可以保证在扩容情况下达到最终均衡。

这种方案的问题是并不是所有的负载均衡器都支持least-connected负载均衡算法,如Nginx支持,但 GCP’s HTTP(S) 负载均衡器不支持,这种情况下可能要诉诸于比较笨拙的办法,如readiness probes:即让具有最多负载的signaling实例暂时处于Unready状态(此时endpoint controller会从所有service上移除该pod),以此来阻止负载均衡器向该实例发送新的连接请求。

我们的解决方案:使用基于哈希的负载均衡算法

使用rendezvous 希解决分布性约束

基于哈希的负载均衡算法是一种确定均衡流量的方法,根据客户端请求中的内容(如header的值、请求或路径参数以及客户端IP等)来计算哈希值。有两种著名的哈希算法: 一致性哈希rendezvous 哈希。这里我们选择了后者,原因是它更加简单,且均衡性更好。算法如下:

H(val, I) = I_i

- H is the hash-based algorithm
- val is the value (extracted from the request) from which the hash is computed
- I = {I_1, I_2, ..., I_N} is the set of all backend instances
- I_i is the backend instance that was "selected" by the algorithm

如果使用客户端的clientId作为参数val,那么就可以将每个客户端映射到特定的signaling实例上。此外,只要知道clientId和后端实例,就可以通过该函数了解到客户端和实例的对应关系,这也意味着,如果一个signaling实例接收到发起端的消息,但没有在本地找到接收端,此时就可以通过哈希算法知道接收端位于哪个实例上。下面看下具体实施步骤:

  1. 当接收到新的WebSocket连接请求时,使用请求中的clientId作为rendezvous 哈希的入参。
  2. 每个signaling实例需要了解系统中的其他实例,这可以通过kubernetes中的Headless Service关联signaling deployment,然后调用Kubernetes Endpoints API获得实例地址。
  3. 当signaling I₁从一个发起端接收到WebSocket消息时,会从请求中读取接收端的clientId,然后从本地查找接收端,如果找到,则通过WebSocket将消息转发给对端即可,如果没有找到,则使用rendezvous 哈希算法,并使用clientId作为val,signaling实例的IPs作为I,计算出接收端注册的实例I₂。如果 I₂ = I₁ ,说明接收端已经断开连接或从未注册,反之则直接将消息转发给 I₂ 。
  4. I₁ 转发给 I₂的方式有很多种,这里采用普通的HTTP请求作为实例间通信。我们采用批量发送的方式来减少HTTP请求数量。

解决均衡约束

使用基于哈希的负载均衡可以优雅地解决分布性约束,通过kubernetes Endpoint API也可以很容易地获取signaling实例的变动。rendezvous哈希的一个特点是,当添加或删除后端实例时,会改变函数的参数I,函数的返回值只会影响一部分数据(如果实例从N-1扩展为N,则平均影响1/N的数据)。

但在实例变更之后,谁去负责重新分配注册的客户端?下面有两种方式解决该问题:

1.强制客户端断开连接

当一个signaling实例Iᵢ通过kubernetes Engpoint API探测到扩缩容事件后,它会遍历本地注册的所有客户端,然后使用rendezvous哈希算法针对更新后的实例集中的每个clientId重新计算所有结果。理论上,计算出的部分新结果不属于Iᵢ,此时Iᵢ可以断开这部分客户端的WebSocket连接,如果客户端有重连机制,就会重新发起建链,当请求到达负载均衡器之后,会被分配到正确的signaling实例上。

扩容前

在扩容后,触发客户端重连

该方式比较简单,但存在一些弊端:

  1. 首先客户端需要有重连机制
  2. 其次会打断客户端会话
  3. 增加了signaling服务实现代码和周边架构的耦合
  4. 在每次扩缩容之后会增加请求峰值。

出于上述原因,我们放弃了这种方式。

2.负载均衡器本身中重新映射Websocket

这里我们自己实现了负载均衡器,但仅用于代理WebSocket的请求和消息,不处理如TLS和ALPN之类的功能(这部分由前置的负载均衡处理)。实现步骤如下:

  1. 通过kubernetes API来发现signaling实例,并实现rendezvous哈希逻辑。
  2. 配置一个基本的Websocket服务监听连接请求,并根据rendezvous哈希计算(客户端的clientId)的结果将请求路由到后端signaling实例,最后将响应返回给客户端。如果返回结果有效,则与该客户端创建两条WebSocket连接:一条从客户端到负载均衡器,另一条从负载均衡器到signaling实例。
  3. 当负载均衡器从 客户端-复杂均衡器 的WebSocket上接收到消息后,它会通过 负载均衡器-signaling 进行转发,反之亦然。
  4. 最后根据扩缩容实现WebSocket的映射逻辑:当负载均衡器通过kubernetes API检测到signaling实例变动时,它会遍历所有客户端及其当前代理Websocket的clientId,然后使用rendezvous哈希算法并代入新的后端实例重新计算结果。当返回的实例与当前客户端注册的不一致,则负载均衡器只会断开与该客户端相关的 负载均衡器-signaling 之间的WebSocket,并重新建立一条到正确的signaling实例的 负载均衡器-signaling 链接。

总结

文中最后使用自实现的负载均衡器来缓解后端实例扩缩容对客户端的影响。需要注意的是,rendezvous哈希算法在扩容场景下不大友好,需要重新计算所有key(文中为clientId)的哈希值,因此在数据量大的情况下会造成一定的性能问题,因此适合数据量减小或缓存场景。

参考

如何在kubernetes中实现分布式可扩展的WebSocket服务架构的更多相关文章

  1. 如何在Python中使用ZeroMQ和Docker构建微服务架构

    @Container容器技术大会将于6月4日在上海光大会展中心国际大酒店举办,来自携程.PPTV.蚂蚁金服.京东.浙江移动.海尔电器.唯品会.eBay.道富银行.麻袋理财等公司的技术负责人将带来实践经 ...

  2. Kubernetes入门(四)——如何在Kubernetes中部署一个可对外服务的Tensorflow机器学习模型

    机器学习模型常用Docker部署,而如何对Docker部署的模型进行管理呢?工业界的解决方案是使用Kubernetes来管理.编排容器.Kubernetes的理论知识不是本文讨论的重点,这里不再赘述, ...

  3. (五):C++分布式实时应用框架——微服务架构的演进

    C++分布式实时应用框架--微服务架构的演进 上一篇:(四):C++分布式实时应用框架--状态中心模块 版权声明:本文版权及所用技术归属smartguys团队所有,对于抄袭,非经同意转载等行为保留法律 ...

  4. 如何在PHP7中安装mysql的扩展

    相对与PHP5,PHP7的最大变化之一是移除了mysql扩展,推荐使用mysqli或者pdo_mysql,实际上在PHP5.5开始,PHP就着手开始准备弃用mysql扩展,如果你使用mysql扩展,可 ...

  5. Ionic中基于js的扩展(指令和服务)来实现各种效果

    1.ion-header-bar ion-footer-bar ion-content align-title='left/ritght/center <body> <ion-hea ...

  6. 实例演示:如何在Kubernetes上大规模运行CI/CD

    本周四晚上8:30,第二期k3s在线培训如约开播!本期课程将介绍k3s的核心架构,如高可用架构以及containerd.一起来进阶探索k3s吧! 报名及观看链接:http://z-mz.cn/PmwZ ...

  7. 使用Kubernetes中的Nginx来改善第三方服务的可靠性和延迟

    使用Kubernetes中的Nginx来改善第三方服务的可靠性和延迟 译自:How we improved third-party availability and latency with Ngin ...

  8. 在Kubernetes中部署GlusterFS+Heketi

    目录 简介 Gluster-Kubernetes 部署 环境准备 下载相关文件 部署glusterfs 部署heketi server端 配置heketi client 简介 在上一篇<独立部署 ...

  9. 深入解析kubernetes中的选举机制

    Overview 在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumt ...

  10. 【CHRIS RICHARDSON 微服务系列】微服务架构中的进程间通信-3

    编者的话 |本文来自 Nginx 官方博客,是微服务系列文章的第三篇,在第一篇文章中介绍了微服务架构模式,与单体模式进行了比较,并且讨论了使用微服务架构的优缺点.第二篇描述了采用微服务架构的应用客户端 ...

随机推荐

  1. ABP - 依赖注入(2)

    依赖注入的使用 构造方法注入 这是将服务注入类的最常用方法,是将依赖项注入类的首选方式,也是微软推崇的模式.这样,除非提供了所有构造方法注入的依赖项,否则无法构造类,显示的声明了类必需的服务,使开发人 ...

  2. 沉痛悼念 pip search 一路走好

    不知道最近大家有没有发现在使用 pip search 的时候,总是出现一个 XMLRPC 的报错. $ pip search xlrdERROR: XMLRPC request failed [cod ...

  3. 20230611 再次升级SSD

    家里常用电脑的硬盘又显得捉襟见肘,老规矩,升级SSD.幸亏几年前摸索的方法记录下来了,翻出以前的博客复习一下.为了保险起见,也重新在网上搜了一下,看是不是有新的更方便的方法,答案是没有,只是搜出很多推 ...

  4. Spring事件监听机制使用和原理解析

    你好,我是刘牌! 前言 好久没有更新Spring了,今天来分享一下Spring的事件监听机制,之前分享过一篇Spring监听机制的使用,今天从原理上进行解析,Spring的监听机制基于观察者模式,就是 ...

  5. 1.redis常见数据类型-字符串String、列表List、集合Set、Hash哈希、Zset有序集合

    背景: 这里说的数据类型是value的数据类型,key的类型都是字符串. 命令不区分大小写,而key的值是区分大小写的 help @+数据类型 会出现命令提示 比如 help@string,help@ ...

  6. win10搜索框的热门搜索怎么关闭?

    搜索cmd,以管理员模式运行,输入下列命令,重启生效 reg add HKCU\Software\Policies\Microsoft\Windows\explorer /v DisableSearc ...

  7. iOS CoreData总结

    相关主要类: NSManagedObjectContext 管理对象,上下文,持久性存储模型对象,处理数据与应用的交互 NSManagedObjectModel 被管理的数据模型,数据结构 NSPer ...

  8. hexo博客yilia主题_more截断文章_多标签添加

    以下均为自己遇到的问题并加以修改或者纠正. 在文章下方可以使用more语句进行截断,这样博客首页只会出现文章的前面一小部分,看起来很清爽简约 或者 language: zh-CN <!--mor ...

  9. 【原创】CPU性能优化小记

    CPU性能优化小记 目录 CPU性能优化小记 一.现象 TOP各指标含义 二.分析 启动应用前 启动应用后 采集内核函数的方法 内核采集分析 火焰图分析 三.解决 一.现象 业务线反馈,单板只要一跑我 ...

  10. Flutter ncnn 使用

    Flutter 实现手机端 App,如果想利用 AI 模型添加新颖的功能,那么 ncnn 就是一种可考虑的手机端推理模型的框架. 本文即是 Flutter 上使用 ncnn 做模型推理的实践分享.有如 ...