使用 Traefik 提高 WebSocket 應用效能

蘇洋發表於2018-09-12

本站使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

建立時間: 2018年09月04日 統計字數: 4350字 閱讀時間: 9分鐘閱讀 本文連結: soulteary.com/2018/09/04/…


使用 Traefik 提高 WebSocket 應用效能

說起 Node.jsWebSocket 方案,可選的方案有許多種,其中許多方案都提供將 WS 服務埠和 HTTP 服務複用的方案,然而這種方案真的是最佳選擇嗎。

不論是專業做實時通訊的 socket.io ,還是使用者量最大的 Express 的熱門中介軟體 express-ws 都支援埠複用,比如 WSHTTP 複用 80 埠, WSSHTTPS 複用 443 埠。

這裡以 express-ws 底層封裝的 ws 庫為例,來簡單剖析,socket.io 實現類似不過分層較多,有興趣可以圍觀程式碼。

不過在聊 Traefik 之前,我們先得聊聊 Node.jsWebsocket

關於同域名埠複用

先說結論,優點:

  1. 使用簡單,尤其是整個專案程式碼量少的時候。
  2. 服務域名複用,不需要額外進行域名解析。
  3. 能夠簡單獲取 HTTP 請求中的會話資訊,進行簡單的驗證操作,能夠程式碼級複用邏輯。

缺點也很明顯:

  1. 因為複用埠,對於每個資料都需要甄別是應該交給 Express 處理還是 WS 處理,存在效能損耗,如果需要進行壓縮等操作,會有更多的損耗。
  2. 相同域名不易進行業務水平擴充套件,比如需要支援更多的實時業務,原本擴容3例項的 WS 服務即可,由於耦合,不得不將整個服務進行擴充套件,存在更多資源的損耗。
  3. 由於耦合,複雜度相比較“各自獨立”的版本高,在維護過程,如果修改底層程式碼,難免會讓兩個服務都不夠健壯穩定。

從程式碼實現角度圍觀埠複用

express-ws 進行埠複用的時候,會進行大量 hacks 操作,包括擴充套件路由、改寫請求地址新增特殊標記、重寫預設響應頭...

下面這段示例是官方給出的埠複用的例子。

var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);

app.use(function (req, res, next) {
  console.log('middleware');
  req.testing = 'testing';
  return next();
});

app.get('/', function(req, res, next){
  console.log('get route', req.testing);
  res.end();
});

app.ws('/', function(ws, req) {
  ws.on('message', function(msg) {
    console.log(msg);
  });
  console.log('socket', req.testing);
});

app.listen(3000);
複製程式碼

實際使用的時候,訪問 WS/,會訪問 Express/.websocket?{QUERY},並使用中介軟體注入處理過程的方式,搶在預設處理前使用 ws 替換處理過程,修改響應頭,輸出處理後的內容,並呼叫 res.end 結束流程。

在路由越來越多、請求量越來越多的情況下,會存在很多不必要的損耗。

如何進行服務拆分

如果不需要埠複用,其實直接使用 ws 來監聽獨立的新埠即可,參考官方示例,可以很輕鬆的寫出這樣一個例子:

const WebSocket = require('ws');

const wss = new WebSocket.Server({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: { // See zlib defaults.
      chunkSize: 1024,
      memLevel: 7,
      level: 3,
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    // Other options settable:
    clientNoContextTakeover: true, // Defaults to negotiated value.
    serverNoContextTakeover: true, // Defaults to negotiated value.
    clientMaxWindowBits: 10,       // Defaults to negotiated value.
    serverMaxWindowBits: 10,       // Defaults to negotiated value.
    // Below options specified as default values.
    concurrencyLimit: 10,          // Limits zlib concurrency for perf.
    threshold: 1024,               // Size (in bytes) below which messages
                                   // should not be compressed.
  }
});

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });

  ws.send('something');
});

server.listen(8080);
複製程式碼

