使用 Node.js 和 Redis 在內部構建 Pub/Sub 服務
已發表: 2022-03-10當今世界實時運行。 無論是交易股票還是訂購食品,今天的消費者都期待立竿見影的效果。 同樣,我們都希望立即了解事情——無論是新聞還是體育。 換句話說,零是新英雄。
這也適用於軟件開發人員——可以說是一些最不耐煩的人! 在深入了解 BrowserStack 的故事之前,如果我不提供一些有關 Pub/Sub 的背景知識,那將是我的失職。 對於那些熟悉基礎知識的人,請隨意跳過接下來的兩段。
當今的許多應用程序都依賴於實時數據傳輸。 讓我們仔細看一個例子:社交網絡。 Facebook 和 Twitter 之類的網站會生成相關的訂閱源,而您(通過他們的應用程序)會使用它並監視您的朋友。 他們通過消息傳遞功能實現了這一點,如果用戶生成數據,它將被發布給其他人以供其他人使用。 任何嚴重的延遲和用戶都會抱怨,使用量會下降,如果持續存在,就會大量生產。 賭注很高,用戶的期望也很高。 那麼 WhatsApp、Facebook、TD Ameritrade、華爾街日報和 GrubHub 等服務如何支持大量實時數據傳輸呢?
它們都使用類似的高級軟件架構,稱為“發布-訂閱”模型,通常稱為 Pub/Sub。
“在軟件架構中,發布-訂閱是一種消息傳遞模式,其中消息的發送者(稱為發布者)不會將消息編程為直接發送給特定的接收者(稱為訂閱者),而是將發布的消息分類為不知道哪些訂閱者的類,如果任何,可能有。 同樣,訂閱者對一個或多個類表示興趣,並且只接收感興趣的消息,而不知道有哪些發布者(如果有的話)。
— 維基百科
對定義感到厭煩? 回到我們的故事。
在 BrowserStack,我們所有的產品(以一種或另一種方式)都支持具有大量實時依賴組件的軟件——無論是自動化測試日誌、新鮮出爐的瀏覽器屏幕截圖還是 15fps 移動流媒體。
在這種情況下,如果一條消息丟失,客戶可能會丟失對於防止錯誤至關重要的信息。 因此,我們需要針對不同的數據大小要求進行擴展。 例如,對於給定時間點的設備記錄器服務,一條消息下可能會生成 50MB 的數據。 像這樣的尺寸可能會使瀏覽器崩潰。 更不用說 BrowserStack 的系統將來需要針對其他產品進行擴展。
由於每條消息的數據大小從幾個字節到高達 100MB 不等,我們需要一個可以支持多種場景的可擴展解決方案。 換句話說,我們尋求一把可以切蛋糕的劍。 在本文中,我將討論在內部構建 Pub/Sub 服務的原因、方式和結果。
通過 BrowserStack 的實際問題,您將更深入地了解構建您自己的 Pub/Sub 的要求和過程。
我們需要發布/訂閱服務
BrowserStack 有大約 100M+ 條消息,每條消息大約在 2 字節和 100+ MB 之間。 它們隨時以不同的互聯網速度在世界各地傳遞。
按消息大小計算,這些消息的最大生成器是我們的 BrowserStack Automate 產品。 兩者都有實時儀表板,顯示用戶測試的每個命令的所有請求和響應。 因此,如果有人運行一個包含 100 個請求的測試,其中平均請求響應大小為 10 個字節,則傳輸 1×100×10 = 1000 個字節。
現在讓我們考慮更大的圖景——當然——我們不會每天只運行一次測試。 每天使用 BrowserStack 運行大約 850,000 多個 BrowserStack Automate 和 App Automate 測試。 是的,我們平均每個測試大約有 235 個請求-響應。 由於用戶可以在 Selenium 中截屏或請求頁面源,我們的平均請求-響應大小約為 220 字節。
所以,回到我們的計算器:
850,000×235×220 = 43,945,000,000 字節(大約)或每天僅 43.945GB
現在讓我們談談 BrowserStack Live 和 App Live。 當然,在數據大小方面,我們有 Automate 是我們的贏家。 但是,在傳遞的消息數量方面,Live 產品處於領先地位。 對於每次實時測試,每轉一分鐘大約有 20 條消息通過。 我們運行了大約 100,000 個實時測試,每個測試平均大約 12 分鐘,這意味著:
100,000×12×20 = 每天 24,000,000 條消息
現在是令人敬畏和非凡的一點:我們為這個名為 pusher 的應用程序構建、運行和維護應用程序,其中包含 6 個 t1.micro ec2 實例。 運行服務的成本? 每月約 70 美元。
選擇建造與購買
首先要做的事情:作為一家初創公司,像大多數其他人一樣,我們總是很高興能夠在內部構建東西。 但我們仍然評估了一些服務。 我們的主要要求是:
- 可靠性和穩定性,
- 高性能,和
- 成本效益。
讓我們把成本效益標準排除在外,因為我想不出任何每月費用低於 70 美元的外部服務(如果知道你這樣做,請發推特給我!)。 所以我們的答案是顯而易見的。
在可靠性和穩定性方面,我們發現提供 Pub/Sub 即服務的公司具有 99.9% 以上的正常運行時間 SLA,但附加了許多 T&C。 問題並不像您想像的那麼簡單,尤其是當您考慮位於系統和客戶端之間的廣闊的開放 Internet 土地時。 熟悉互聯網基礎設施的人都知道,穩定的連接是最大的挑戰。 此外,發送的數據量取決於流量。 例如,一分鐘為零的數據管道可能會在下一分鐘內爆裂。 在這種突發時刻提供足夠可靠性的服務很少見(谷歌和亞馬遜)。
我們項目的性能意味著以接近零的延遲獲取數據並將數據發送到所有偵聽節點。 在 BrowserStack,我們利用雲服務 (AWS) 和主機託管。 但是,我們的發布者和/或訂閱者可以放置在任何地方。 例如,它可能涉及生成急需的日誌數據的 AWS 應用程序服務器或終端(用戶可以安全連接以進行測試的機器)。 再次回到開放的 Internet 問題,如果我們要降低風險,我們將不得不確保我們的 Pub/Sub 利用最好的主機服務和 AWS。
另一個基本要求是能夠傳輸所有類型的數據(字節、文本、奇怪的媒體數據等)。 綜合考慮,依靠第三方解決方案來支持我們的產品是沒有意義的。 反過來,我們決定重振創業精神,捲起袖子編寫自己的解決方案。
構建我們的解決方案
Pub/Sub 的設計意味著將有一個發布者,生成和發送數據,以及一個訂閱者接受和處理它。 這類似於廣播:廣播頻道在一定範圍內的任何地方廣播(發布)內容。 作為訂閱者,您可以決定是否收聽該頻道並收聽(或完全關閉您的收音機)。
與廣播類比中數據對所有人免費並且任何人都可以決定收聽的情況不同,在我們的數字場景中,我們需要身份驗證,這意味著發布者生成的數據只能用於單個特定的客戶或訂閱者。


