专栏首页运维一切kubernetes 下实现socket.io 的集群模式

kubernetes 下实现socket.io 的集群模式

socket.io 单节点模式是很容易部署的,但是往往在生产环境一个节点不能满足业务需求,况且还要保证节点挂掉的情况仍能正常提供服务,所以多节点模式就成为了生成环境的一种必须的部署模式。

本文将介绍如何在kubernetes 集群上部署多节点的socket.io服务。

文章中涉及到的代码可以前往https://github.com/cnych/k8s-socketio-cluster-demo查看。

问题

现在正在准备将线上环境一步步迁移到kubernetes 集群上,这样我们可以根据实际情况部署多个POD 来提供服务,但是socket.io服务并不是单纯的无状态应用,只需要将POD 部署成多个就可以正常提供服务了,因为其底层需要建立很多连接来保持长连接,但是这样的话上一个请求可能会被路由到一个POD,下一个请求则很有可能会被路由到另外一个POD 中去了,这样就会出现错误了,如下图:

socket-io errors

从上面的错误中我们可以看出是有的请求找不到对应的Session ID,也证明了上面提到的引起错误的原因。

解决方法

我们从socket.io 官方文档中可以看到对于多节点的介绍,其中通过Nginxip_hash 配置用得比较多,同一个ip 访问的请求通过hash 计算过后会被路由到相同的后端程序去,这样就不会出现上面的问题了。我们这里是部署在kubernetes集群上面的,通过traefik ingress来连接外部和集群内部间的请求的,所以这里中间就省略了Nginx这一层,当然你也可以多加上这一层,但是这样显然从架构上就冗余了,而且还有更好的解决方案的:sessionAffinity(也称会话亲和力)

什么是sessionAffinity? sessionAffinity是一个功能,将来自同一个客户端的请求总是被路由回服务器集群中的同一台服务器的能力。

kubernetes中启用sessionAffinity很简单,只需要简单的Service中配置即可:

service.spec.sessionAffinity = "ClientIP"

默认情况下sessionAffinity=None,会随机选择一个后端进行路由转发的,设置成ClientIP后就和上面的ip_hash功能一样了,由于我们使用的是traefik ingress,这里还需要在Service中添加一个traefikannotation

apiVersion: v1
kind: Service
metadata:
  name: socket-demo
  namespace: kube-apps
  annotations:
    traefik.backend.loadbalancer.stickiness: "true"
    traefik.backend.loadbalancer.stickiness.cookieName: "socket"
  labels:
    k8s-app: socket-demo
spec:
  sessionAffinity: "ClientIP"
  ports:
    - name: socketio
      port: 80
      protocol: TCP
      targetPort: 3000
  selector:
    k8s-app: socket-demo

注意上面的annotationssessionAffinity两项配置,然后我们再来看看我们的socket.io服务吧

已经正常了吧,注意看上面打印出来的hostname都是一样的,因为我们这里去访问的都是来自同一个IP,多刷新几次是不是还是这样,证明上面的sessionAffinity配置生效了。

如果是另外的地方去访问,会路由到不一样的后端去吗?我们这里启用一个代理来测试下:

从上图中打印出来的hostname可以看出两个请求被路由到了不同的POD 中,但是现在又有一个新的问题了:绘制的图形并没有被广播出去,这是为什么呢?其实在上面提到的socket.io 官方文档中已经提到过了

Now that you have multiple Socket.IO nodes accepting connections, if you want to broadcast events to everyone (or even everyone in a certain room) you’ll need some way of passing messages between processes or computers. The interface in charge of routing messages is what we call the Adapter. You can implement your own on top of the socket.io-adapter (by inheriting from it) or you can use the one we provide on top of Redis: socket.io-redis:

var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));

总结起来就是你如果想在进程间或者节点之前发送信息,那么就需要实现自己实现一个socket.io-adapter或者利用官方提供的socket.io-redis

我们这里利用socket.io-redis 这个adapter 来实现消息的广播,最终的服务端代码如下:

const express = require('express');
const socketRedis = require('socket.io-redis');
const os = require('os');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;

app.use(express.static(__dirname + '/static'));

app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');

app.get('/', function (req, res) {
    res.render('index', {
        'os': os.hostname()
    });
});

function onConnection(socket){
  socket.on('drawing', (data) => socket.broadcast.emit('drawing', data));
}

