本站使用「署名 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.js
的 WebSocket
方案,可選的方案有許多種,其中許多方案都提供將 WS
服務埠和 HTTP
服務複用的方案,然而這種方案真的是最佳選擇嗎。
不論是專業做實時通訊的 socket.io ,還是使用者量最大的 Express
的熱門中介軟體 express-ws 都支援埠複用,比如 WS
和 HTTP
複用 80
埠, WSS
和 HTTPS
複用 443
埠。
這裡以 express-ws
底層封裝的 ws 庫為例,來簡單剖析,socket.io
實現類似不過分層較多,有興趣可以圍觀程式碼。
不過在聊 Traefik
之前,我們先得聊聊 Node.js
和 Websocket
。
關於同域名埠複用
先說結論,優點:
- 使用簡單,尤其是整個專案程式碼量少的時候。
- 服務域名複用,不需要額外進行域名解析。
- 能夠簡單獲取
HTTP
請求中的會話資訊,進行簡單的驗證操作,能夠程式碼級複用邏輯。
缺點也很明顯:
- 因為複用埠,對於每個資料都需要甄別是應該交給
Express
處理還是WS
處理,存在效能損耗,如果需要進行壓縮等操作,會有更多的損耗。 - 相同域名不易進行業務水平擴充套件,比如需要支援更多的實時業務,原本擴容3例項的
WS
服務即可,由於耦合,不得不將整個服務進行擴充套件,存在更多資源的損耗。 - 由於耦合,複雜度相比較“各自獨立”的版本高,在維護過程,如果修改底層程式碼,難免會讓兩個服務都不夠健壯穩定。
從程式碼實現角度圍觀埠複用
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
埠的簡單服務,支援使用 WS
和 HTTP
進行資料互動。
這樣的服務的優勢和不足
優勢:
- 可以輕鬆針對不同協議的服務進行擴容操作。
- 彼此執行時資源隔離,安全性和穩定性更好。
- 可以使用相同域名、不同埠部署,也可以使用不同域名,預設埠進行部署,部署選擇也更多。
劣勢:
- 在不依賴
RDS
、Redis Cache
等方案的前提下,請求之間的資料難以共享。 - 如果需要都使用預設埠進行部署,那麼需要額外進行一個域名的解析。
搭配 Traefik 使用
我將 SSL
證書掛載和 HTTP
壓縮放在 Traefik
端處理,相比較 Node.js
來做,一來可以保障業務程式碼功能獨立純粹,二來效能確實不如它,而且維護起來也比較麻煩(證書管理)。
對於接入閘道器的服務,只要宣告提供 HTTP
和 WS
的埠和對應的域名即可,程式啟動之後,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
埠,都被“改寫”成為了 80
和 443
埠上了,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、程式設計上的一些問題,也會在群裡不定期的分享一些技術沙龍的資料。
喜歡折騰的小夥伴歡迎掃碼新增好友。(請註明來源和目的,否則不會通過稽核) 關於折騰群入群的那些事