上圖提供了一個良好的 Pub/Sub 示例,其中包含:
- 出版商
在這裡,我們有兩個發布者根據預定義的邏輯生成消息。 在我們的廣播類比中,這些是我們創建內容的廣播節目主持人。 - 話題
這裡有兩個,意味著有兩種類型的數據。 我們可以說這些是我們的廣播頻道 1 和 2。 - 訂戶
我們有三個,每個都讀取特定主題的數據。 需要注意的一件事是,訂閱者 2 正在閱讀多個主題。 在我們的無線電類比中,這些人是被調到無線電頻道的人。
讓我們開始了解服務的必要要求。
- 事件組件
這只有在有東西可以啟動時才會啟動。 - 瞬態存儲
這可以使數據在短時間內保持不變,因此如果訂閱者速度較慢,它仍然有一個窗口可以使用它。 - 減少延遲
通過網絡以最小的跳數和距離連接兩個實體。
我們選擇了一個滿足上述要求的技術棧:
- 節點.js
因為為什麼不呢? Evented,我們不需要繁重的數據處理,而且很容易上手。 - 雷迪斯
支持完美的短期數據。 它具有啟動、更新和自動過期的所有功能。 它還減少了應用程序的負載。
用於業務邏輯連接的 Node.js
在編寫包含 IO 和事件的代碼時,Node.js 是一種近乎完美的語言。 我們特定的給定問題兩者都有,這使得這個選項對我們的需求最實用。
當然,其他語言(如 Java)可能會更優化,或者 Python 等語言提供可擴展性。 然而,從這些語言開始的成本是如此之高,以至於開發人員可以在相同的時間內完成在 Node 中編寫代碼。
老實說,如果該服務有機會添加更複雜的功能,我們可以查看其他語言或完整的堆棧。 但這裡是天作之合。 這是我們的package.json :
{ "name": "Pusher", "version": "1.0.0", "dependencies": { "bstack-analytics": "*****", // Hidden for BrowserStack reasons. :) "ioredis": "^2.5.0", "socket.io": "^1.4.4" }, "devDependencies": {}, "scripts": { "start": "node server.js" } }
簡單地說,我們相信極簡主義,尤其是在編寫代碼時。 另一方面,我們可以使用 Express 之類的庫來為這個項目編寫可擴展的代碼。 但是,我們的創業本能決定將其傳遞下去,並將其保存到下一個項目中。 我們使用的其他工具:
- 奧雷迪斯
這是包括阿里巴巴在內的公司使用的最受支持的 Redis 與 Node.js 連接庫之一。 - 套接字.io
使用 WebSocket 和 HTTP 進行優雅連接和回退的最佳庫。
Redis 用於瞬態存儲
Redis 即服務可擴展性非常可靠且可配置。 此外,還有許多可靠的 Redis 託管服務提供商,包括 AWS。 即使您不想使用提供程序,Redis 也很容易上手。
讓我們分解可配置部分。 我們從通常的主從配置開始,但 Redis 也帶有集群或哨兵模式。 每種模式都有自己的優勢。
如果我們可以通過某種方式共享數據,那麼 Redis 集群將是最佳選擇。 但是,如果我們通過任何啟發式共享數據,我們的靈活性就會降低,因為必須遵循啟發式。 少一些規則,多一些控制對生活有好處!
Redis Sentinel 最適合我們,因為數據查找僅在一個節點中完成,在給定時間點連接,而數據沒有分片。 這也意味著即使多個節點丟失,數據仍然分佈並存在於其他節點中。 所以你有更多的 HA 和更少的損失機會。 當然,這使專業人員不再擁有集群,但我們的用例有所不同。
30000 英尺的建築
下圖提供了我們的 Automate 和 App Automate 儀表板如何工作的非常高級的圖片。 還記得我們在前面部分中的實時系統嗎?

在我們的圖表中,我們的主要工作流程以較粗的邊框突出顯示。 “自動化”部分包括:
- 終端
由您在 BrowserStack 上測試時獲得的原始版本的 Windows、OSX、Android 或 iOS 組成。 - 中心
使用 BrowserStack 進行所有 Selenium 和 Appium 測試的聯繫點。
這裡的“用戶服務”部分是我們的看門人,確保將數據發送給正確的個人並保存。 它也是我們的安全守護者。 “推動者”部分包含我們在本文中討論的核心內容。 它由通常的嫌疑人組成,包括:
- 雷迪斯
我們用於消息的臨時存儲,在我們的例子中,自動化日誌是臨時存儲的。 - 出版商
這基本上是從集線器獲取數據的實體。 您的所有請求響應都由該組件捕獲,該組件以session_id
作為通道寫入 Redis。 - 訂戶
這將從為session_id
生成的 Redis 中讀取數據。 它也是客戶端通過WebSocket(或HTTP)連接以獲取數據然後將其發送給經過身份驗證的客戶端的Web服務器。
最後,我們有用戶的瀏覽器部分,代表一個經過身份驗證的 WebSocket 連接,以確保發送session_id
日誌。 這使得前端JS可以為用戶解析和美化它。
與日誌服務類似,我們這裡有用於其他產品集成的推送器。 我們使用另一種形式的 ID 代替session_id
來表示該通道。 這一切都靠推桿!
結論 (TLDR)
我們在構建 Pub/Sub 方面取得了相當大的成功。 總結一下我們內部構建它的原因:
- 更好地滿足我們的需求;
- 比外包服務便宜;
- 完全控制整體架構。
更不用說 JS 非常適合這種場景。 事件循環和海量IO才是問題所需要的! JavaScript 是單偽線程的魔法。
事件和 Redis 作為一個系統使開發人員的工作變得簡單,因為您可以從一個來源獲取數據並通過 Redis 將其推送到另一個來源。 所以我們建造了它。
如果該用法適合您的系統,我建議您也這樣做!