HTTP 服務監聽在另外一個埠,可以參考 Express 最簡單的示例:

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

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(3000, () => console.log('Example app listening on port 3000!'))
複製程式碼

這裡分別將程式碼片段進行儲存,當你分別使用 Node.js 執行它的時候,你將會得到監聽 3000 埠和 8080 埠的簡單服務,支援使用 WSHTTP 進行資料互動。

這樣的服務的優勢和不足

優勢:

  1. 可以輕鬆針對不同協議的服務進行擴容操作。
  2. 彼此執行時資源隔離,安全性和穩定性更好。
  3. 可以使用相同域名、不同埠部署,也可以使用不同域名,預設埠進行部署,部署選擇也更多。

劣勢:

  1. 在不依賴 RDSRedis Cache 等方案的前提下,請求之間的資料難以共享。
  2. 如果需要都使用預設埠進行部署,那麼需要額外進行一個域名的解析。

搭配 Traefik 使用

我將 SSL 證書掛載和 HTTP 壓縮放在 Traefik 端處理,相比較 Node.js 來做,一來可以保障業務程式碼功能獨立純粹,二來效能確實不如它,而且維護起來也比較麻煩(證書管理)。

對於接入閘道器的服務,只要宣告提供 HTTPWS 的埠和對應的域名即可,程式啟動之後,Traefik 會自動將應用掛載到對應域名上,並支援 HTTP(S)WS(S) 的服務。

為圖簡便,我將上面的程式碼片段儲存為一個基礎映象,交付給編排工具使用。

如果你將上面的程式碼片段儲存為一個檔案,可以試試下面的配置:

version: '3'

services:

  node:
    image: docker.lab.com/example.lab.com:0.0.1
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.web.port=3000"
      - "traefik.web.frontend.rule=Host:web.soulteary.com"
      - "traefik.ws.port=8080"
      - "traefik.ws.frontend.rule=Host:ws.soulteary.com"
    networks:
      - traefik
    expose:
      - 3000
      - 8080
    extra_hosts:
      - "web.soulteary.com:127.0.0.1"
      - "ws.soulteary.com:127.0.0.1"

networks:
  traefik:
    external: true
複製程式碼

使用上面的配置執行之後,你會發現原本的 3000 埠和 8080 埠,都被“改寫”成為了 80443 埠上了,Web 應用使用的時候,便不用額外寫入“醜陋”的埠號了,但是這樣的配置不利於服務擴充套件,在埠複用優劣小節中我提到過。

那麼,如果你有意將程式碼進行拆分,那麼可以試試下面的配置:

version: '3'

services:

  web:
    image: docker.lab.com/example.lab.com:0.0.1
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.web.port=3000"
      - "traefik.web.frontend.rule=Host:web.soulteary.com"
    networks:
      - traefik
    expose:
      - 3000
    extra_hosts:
      - "web.soulteary.com:127.0.0.1"

  ws:
    image: docker.lab.com/example.lab.com:0.0.1
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.ws.port=8080"
      - "traefik.ws.frontend.rule=Host:ws.soulteary.com"
    networks:
      - traefik
    expose:
      - 8080
    extra_hosts:
      - "ws.soulteary.com:127.0.0.1"

networks:
  traefik:
    external: true
複製程式碼

擴容也很簡單,如果你要以 2:3 的比例執行不同協議的話,只需要:

docker-compose scale web=2 ws=3
複製程式碼

其他

如果你還在使用 ajax polling 或許這個方案可以給你更好的體驗。

如果你對 Traefik 期望有更多的瞭解,也歡迎和我溝通討論。


我現在有一個小小的折騰群,裡面聚集了一些喜歡折騰的小夥伴。

在不發廣告的情況下,我們在裡面會一起聊聊軟體、HomeLab、程式設計上的一些問題,也會在群裡不定期的分享一些技術沙龍的資料。

喜歡折騰的小夥伴歡迎掃碼新增好友。(請註明來源和目的,否則不會通過稽核) 關於折騰群入群的那些事

關於折騰群入群的那些事

相關文章