如何在kubernetes中实现分布式可扩展的WebSocket服务架构
如何在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中的长连接负载均衡问题。在讨论如何解决该问题之前,需要明确连个目标:
- 分布式约束:系统必须保证发送方的消息能够被正确转发到期望的接收方,即使二者并没有注册到相同的实例上。
- 均衡约束:系统在实例增加或减少的情况下必须保证负载均衡。
经典的解决方式
使用pub/sub broker来解决分布式约束
网上的大部分方式都推荐使用一个Pub/Sub broker来实现实例间的交互,如下:

这种方式可以解决分布式约束问题,但有两个关键限制:
- 每个signaling实例都会读取其他实例发布的消息,这会导致读取的消息数量是实例数的平方,但平均只有1/N 的消息是有效的(即被接收方所在的实例接收到),大部分消息都会被丢弃。
- 有可能还需要对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实例接收到发起端的消息,但没有在本地找到接收端,此时就可以通过哈希算法知道接收端位于哪个实例上。下面看下具体实施步骤:
- 当接收到新的WebSocket连接请求时,使用请求中的
clientId作为rendezvous 哈希的入参。 - 每个signaling实例需要了解系统中的其他实例,这可以通过kubernetes中的Headless Service关联signaling deployment,然后调用Kubernetes Endpoints API获得实例地址。
- 当signaling I₁从一个发起端接收到WebSocket消息时,会从请求中读取接收端的
clientId,然后从本地查找接收端,如果找到,则通过WebSocket将消息转发给对端即可,如果没有找到,则使用rendezvous 哈希算法,并使用clientId作为val,signaling实例的IPs作为I,计算出接收端注册的实例I₂。如果 I₂ = I₁ ,说明接收端已经断开连接或从未注册,反之则直接将消息转发给 I₂ 。 - 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实例上。

扩容前

在扩容后,触发客户端重连
该方式比较简单,但存在一些弊端:
- 首先客户端需要有重连机制
- 其次会打断客户端会话
- 增加了signaling服务实现代码和周边架构的耦合
- 在每次扩缩容之后会增加请求峰值。
出于上述原因,我们放弃了这种方式。
2.负载均衡器本身中重新映射Websocket
这里我们自己实现了负载均衡器,但仅用于代理WebSocket的请求和消息,不处理如TLS和ALPN之类的功能(这部分由前置的负载均衡处理)。实现步骤如下:
- 通过kubernetes API来发现signaling实例,并实现rendezvous哈希逻辑。
- 配置一个基本的Websocket服务监听连接请求,并根据rendezvous哈希计算(客户端的clientId)的结果将请求路由到后端signaling实例,最后将响应返回给客户端。如果返回结果有效,则与该客户端创建两条WebSocket连接:一条从客户端到负载均衡器,另一条从负载均衡器到signaling实例。
- 当负载均衡器从 客户端-复杂均衡器 的WebSocket上接收到消息后,它会通过 负载均衡器-signaling 进行转发,反之亦然。
- 最后根据扩缩容实现WebSocket的映射逻辑:当负载均衡器通过kubernetes API检测到signaling实例变动时,它会遍历所有客户端及其当前代理Websocket的
clientId,然后使用rendezvous哈希算法并代入新的后端实例重新计算结果。当返回的实例与当前客户端注册的不一致,则负载均衡器只会断开与该客户端相关的 负载均衡器-signaling 之间的WebSocket,并重新建立一条到正确的signaling实例的 负载均衡器-signaling 链接。

