關(guān)于socket.io服務(wù)器集群

基本環(huán)境介紹

由于每個(gè)socket.io服務(wù)器上限就是保持5000個(gè)連接數(shù),考慮到大用戶量的情況,需要用到一定數(shù)量的socket.io服務(wù)器,專門用來保持和用戶的持久化連接,以便以后可以滿足消息的推送功能。服務(wù)器數(shù)量多了起來,由于socket.io的特殊性,需要考慮到在用戶和socket.io服務(wù)器建立TCP連接前的http請求,為此,在socket.io服務(wù)器和用戶中間使用了nginx做了代理轉(zhuǎn)發(fā)以及均衡負(fù)載作用。


socket.io具體機(jī)制

socket.io在建立連接之前,會經(jīng)過3次http請求,叫做3次握手,當(dāng)3次握手成功之后,才會建立TCP連接。socket.io底層是建立在engine.io之上,而非完全建立在websocket之上。engine.io使用了XMLHttpRequest(或JSON)和websocket封裝了自己的一套socket協(xié)議。在低版本瀏覽器里面使用長輪詢替代 WebSocket。一個(gè)完整的 EIO Socket 包括多個(gè) XHR 和 WebSocket 連接。

首先,前端,也可以理解為客戶端,EIO socket通過長輪詢,也就是一個(gè)XHR請求握手,告訴socket.io端,我要進(jìn)行XHR長輪詢了。后端返回的數(shù)據(jù),包括open(數(shù)字代表為0),以及一個(gè)sid(可以理解為session.id)和upgrades字段。由于一個(gè)EIO socket包含了多個(gè)請求,而后端又會同時(shí)連接多個(gè)EIO socket,此時(shí)sid就可以理解為session.id。

另一個(gè)字段upgrades,表示可以將連接方式從長輪訓(xùn)升級到websocket。
前端在發(fā)送第一個(gè)XHR時(shí)候就開始了長輪詢,所謂長輪詢,就是前端在發(fā)送一個(gè)請求的時(shí)候,服務(wù)端會等到有數(shù)據(jù)來到時(shí)候發(fā)送一個(gè)response,前端收到response之后再發(fā)送下一個(gè)request,實(shí)現(xiàn)雙向通信。

前端收到握手的 upgrades 后,EIO 會檢測瀏覽器是否支持 WebSocket,如果支持,就會啟動(dòng)一個(gè) WebSocket 連接,然后通過這個(gè) WebSocket 往服務(wù)器發(fā)一條內(nèi)容為 probe, 類型為 ping 的數(shù)據(jù)。如果這時(shí)服務(wù)器返回了內(nèi)容為 probe, 類型為 pong 的數(shù)據(jù),前端就會把前面建立的 HTTP 長輪詢停掉,后面只使用 WebSocket 通道進(jìn)行收發(fā)數(shù)據(jù)。


socket.io服務(wù)器集群基本思路

開始的思路,在集群方面,利用cluster的方式來生成socket.io服務(wù)器群,具體代碼如下

var cluster = require('cluster');
var os = require('os');

if (cluster.isMaster) {
  var server = require('http').createServer();
  for (var i = 0; i < os.cpus().length; i++) {
    cluster.fork();
  }

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  }); 
}

if (cluster.isWorker) {
  var express = require('express');
  var app = express();

  var http = require('http');
  var server = http.createServer(app);
  var io = require('socket.io').listen(server);
  var redis = require('socket.io-redis');

  io.adapter(redis({ host: 'localhost', port: 6379 }));
  io.on('connection', function(socket) {
    socket.emit('data', 'connected to worker: ' + cluster.worker.id);
  });

  app.listen(80);
}

cluster支持兩種分發(fā)模式,第一是由主進(jìn)程負(fù)責(zé)監(jiān)聽端口,接收新連接后再將連接循環(huán)分發(fā)給工作進(jìn)程。在分發(fā)中使用了一些內(nèi)置技巧防止工作進(jìn)程任務(wù)過載。子進(jìn)程無法去監(jiān)聽端口,不能最大化去設(shè)定每個(gè)子進(jìn)程的任務(wù)。

第二是主進(jìn)程創(chuàng)建監(jiān)聽socket后發(fā)送給感興趣的工作進(jìn)程,由工作進(jìn)程負(fù)責(zé)直接接收連接。

理論上第二種方法應(yīng)該是效率最佳的,但在實(shí)際情況下,由于操作系統(tǒng)調(diào)度機(jī)制的難以捉摸,會使分發(fā)變得不穩(wěn)定。我們遇到過這種情況:8個(gè)進(jìn)程中的2個(gè),分擔(dān)了70%的負(fù)載。

所以在下面例子中直接不采用cluster方式,而直接粗暴的創(chuàng)建socket.io服務(wù)器。

由于利用到多個(gè)socket.io服務(wù)器,為了能夠?qū)⒄麄€(gè)socket.io服務(wù)器群對于用戶而言等同于一個(gè)大的服務(wù)器,在socket.io和用戶之間用Nginx來做請求轉(zhuǎn)發(fā)?;舅悸穲D如下


image.png

首先用戶將請求發(fā)送到Nginx,Nginx在此基礎(chǔ)上做請求轉(zhuǎn)發(fā),將請求根據(jù)相關(guān)配置發(fā)送到4個(gè)服務(wù)器中的一個(gè)。假設(shè)socket.io服務(wù)器分別占用端口3001、3002、3003、3004的話,在Nginx中的相關(guān)配置nginx.conf如下:
在http模塊下有

http{
    #在upstream配置socket.io服務(wù)器,可以理解為上游部分
    upstream my_servers{
        server 127.0.0.1:3001
        server 127.0.0.1:3002
        server 127.0.0.1:3003
        server 127.0.0.1:3004
    }
    server{
        listen 8080;
        server_name localhost;
        location / {
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header Host $host;
          proxy_http_version 1.1;
          proxy_pass http://my_servers;
        }
    }
}

