在云中扩展 Websocket(第 1 部分)。从 Socket.io 和 Redis 到 Docker 和 Kubernetes 的分布式架构
在这篇文章中,我们将介绍 Websockets 的几种配置。假设从零基础架构到相当复杂的集群配置。
简介
Websocket 是一种广泛传播的协议,允许通过 TCP 进行全双工通信。有几个库实现了该协议,其中最强大和最著名的库之一是 Javascript 端的Socket.io
,它允许快速创建实时通信模式。
简单的端到端通信
使用Socket.io
库本身的任何示例,在NodeJS
中创建一个可以连接多个客户端的单个服务器非常简单和直接。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--Dm7KFqD---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com /sw360cab/websockets-scaling/master/diagrams/end_to_end_singleserver.png)
该库的主要功能之一是它可以将 websocket 包装在 http 服务器中。
例如,第一种方法可以是,一旦客户端连接到 websocket,服务器应用程序就会记录该消息并可能在其上等待特定的topic
:
// Server
const socketServer = require('http').createServer();
const io = require('socket.io')(socketServer, {});
const socketPort = 5000;
io.on('connection', client => {
console.log('New incoming Connection from', client.id);
client.on('testMsg', function(message) {
console.log('Message from the client:',client.id,'->',message);
})
});
socketServer.listen(socketPort);
// Client
const io = require('socket.io-client');
const client = io('http://localhost:5000', {
transports: ['websocket']
});
client.on('connect', function(){
console.log("Connected!");
client.emit('testMsg', 'a message');
});
进入全屏模式 退出全屏模式
客户端连接后的服务器将等待testMsg
类型的消息。
客户端被配置为使用本机 websockets,而不是尝试像 http 轮询那样简单的解决方案。
{ transports: ['websocket'] }
进入全屏模式 退出全屏模式
服务器还能够向连接到 websocket 的所有客户端广播消息。
io.emit("brdcst", 'A broadcast message');
进入全屏模式 退出全屏模式
Tip
:这些消息将被所有连接的客户端接收。
多服务器到多客户端通信
前面的例子既简单又有趣。我们可以有效地使用它。但是创建一个可扩展的多服务器架构是另一回事,并不像之前的案例那么直接。
有两个主要问题需要考虑:
1.涉及的多个websocket服务器之间应该相互协调。一旦服务器从客户端接收到消息,它应该确保连接到任何服务器的任何客户端都接收到该消息。
2.当客户端与服务器握手并建立连接时,它所有的未来消息都应该通过同一个服务器,否则另一个服务器将在接收到它们时拒绝进一步的消息。
幸运的是,这两个问题并不像看起来那么困难。
第一个问题可以通过使用Adapter
本地解决。这种 Socket.io 机制,本地是 in-memory,允许在进程(服务器)之间传递消息并将事件广播到所有客户端。
最适合多服务器场景的适配器是socket.io-redis,利用Redis
的 pub/sub 范式。
正如预期的那样,配置简单流畅,只需要一小段代码。
const redisAdapter = require('socket.io-redis');
const redisHost = process.env.REDIS_HOST || 'localhost';
io.adapter(redisAdapter({ host: redisHost, port: 6379 }));
进入全屏模式 退出全屏模式
保持会话由一个客户端与同一源服务器初始化的第二个问题可以在没有特别痛苦的情况下解决。诀窍在于创建sticky
个连接,以便当客户端连接到特定服务器时,它会开始一个有效绑定到同一服务器的会话。
这不能直接实现,但我们应该将 something 放在 NodeJS 服务器应用程序的前面。
这通常是Reverse Proxy
,如 NGINX、HAProxy、Apache Httpd。在 HAProxy 的情况下,假设 websocket 服务器应用程序的两个副本,backend 部分的配置将是:
cookie io prefix indirect nocache
server ws0 socket-server:5000 check cookie ws0
server ws1 socket-server2:5000 check cookie ws1
进入全屏模式 退出全屏模式
它在握手时设置 io 会话 cookie。
多服务器回顾
总结一下您对这个配置的期望:
-
每个客户端都会与特定的 websocket 应用服务器实例建立连接。
-
从客户端发送的任何消息总是通过与初始化会话的服务器相同的。
-
服务器收到消息后,可以广播它。适配器负责通告所有其他服务器,这些服务器将依次将消息转发给与它们建立连接的所有客户端。
容器化一切:微服务的 Docker 堆栈
到目前为止,我们所看到的可以总结为下图:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--DxtykuUC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/ sw360cab/websockets-scaling/master/diagrams/multi_server_proxied.png)
自然演变是一堆微服务容器化为 Docker 镜像。我们之前的场景包含 3 个可以容器化的构建块:
-
Websocket 服务器应用程序(1+ 副本)
-
Redis(发布/订阅适配器)
-
HAProxy(作为反向代理处理粘性连接)
对于第一个服务,我创建了一个预定义的多阶段Docker 映像,从NodeJS
本机映像的alpine
变体开始。其他两个微服务将使用本机图像。
这 3 个服务可以利用docker-compose
进行部署。它们的配置可以如下:
version: '3.2'
services:
socket-server:
image: sw360cab/wsk-base:0.1.1
container_name: socket-server
restart: always
deploy:
replicas: 2
environment:
- "REDIS_HOST=redis"
proxy:
image: haproxy:1
container_name: proxy
ports:
- 5000:80
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
depends_on:
- socket-server
redis:
image: redis:5.0
container_name: redis
进入全屏模式 退出全屏模式
deploy
部分仅适用于Swarm
堆栈部署。假设您已准备好Swarm
,则可以使用以下命令开始部署:
docker stack deploy --compose-file stack/docker-compose.yml wsk
进入全屏模式 退出全屏模式
但是,前面的选项 (deploy) 在Swarm
之外无效,例如在本地开发环境中。
在这种情况下,您可以使用docker-compose
基本命令。可以手动添加 websocket 服务器应用程序的额外副本。
socket-server2:
image: sw360cab/wsk-base:0.1.1
container_name: socket-server2
restart: always
environment:
- "REDIS_HOST=redis"
进入全屏模式 退出全屏模式
Tip
:另一种优雅的实现方式是使用docker-compose up -d --scale socket-server=2 socket-server
现在把所有的东西都拿出来就足够了:
docker-compose up -d
进入全屏模式 退出全屏模式
Tip
:使用HAProxy
时,请记住在配置文件中定义正确的端点主机名。
backend bk_socket
http-check expect status 200
cookie io prefix indirect nocache
# Using the `io` cookie set upon handshake
server ws0 socket-server:5000 check cookie ws0
# Uncomment following line for non-Swarm usage
#server ws1 socket-server2:5000 check cookie ws1
进入全屏模式 退出全屏模式
Tip
:引入_Reverse Proxy_后,请记住调整客户端将尝试连接的端点主机名和端口。
编排越来越多:运行中的 Kubernetes 集群
到目前为止,我们已经解决了多服务器案例的问题。我们还在 Docker 中实现了一个容器化的微服务堆栈。
现在我们可以进一步添加步骤,我们可以编排所有微服务,将它们转换为Kubernetes
(K8s) 资源并在集群中运行。
从 Docker 的转变真的很容易。首先我们可以删除反向代理服务,稍后我们会看到原因。然后我们将每个服务转化为一对服务和部署。服务将是接收请求的接口,而部署将维护一组运行应用程序的 pod(如果您不了解 K8s 术语,请将每个 pod 想象成一个容器):在这种情况下,Websocket 应用程序或 Redis .
服务收到的每个请求都将转发到部署定义的副本集。
假设您有一个 K8s 集群设置(我建议K3s或Kind用于开发目的),我们将拥有:
-
Redis 服务和部署(为了在repo 中添加更多的糖我采用了 Master-Slave Redis 解决方案)
-
Websocket应用服务及部署(后者由NodeJS应用的自定义镜像的3个副本组成)
我们将要实现的新架构可以总结如下:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--pajgTrGU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/ sw360cab/websockets-scaling/master/diagrams/clustered_k8s_nodeport.png)
Websocket Kubernetes 服务
正如我们之前在向 Kubernetes 的转变中看到的那样,我们跳过了反向代理部分 (HAProxy)。
这完全是故意的,因为创建 Kubernetes 服务可以直接实现这种形式的代理。
虽然 Kubernetes Service 可以通过多种方式暴露在集群外部(NodePort、LoadBalancer、Ingress,在此处检查),但在这种情况下,我决定采用一种简单的方式。实际上,使用NodePort
允许在集群外部公开一个特定端口,客户端将连接到该端口。
apiVersion: v1
kind: Service
metadata:
name: wsk-svc
spec:
selector:
app: wsk-base
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10
ports:
- protocol: TCP
port: 3000
targetPort: 5000
nodePort: 30000
type: NodePort
进入全屏模式 退出全屏模式
服务的关键点是sessionAffinity: ClientIP
,它将模拟服务的粘性连接。
Tip
:通常使用 NodePort 之类的简单解决方案无法保证 Pod 中的Round Robin
。为了模仿循环策略,使用了之前配置的这一部分:
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10
进入全屏模式 退出全屏模式
可以使用简单的kubectl
命令建立集群:
# Launch Redis Master/Slave Deployment and Service
kubectl create -f k8s/redis
# Launch Websocket Deployment and Service
kubectl create -f k8s/wsk
进入全屏模式 退出全屏模式
客户陷阱
处理这种新架构将导致客户端发生轻微变化:
-
应根据集群配置调整端点。在上述本地集群具有 NodePort 服务的情况下,端点将是 http://localhost:30000
-
依赖 K8s 编排意味着可以透明地重新调度 Pod。因此,一个 pod 可能会突然终止,这会给客户端带来困难。
但是,如果我们向客户端添加reconnection
个策略,一旦连接丢失,它将重新连接到部署维护的副本集中的第一个可用 pod。
这是新的客户端配置:
const io = require('socket.io-client')
const client = io('http://localhost:30000', {
reconnection: true,
reconnectionDelay: 500,
transports: ['websocket']
});
进入全屏模式 退出全屏模式
结论
在这一部分中,我们从头开始,我们进入了一个完全集群的架构。在下一章中,我们将看到我们可以实现更复杂的解决方案。
Note
:这部分会在repository中对应haproxy
分支
参考文献
-
sw360cab/websockets-scaling:通过 Docker Swarm 和 Kubernetes 扩展 Websocket 的教程
-
缩放 Websockets 教程
-
Socket.io - 使用多个节点
-
使用 Nginx 和 Redis 扩展 Node.js 套接字服务器
-
在 Kubernetes 上部署实时通知系统
更多推荐
所有评论(0)