1. 程式人生 > >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

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服務吧

socket.io已經正常了吧,注意看上面打印出來的hostname都是一樣的,因為我們這裡去訪問的都是來自同一個IP,多重新整理幾次是不是還是這樣,證明上面的sessionAffinity配置生效了。

如果是另外的地方去訪問,會路由到不一樣的後端去嗎?我們這裡啟用一個代理來測試下:socket.io從上圖中打印出來的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

現在看看最終的效果吧:socket.io cluster

不同節點間也可以傳遞資料了,到這裡我們就實現了在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方法,不然多個節點間的資料同步有問題,這個被坑了好久