這樣就可以將Client的請求轉(zhuǎn)發(fā)到這4個(gè)服務(wù)器中的任何一個(gè),此時(shí)socket.io代碼如下,僅僅將socket.io2作為實(shí)例,其余socket.io服務(wù)器代碼完全一致,除了各自編號

const express = require('express');
const http = require('http');
const app = express();

const server = http.Server(app)
const io = require('socket.io')(server);

io.on('connection',(socket)=>{
    console.log('Server No.2 is connected');
    socket.emit('message', {message: 'Here is the message coming from the server 2'});
})

server.listen(3002, ()=>{
    console.log('hey there')
})

客戶端服務(wù)器代碼如下,主要是為了加載靜態(tài)頁,

const path = require('path');
const app = require('express')();
const server = require('http').createServer(app);
const io = require('socket.io')(server);

app.get('/',(req, res)=>{
    res.sendFile(path.join(__dirname, './index.html'));
})

server.listen(3000)

index.html代碼如下

  <script>
        var btn = document.getElementById('btn1');
        btn.addEventListener('click',function(){
            var socket = io.connect('https://localhost:8080',{
                reconnection: false
            });
            socket.on('connect', ()=>{
                console.log('Client has connected')
                socket.on('s:message',(d)=>{
                    console.log(d);
                });

            });
            socket.on('error',function(err){
                console.log(err);
            })
        });
    </script>

此時(shí)將4個(gè)socket.io服務(wù)器同時(shí)運(yùn)行,模擬一個(gè)最小的集群樣例,開啟客戶端服務(wù)器,打開網(wǎng)頁localhost:3000,此時(shí)頁面上有一個(gè)連接的按鈕,點(diǎn)擊之后,打開瀏覽器控制臺可以看到


image.png

由于socket.io服務(wù)器要和用戶建立TCP的連接的前提在于三次握手成功,但是目前存在的問題是,比如,第一次用戶的請求分發(fā)給socket.io No.1,然后第二次請求分發(fā)給socket.io No.2,用戶所攜帶的sid無法被No.2所識別,所以導(dǎo)致失敗,也許前面3次都成功握手了,負(fù)責(zé)監(jiān)聽update事件的server也不是那個(gè)server也會導(dǎo)致失敗。

基本原理基本清楚了之后,重點(diǎn)是解決用戶的一次建立tcp之前的http請求,必須固定分發(fā)給一個(gè)服務(wù)器,在socket.io官方文檔給出的解決方案是利用sticky session,而Nginx本身也提供了這一策略,即ip_hash。

將ip_hash添加到http部分的upstream部分中,放置在首位。然后重啟nginx,運(yùn)行客戶端,點(diǎn)擊連接按鈕,連接成功。當(dāng)模擬一個(gè)socket.io掛掉的時(shí)候,用戶請求仍然可以轉(zhuǎn)發(fā)至其他正常運(yùn)行的服務(wù)器,針對sticky session策略目前是可行的。


模擬大量用戶并發(fā)訪問

單個(gè)用戶訪問建立TCP連接在上述是可行的,但是如果大量用戶并發(fā)訪問呢?目前用到apache ab工具來進(jìn)行高并發(fā)訪問實(shí)驗(yàn)。

命令如下

ab -n 10000 -c 100 -r 127.0.0.1:8080

模擬總量10000次,每次同時(shí)請求100。

此時(shí)socket.io服務(wù)器只有一個(gè)接受請求,其余3個(gè)都是處于空閑狀態(tài),而接受請求的那個(gè)負(fù)載很高,并且經(jīng)常以外拒絕請求,導(dǎo)致失敗。

原因分析應(yīng)該在于ip_hash策略,導(dǎo)致所有的請求都自動(dòng)綁定到某一個(gè)服務(wù)器上,而無法實(shí)現(xiàn)分?jǐn)偟狡渌?wù)器上。Nginx還有提供一個(gè)策略就是least_conn,即根據(jù)最小連接數(shù)來選擇相應(yīng)的服務(wù)器進(jìn)行連接,當(dāng)在upstream中加入least_conn策略的時(shí)候,運(yùn)行的時(shí)候提示least_conn把ip_hash覆蓋掉了,看來策略只能存在一個(gè)。但是least_conn可以將請求均攤到4個(gè)服務(wù)器上,看來還是在一定情況下滿足我們的需求。

在每個(gè)server 127.0.0.1:300x 后面加上權(quán)重的話,可以將服務(wù)器的訪問分流到我們想要的服務(wù)器上,但是缺點(diǎn)在于,目前只會手動(dòng)處理,不現(xiàn)實(shí)。因此這個(gè)方案也pass掉。


新解決方案1(代碼部分還未實(shí)現(xiàn))

由于least_conn方案可以將用戶的請求均攤到4個(gè)服務(wù)器上,如果用戶只通過nginx請求一次,分發(fā)到空閑的服務(wù)器上,然后再由服務(wù)器本身去與客戶直接連接tcp,將websocket連接繞開Nginx


image.png

白色箭頭表示普通的http的請求,也僅僅是單次請求,只請求一次,通俗而言就是客戶端通過Nginx,告訴4個(gè)socket.io服務(wù)器中的某一個(gè),我要開始進(jìn)行連接了,得到服務(wù)器端的通過之后,服務(wù)器端會繞開nginx,主動(dòng)與client建立websocket連接,目前方法是可行的,只是代碼部分還未實(shí)現(xiàn)。


新解決方案2

可以定制nginx的策略,來完成相關(guān)的均衡負(fù)載,但是不知道可行與否


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容