总结
文中最后使用自实现的负载均衡器来缓解后端实例扩缩容对客户端的影响。需要注意的是,rendezvous哈希算法在扩容场景下不大友好,需要重新计算所有key(文中为clientId)的哈希值,因此在数据量大的情况下会造成一定的性能问题,因此适合数据量减小或缓存场景。
参考
如何在kubernetes中实现分布式可扩展的WebSocket服务架构的更多相关文章
- 如何在Python中使用ZeroMQ和Docker构建微服务架构
@Container容器技术大会将于6月4日在上海光大会展中心国际大酒店举办,来自携程.PPTV.蚂蚁金服.京东.浙江移动.海尔电器.唯品会.eBay.道富银行.麻袋理财等公司的技术负责人将带来实践经 ...
- Kubernetes入门(四)——如何在Kubernetes中部署一个可对外服务的Tensorflow机器学习模型
机器学习模型常用Docker部署,而如何对Docker部署的模型进行管理呢?工业界的解决方案是使用Kubernetes来管理.编排容器.Kubernetes的理论知识不是本文讨论的重点,这里不再赘述, ...
- (五):C++分布式实时应用框架——微服务架构的演进
C++分布式实时应用框架--微服务架构的演进 上一篇:(四):C++分布式实时应用框架--状态中心模块 版权声明:本文版权及所用技术归属smartguys团队所有,对于抄袭,非经同意转载等行为保留法律 ...
- 如何在PHP7中安装mysql的扩展
相对与PHP5,PHP7的最大变化之一是移除了mysql扩展,推荐使用mysqli或者pdo_mysql,实际上在PHP5.5开始,PHP就着手开始准备弃用mysql扩展,如果你使用mysql扩展,可 ...
- Ionic中基于js的扩展(指令和服务)来实现各种效果
1.ion-header-bar ion-footer-bar ion-content align-title='left/ritght/center <body> <ion-hea ...
- 实例演示:如何在Kubernetes上大规模运行CI/CD
本周四晚上8:30,第二期k3s在线培训如约开播!本期课程将介绍k3s的核心架构,如高可用架构以及containerd.一起来进阶探索k3s吧! 报名及观看链接:http://z-mz.cn/PmwZ ...
- 使用Kubernetes中的Nginx来改善第三方服务的可靠性和延迟
使用Kubernetes中的Nginx来改善第三方服务的可靠性和延迟 译自:How we improved third-party availability and latency with Ngin ...
- 在Kubernetes中部署GlusterFS+Heketi
目录 简介 Gluster-Kubernetes 部署 环境准备 下载相关文件 部署glusterfs 部署heketi server端 配置heketi client 简介 在上一篇<独立部署 ...
- 深入解析kubernetes中的选举机制
Overview 在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumt ...
- 【CHRIS RICHARDSON 微服务系列】微服务架构中的进程间通信-3
编者的话 |本文来自 Nginx 官方博客,是微服务系列文章的第三篇,在第一篇文章中介绍了微服务架构模式,与单体模式进行了比较,并且讨论了使用微服务架构的优缺点.第二篇描述了采用微服务架构的应用客户端 ...
随机推荐
- 代码随想录算法训练营Day18 二叉树
代码随想录算法训练营 代码随想录算法训练营Day18 二叉树| 513.找树左下角的值 112. 路径总和 113.路径总和ii 106.从中序与后序遍历序列构造二叉树 105.从前序与中序遍历序列构 ...
- ODOO配置属性
2字段的属性 2.1 隐藏字段 <field name='model_name' invisible="True"/> 2.2 条件下隐藏 <field name ...
- uniapp主题切换功能的第二种实现方式(scss变量+require)
在上一篇 "uniapp主题切换功能的第一种实现方式(scss变量+vuex)" 中介绍了第一种如何切换主题,但我们总结出一些不好的地方,例如扩展性不强,维护起来也困难等等,那么接 ...
- 声音克隆,精致细腻,人工智能AI打造国师“一镜到底”鬼畜视频,基于PaddleSpeech(Python3.10)
电影<满江红>上映之后,国师的一段采访视频火了,被无数段子手恶搞做成鬼畜视频,诚然,国师的这段采访文本相当经典,他生动地描述了一个牛逼吹完,大家都信了,结果发现自己没办法完成最后放弃,随后 ...
- SpringBoot集成支付宝 - 少走弯路就看这篇
最近在做一个网站,后端采用了SpringBoot,需要集成支付宝进行线上支付,在这个过程中研究了大量支付宝的集成资料,也走了一些弯路,现在总结出来,相信你读完也能轻松集成支付宝支付. 在开始集成支付宝 ...
- 常用的Java Enum JdbcType
常用的Java Enum JdbcType ARRAY BIGINT BINARY BIT BLOB BOOLEAN CHAR CLOB CURSOR DATE DECIMAL DOUBLE FLOA ...
- 基于瑞芯微camera文章汇总pdf分享给大家
前面一段时间基于瑞芯微3568平台编写了camera系列文章, 一共12篇,目前更新完毕. <瑞芯微文章合集> camera系列文章编写前后一共花费2个月时间, 期间查阅了很多资料,做了很 ...
- JavaCV人脸识别三部曲之一:视频中的人脸保存为图片
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于人脸识别 本文是<JavaCV人脸识别三部曲 ...
- SSIS向MySQL目标(Destination)写数据--Step By Step
前言(废话) 最近的工作中涉及到SQLSERVER向MySQL的数据迁移同步,团队中理所当然准备用开发C#微服务接口的方式实现,我觉得这个路子曲折了,推荐SSIS的方式并自告奋勇接下了这个活.不过以前 ...
- Redis数据类型之Stream系列一
一:Stream简介 Redis Stream是5.0版本之后新增的一种数据结构,其结构类似于'仅追加日志'.但也实现了多种操作来克服'仅追加日志'的一些限制,如读取策略(xread,xrange ...