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

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 如何在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)的哈希值,因此在数据量大的情况下会造成一定的性能问题,因此适合数据量减小或缓存场景。

参考

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
15天前
|
弹性计算 人工智能 Serverless
阿里云ACK One:注册集群云上节点池(CPU/GPU)自动弹性伸缩,助力企业业务高效扩展
在当今数字化时代,企业业务的快速增长对IT基础设施提出了更高要求。然而,传统IDC数据中心却在业务存在扩容慢、缩容难等问题。为此,阿里云推出ACK One注册集群架构,通过云上节点池(CPU/GPU)自动弹性伸缩等特性,为企业带来全新突破。
|
13天前
|
存储 Kubernetes 调度
|
7天前
|
运维 Kubernetes Docker
利用Docker和Kubernetes构建微服务架构
利用Docker和Kubernetes构建微服务架构
|
5天前
|
监控 持续交付 Docker
Docker 容器化部署在微服务架构中的应用有哪些?
Docker 容器化部署在微服务架构中的应用有哪些?
|
5天前
|
监控 持续交付 Docker
Docker容器化部署在微服务架构中的应用
Docker容器化部署在微服务架构中的应用
|
7天前
|
Kubernetes API 调度
【赵渝强老师】Kubernetes的体系架构
本文介绍了Kubernetes的体系架构及其核心组件。Kubernetes采用主从分布式架构,由master主节点和多个node工作节点组成。master节点负责集群管理和调度,运行API Server、scheduler、controller-manager等服务组件;node节点运行kubelet、kube-proxy和Docker容器守护进程,负责实际业务应用的运行。文章还简要介绍了Kubernetes的附加组件及其作用。
|
5天前
|
安全 持续交付 Docker
微服务架构和 Docker 容器化部署的优点是什么?
微服务架构和 Docker 容器化部署的优点是什么?
|
6天前
|
存储 监控 Docker
探索微服务架构下的容器化部署
本文旨在深入探讨微服务架构下容器化部署的关键技术与实践,通过分析Docker容器技术如何促进微服务的灵活部署和高效管理,揭示其在现代软件开发中的重要性。文章将重点讨论容器化技术的优势、面临的挑战以及最佳实践策略,为读者提供一套完整的理论与实践相结合的指导方案。
|
12天前
|
监控 前端开发 JavaScript
探索微前端架构:构建可扩展的现代Web应用
【10月更文挑战第29天】本文探讨了微前端架构的核心概念、优势及实施策略,通过将大型前端应用拆分为多个独立的微应用,提高开发效率、增强可维护性,并支持灵活的技术选型。实际案例包括Spotify和Zalando的成功应用。
|
9天前
|
运维 Kubernetes Cloud Native
Kubernetes云原生架构深度解析与实践指南####
本文深入探讨了Kubernetes作为领先的云原生应用编排平台,其设计理念、核心组件及高级特性。通过剖析Kubernetes的工作原理,结合具体案例分析,为读者呈现如何在实际项目中高效部署、管理和扩展容器化应用的策略与技巧。文章还涵盖了服务发现、负载均衡、配置管理、自动化伸缩等关键议题,旨在帮助开发者和运维人员掌握利用Kubernetes构建健壮、可伸缩的云原生生态系统的能力。 ####