實簡單現websocket信道服務

實簡單現websocket信道服務

開端

之前有爲一個項目做顧問工作,幫助解決了幾個問題。該項目完全按照wafer解決方案做的,我在解決問題過程中,發現wafer信道容易出現消息丟失情況,當時由於金主預算有限,沒有從根本上解決這個問題,只是做了些曲線救國的優化,然後繼續將就着用。與此同時騰訊官方表態wafer整套解決方案都不再提倡,同時不再維護,希望開發者往小程序“雲開發”的方向靠攏。

直到有一天,wafer信道停止了服務,導致整個項目癱住,金主沒轍了,又找到了我…

可選方案

爲了生計,來活了,自己當然是考慮的,所以爲金主想了兩個方案:重寫信道服務與重寫項目。

重寫信道服務

wafer不再維護,但原有的功能可以繼續使用,只是不再提供信道服務,如果要讓整個項目“復活”,可以選擇再實現一個信道服務用來替代曾經的免費信道服務。

這個方案的優點是只需要專心解決技術問題,對項目原有業務的影響幾乎爲0,對金主而言成本一般。

但從開發的角度來看,缺點也比較明顯:“需要嚴格適配已有的SDK”,wafer信道的參與方是server、tunnel-server與client,這其中與tunnel-server通信的雙方都依賴wafer提供的SDK。要做到不影響業務代碼,需嚴格適配這兩個SDK,也就是要去仔細閱讀SDK源碼,然後定義tunnel-server的相關接口和參數,這對我而言並不是一件愉快的事。

重寫項目

只是重新實現信道服務,金主擔心不穩定,所以就諮詢了一下重寫的成本。我的評估是相差不大,因爲重寫,信道服務由自己定接口相較於適配SDK會快很多,而且整個項目的業務並不複雜,從零實現也比較快。

這裏我個人也偏向於重寫,因爲老的代碼質量我個人覺得很不靠譜,而且設計也不合理。比如對戰匹配隊列、用戶對戰信息等,竟然全放在內存中,這意味着無法部署多個實例,要知道金主的單機可是8核16G。

這裏怕有一些新手不太理解,再詳細解釋一下,假設我們把匹配隊列放在內存裏面:

匹配隊列const queueList = [];,第一個用戶A來排隊,往queueList中push這個用戶的ID,又一個用戶B來排隊,發現queueList中已經有一個用戶了,取出來然後通知他們(A-B)匹配成功

咋一看沒什麼問題,其實不然:

內存是的scope是進程,而Node.js是單線程,scope變相成了線程,一個線程只能運行在CPU的一個核上,像queueList這種數據限制了業務同時啓動多個進程實例(啓多個進程實例,用戶請求可能落在不同進程上,上面的A-B就不一定能匹配成功),所以最後只能啓一個進程,最多能利用1/8的CPU資源

資源浪費不是最致命的,最致命的是無法橫向擴展,應用承壓的時候就只能乾瞪眼。這個基礎設計層面的坑以及代碼本身的質量讓我時刻惦記着重寫,所以最終也說服金主選擇了重寫這個項目。

設計

在決定重寫之後,對websocket信道通信這塊有想過兩個方案:

信道通信功能與業務服務作爲一個整體

最初是傾向這個方案,因爲效率更高,業務模塊可以直接通過socket給client推送消息,但有一個致命問題沒找到好的解決辦法:“用於通信的socket只能在內存中持有,這讓應用服務是有狀態的”。針對這個問題有想過在request的url中攜帶用戶ID然後通過nginx路由,但覺得會複雜度上升太多,故放棄。

這裏同樣解釋一下上述提到應用是有狀態的含義:

在Node.js中,藉助websocket庫實現websocket通信,其原理是server與client建立連接後,server持有一個該連接對應的socket引用,然後通過該socket與客戶端進行通信。這個socket引用只能在內存中使用,與前面的queueList例子類似,如果A用戶的某些消息要通知給B,如何能找到B用戶對應的socket?如果只有一個進程,提前在內存中記錄好映射關係還可以找到,但如果是多個進程,就不好辦了,這時我們稱服務是有狀態的,因爲用戶跟進程有綁定關係

模仿wafer這種設計,將信道通信功能剝離成獨立的服務

因爲信道通信功能與業務服務作爲一個整體,自己沒找到很好的解決應用服務有狀態問題的方案,所以考慮將信道通信功能作爲獨立的服務剝離。

剝離成獨立的服務之後,應用服務繼續保持無狀態,信道服務來處理狀態問題,我的做法是:

信道服務設計一個節點(node)的概念,每一個節點只啓動一個實例,信道服務的使用者從任意節點獲取連接URL(‘wss://xxx/{node}?id={tunnelId}’)後,自己維護tunnelId與node的對應關係,向信道推送消息時攜帶節點信息,比如在http請求的URL中包含節點信息(‘http://xxx/{node}/send-message?id={tunnelId}’),這個請求通過nginx路由轉發到與之匹配的信道節點,從而保證該節點持有與client通信的socket

這樣設計之後,信道服務器可以通過啓動多個節點實現水平擴展,暫時不會成爲單點瓶頸。目前設計中節點信息是單獨體現,應用服務需要維護tunnelId與node的映射,而如果將node信息融入tunnelId中的話,可以節省這一步,不過應用服務本身需要維護用戶與tunnelId的對應關係,順便一起維護node的信息非常容易,也就無所謂了。

實現

實現並不複雜,信道服務對外提供四個接口:

  • init

監聽host:port,啓動websocket服務器,等待連接

  • connectUrl

獲取一個信道的連接URL,同時提供回調信息用於信道收到客戶端消息後通知下游,用RPC框架或者用HTTP通知均可,目前自己是用框架內的HTTP-RPC

  • sendMessageById

這個接口用於通過信道發送消息給客戶端,是業務服務器調用,調用方式採用HTTP

  • closeById

同sendMessageById,只不過是關閉連接時使用罷了

上述4個接口,除了init其他三個對外表現爲HTTP請求的方式,也就是業務服務器通過HTTP與信道服務器交互,我認爲這裏降低了效率,但目前影響不大,等到需要降低通信延遲,提高效率的時候再單獨重構交互方式即可。

這4個接口,自己在框架下實現起來並不費勁,共300行代碼左右,其中核心代碼放在Gist上,有興趣的朋友可以看看。

後記

最近有段時間沒更新博客,主要因爲太忙,且不知道應該寫什麼樣的內容,覺得只是記錄自己遇到的問題與如何解決的話就過於流水,況且有深度的問題本身就少。有時侯想分享自己做項目的經驗,又感覺內容寬泛,不知道從何寫起。還是慢慢平衡,儘量寫寫,堅持纔是勝利,畢竟寫博客這事是對自己的大利好。

博客原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章