io.adapter(socketRedis({host: 'redis', port: 6379}));
io.on('connection', onConnection);

http.listen(port, () => console.log('listening on port ' + port));

部署在kubernetes集群上的yaml文件如下:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: socket-demo
  namespace: kube-apps
  labels:
    k8s-app: socket-demo
spec:
  replicas: 3
  template:
    metadata:
      labels:
        k8s-app: socket-demo
    spec:
      containers:
        - image: cnych/socketdemo:k8s
          imagePullPolicy: Always
          name: socketdemo
          ports:
            - containerPort: 3000
              protocol: TCP
          resources:
            limits:
              cpu: 100m
              memory: 100Mi
            requests:
              cpu: 50m
              memory: 50Mi

---
apiVersion: v1
kind: Service
metadata:
  name: socket-demo
  namespace: kube-apps
  annotations:
    traefik.backend.loadbalancer.stickiness: "true"
    traefik.backend.loadbalancer.stickiness.cookieName: "socket"
  labels:
    k8s-app: socket-demo
spec:
  sessionAffinity: None
  ports:
    - name: socketio
      port: 80
      protocol: TCP
      targetPort: 3000
  selector:
    k8s-app: socket-demo

现在看看最终的效果吧:

不同节点间也可以传递数据了,到这里我们就实现了在kubernetes集群下部署socket.io多节点。

上面的根据traefik.backend.loadbalancer.stickiness.cookieName来进行路由的规则在测试环境生效了,在线上没生效,可能这个地方有什么问题? 上面没有生效是因为客户端连接socket.io的协议的时候没有使用polling造成的,客户端连接socket.io要按照标准的方式指定trasports=[‘polling’, ‘websocket’] sessionAffinity 与 traefik设置cookieName的方式貌似不能同时存在,如果遇到不生效的,将sessionAffinity设置为None ,只保留traefik的annotaions。 在使用socket.io-redis的时候一定要注意,在joinleave房间的时候一定要使用adapter提供的remoteJoinremoteLeave方法,不然多个节点间的数据同步有问题,这个被坑了好久……

(adsbygoogle = window.adsbygoogle || []).push({});

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Redis源码分析2:Redis的事件处理模型

    上一篇分析了一下redis的大致框架和启动过程,这篇我想分析一下redis的事件处理模型,当然也包含了网络事件的处理模型。redis除了其高效的nosql存储非...

    黑光技术
  • Redis的事件处理模型2:客户端命令执行过程

    上篇分析了Redis的事件处理模型,了解了Redis的事件处理基本过程,这篇还想继续顺着上面的思路分析一下这种情况:在客户端向服务端发起一个set指令或者get...

    黑光技术
  • 【高并发】redis队列缓存 + mysql 批量入库 + php离线整合

    需求背景:有个 调用统计日志存储和统计需求 ,要求存储到mysql中;存储数据高峰能达到日均千万,瓶颈在于 直接入库并发太高,可能会把mysql干垮 。

    猿哥
  • docker搭建php+nginx+swoole+mysql+redis环境

    软件:docker-ce version 18.09.3, docker-compose version 1.23.2

    猿哥
  • Spring Boot 2.x(十四):整合Redis,看这一篇就够了

    Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 ...

    Vi的技术博客
  • Redis在项目中合理使用经验总结

    key: active:spring2019:title value:"2019春节活动" 操作:set

    猿哥
  • 谈谈Java任务的并行处理

    谈到并行,我们可能最先想到的是线程,多个线程一起运行,来提高我们系统的整体处理速度;为什么使用多个线程就能提高处理速度,因为现在计算机普遍都是多核处理器,我们需...

    本人秃顶程序员
  • “大”事务引起的锁等待分析案例

    生产环境数据库在某一刻突然发现大量活跃连接,而且大部分状态是updating。问题出现在周六上午,持续了大概三、四分钟,得益于我们自己的快照程序,拿到了当时现场...

    wubx
  • 如何扩展Laravel

    Laravel中的很多组件都使用了Manager,如:Auth、Cache、Log、Notification、Queue、Redis等等,每个组件都有一个xxx...

    猿哥
  • 双非本科没有项目经历,如何进入BATJ等一线公司?

    没有项目经历,如何找到大厂实习?这个问题是有读者后台留言问过的,相信这个也是很多人的困惑,希望这篇内容能对你有所帮助。

    乔戈里

扫码关注云+社区

领取腾讯云代金券