Braze 如何大規模利用 Ruby

已發表: 2022-08-18

如果你是一名閱讀 Hacker News、Developer Twitter 或任何其他類似信息來源的工程師,你幾乎肯定會遇到過一千篇標題為“Rust 與 C 的速度”、“是什麼讓 Node. js 比 Java 更快?”,或“為什麼要使用 Golang 以及如何開始。” 這些文章通常表明,有一種特定的語言是可擴展性或速度的明顯選擇,而您唯一要做的就是接受它。

當我在大學和作為工程師的第一年或第二年時,我會閱讀這些文章並立即啟動一個寵物項目來學習新的語言或框架。 畢竟,它可以保證“在全球範圍內”工作並且“比你見過的任何東西都快”,誰能抗拒呢? 最終我發現對於我的大多數項目,我實際上並不需要這些非常具體的東西。 隨著我職業生涯的發展,我開始意識到沒有任何語言或框架選擇實際上可以免費為我提供這些東西。

相反,我發現當你想要擴展系統時,架構實際上是最大的槓桿,而不是語言或框架。

在 Braze,我們在全球範圍內開展業務。 是的,我們使用 Ruby 和 Rails 作為我們執行此操作的兩個主要工具。 然而,沒有“global_scale = true”的配置值可以讓這一切成為可能——這是一個深思熟慮的架構的結果,從應用程序的深處一直延伸到部署拓撲。 Braze 的工程師一直在檢查擴展瓶頸並找出如何使我們的系統更快,而答案通常不是“遠離 Ruby”:它幾乎肯定會改變架構。

因此,讓我們來看看 Braze 如何利用深思熟慮的架構來實際解決速度和大規模的全球規模問題——以及 Ruby 和 Rails 適合它的地方(和不適合)!

一流架構的力量

一個簡單的網絡請求

由於我們的運營規模,我們知道與客戶用戶群相關的設備每天都會發出數十億個 Web 請求,這些請求必須由某個 Braze Web 服務器提供服務。 即使在最簡單的網站中,您也會有一個相對複雜的流程,與從客戶端到服務器並返回的請求相關聯:

  1. 首先是客戶端的 DNS 解析器(通常是他們的 ISP)根據您網站 URL 中的域確定要訪問的 IP 地址。

  2. 一旦客戶端有了 IP 地址,他們就會將請求發送到網關路由器,網關路由器會將請求發送到“下一跳”路由器(​​可能會發生多次),直到請求到達目標 IP 地址。

  3. 從那裡,接收請求的服務器上的操作系統將處理網絡詳細信息,並通知 Web 服務器的等待進程在它正在偵聽的套接字/端口上接收到傳入請求。

  4. Web 服務器會將響應(請求的資源,可能是 index.html)寫入該套接字,該套接字將通過路由器返回客戶端。

對於一個簡單的網站來說相當複雜的東西,不是嗎? 幸運的是,其中許多事情都為我們處理好了(稍後會詳細介紹)。 但是我們的系統仍然有數據存儲、後台作業、並發問題等等需要處理! 讓我們深入了解它的外觀。

第一個支持擴展的系統

在大多數情況下,DNS 和名稱服務器通常不需要太多關注。 您的頂級域名服務器可能會有一些條目將“yourwebsite.com”映射到您的域的名稱服務器,如果您使用的是 Amazon Route 53 或 Azure DNS 等服務,它們將處理該名稱您的域的服務器(例如管理 A、CNAME 或其他類型的記錄)。 您通常不必考慮縮放這部分,因為這將由您使用的系統自動處理。

然而,流程的路由部分可能會變得有趣。 有幾種不同的路由算法,例如開放最短路徑優先或路由信息協議,所有這些算法都旨在找到從客戶端到服務器的最快/最短路徑。 因為互聯網實際上是一個巨大的連接圖(或者,或者,一個流網絡),可能有多個可以利用的路徑,每個路徑都有相應的更高或更低的成本。 尋找絕對最快路線的工作是令人望而卻步的,因此大多數算法都使用合理的啟發式方法來獲得可接受的路線。 計算機和網絡並不總是可靠的,因此我們依靠 Fastly 來增強客戶更快地路由到我們的服務器的能力。

通過在世界各地提供快速、可靠的連接,快速工作。 將它們視為互聯網的州際高速公路。 我們域的 A 和 CNAME 記錄指向 Fastly,這導致我們客戶的請求直接進入高速公路。 從那裡,Fastly 可以將它們路由到正確的位置。

釬焊的前門

好的,所以我們的客戶的請求已經通過 Fastly 高速公路,就在 Braze 平台的前門——接下來會發生什麼?

在一個簡單的情況下,該前門將是一個接受請求的服務器。 正如你可以想像的那樣,這不會很好地擴展,所以我們實際上將 Fastly 指向一組負載均衡器。 負載均衡器可以使用各種策略,但想像一下,在這種情況下,Fastly 循環將請求均勻地發送到負載均衡器池。 這些負載均衡器將對請求進行排隊,然後將這些請求分發到 Web 服務器,我們也可以想像以循環方式處理客戶端請求。 (實際上,某些類型的親和力可能有優勢,但這是另一個話題。)

這使我們能夠根據我們獲得的請求的吞吐量和我們可以處理的請求的吞吐量來擴大負載均衡器的數量和 Web 服務器的數量。 到目前為止,我們已經構建了一個架構,可以毫不費力地處理大量請求! 它甚至可以通過負載均衡器請求隊列的彈性來處理突發流量模式——這太棒了!

網絡服務器

最後,我們進入令人興奮的(Ruby)部分:Web 服務器。 我們使用 Ruby on Rails,但這只是一個 Web 框架——實際的 Web 服務器是 Unicorn。 Unicorn 的工作原理是在一台機器上啟動多個工作進程,其中每個工作進程偵聽操作系統套接字以進行工作。 它為我們處理進程管理,並將請求的負載平衡推遲到操作系統本身。 我們只需要我們的 Ruby 代碼盡可能快地處理請求; 在 Ruby 之外對我們來說,其他一切都得到了有效的優化。

由於我們的 SDK 在客戶應用程序中或通過我們的 REST API 發出的大多數請求都是異步的(即,我們不需要等待操作完成即可向客戶返回特定響應),我們的大多數API 服務器非常簡單——它們驗證請求的結構、任何 API 密鑰約束,然後將請求放入 Redis 隊列,如果一切順利,則向客戶端返回 200 響應。

這個請求/響應週期大約需要 10 毫秒來處理 Ruby 代碼,其中一部分用於等待 Memcached 和 Redis。 即使我們用另一種語言重寫所有這些,也不可能從中擠出更多的性能。 而且,最終,正是您到目前為止所閱讀的所有內容的架構,使我們能夠擴展此數據攝取過程以滿足客戶不斷增長的需求。

作業隊列

這是我們過去探討過的一個主題,因此我不會深入探討這方面 - 要了解有關我們的工作排隊系統的更多信息,請查看我關於通過隊列實現彈性的帖子。 在高層次上,我們所做的是利用充當作業隊列的大量 Redis 實例,進一步緩衝需要完成的工作。 與我們的 Web 服務器類似,這些實例分佈在可用區中——以在特定可用區出現問題時提供更高的可用性——並且它們使用 Redis Sentinel 以主/輔助對形式出現以實現冗餘。 我們還可以水平和垂直擴展這些,以優化容量和吞吐量。

工人

這當然是最有趣的部分——我們如何讓工人擴大規模?

首先,我們的工作人員和隊列按多個維度進行細分:客戶、工作類型、所需的數據存儲等。這使我們具有高可用性; 例如,如果特定數據存儲出現問題,其他功能將繼續正常工作。 它還允許我們根據這些維度中的任何一個獨立地自動縮放工人類型。 我們最終能夠以水平可擴展的方式管理工作人員的能力——也就是說,如果我們有更多的某種工作,我們可以擴展更多的工作人員。

在這裡,您可能會開始看到語言或框架選擇很重要。 最終,更高效的工人將能夠更快地完成更多工作。 像 C 或 Rust 這樣的編譯語言在計算任務上往往比像 Ruby 這樣的解釋語言快得多,這可能會在某些工作負載上產生更高效的工作人員。 但是,我花了很多時間查看痕跡,在 Braze 的大局中,原始 CPU 處理只是其中的一小部分。 我們的大部分處理時間都花在等待數據存儲或外部請求的響應上,而不是處理數字; 我們不需要為此進行大量優化的 C 代碼。

數據存儲

到目前為止,我們所涵蓋的所有內容都具有相當大的可擴展性。 因此,讓我們花點時間談談我們的員工將大部分時間花在哪裡——數據存儲。

任何曾經擴展 Web 服務器或使用 SQL 數據庫的異步工作者的人都可能遇到過特定的擴展問題:事務。 您可能有一個負責完成訂單的端點,該端點創建兩個 FulfillmentRequest 和一個 PaymentReceipt。 如果這一切都沒有發生在事務中,您最終可能會得到不一致的數據。 同時在單個數據庫上執行大量事務會導致大量時間花在鎖上,甚至死鎖。 在 Braze,我們通過對象獨立性和最終一致性來直接解決數據模型本身的擴展問題。 使用這些原則,我們可以從數據存儲中擠出大量性能。

獨立數據對象

我們在 Braze 大量使用 MongoDB,原因很充分:也就是說,它使我們能夠大幅水平擴展 MongoDB 分片並獲得近乎線性的存儲和性能提升。 這對我們的用戶配置文件非常有效,因為它們彼此獨立——用戶配置文件之間沒有要維護的 JOIN 語句或約束關係。 隨著我們每個客戶的增長或我們添加新客戶(或兩者兼而有之),我們可以簡單地向現有數據庫添加新數據庫和新分片以增加我們的容量。 我們明確避免使用多文檔事務等功能來保持這種級別的可擴展性。

除了 MongoDB,我們經常使用 Redis 作為臨時數據存儲,用於緩衝分析信息等。 由於許多這些分析的真實來源作為獨立文檔存在於 MongoDB 中一段時間,因此我們維護了一個水平可擴展的 Redis 實例池作為緩衝區; 在這種方法下,散列文檔 ID 用於基於密鑰的分片方案,由於獨立性而均勻分佈負載。 定期作業將這些緩衝區從一個水平擴展的數據存儲刷新到另一個水平擴展的數據存儲。 規模達到!

此外,我們將 Redis Sentinel 用於這些實例,就像我們對上述作業隊列所做的那樣。 我們還為不同的目的部署了許多“類型”的 Redis 集群,為我們提供受控的故障流(即,如果一種特定類型的 Redis 集群出現問題,我們不會看到不相關的功能同時開始失敗)。

最終一致性

Braze 還利用最終一致性作為大多數讀取操作的原則。 這使我們能夠在大多數情況下利用 MongoDB 副本集的主要和次要成員的讀取,從而使我們的架構更加高效。 我們數據模型中的這一原則允許我們在整個堆棧中大量使用緩存。

我們使用 Memcached 的多層方法——基本上,當從數據庫請求文檔時,我們將首先檢查具有非常低生存時間 (TTL) 的機器本地 Memcached 進程,然後檢查遠程 Memcached 實例(使用更高的 TTL),在直接詢問數據庫之前。 這有助於我們顯著減少對常見文檔的數據庫讀取,例如客戶設置或活動詳細信息。 “最終”可能聽起來很可怕,但實際上,它只有幾秒鐘,採用這種方法可以減少大量來自事實來源的流量。 如果您曾經參加過計算機體系結構課程,您可能會認識到這種方法與 CPU L1、L2 和 L3 緩存系統的工作方式有多麼相似!

通過這些技巧,我們可以從我們架構中最慢的部分中擠出大量性能,然後在我們的吞吐量或容量需求增加時適當地水平擴展它。

Ruby 和 Rails 適合的地方

事情是這樣的:事實證明,當您花費大量精力構建一個整體架構時,每個層都可以很好地水平擴展,語言或運行時的速度並沒有您想像的那麼重要。 這意味著語言、框架和運行時的選擇是根據一組完全不同的要求和約束進行的。

當 Braze 於 2011 年成立時,Ruby 和 Rails 在幫助團隊快速迭代方面有著良好的記錄——它們仍然被 GitHub、Shopify 和其他領先品牌使用,因為它繼續使這成為可能。 它們繼續分別由 Ruby 和 Rails 社區積極開發,並且它們仍然擁有大量可滿足各種需求的開源庫。 這對是快速迭代的絕佳選擇,因為它們具有極大的靈活性,並且對於常見的用例保持了大量的簡單性。 我們發現,在我們每天使用它的時候,這都是絕對正確的。

現在,這並不是說 Ruby on Rails 是適合所有人的完美解決方案。 但是在 Braze,我們發現它可以很好地為我們的大部分數據攝取管道、消息發送管道和麵向客戶的儀表板提供動力,所有這些都需要快速迭代,並且是 Braze 成功的核心整個平台。

當我們不使用 Ruby 時

可是等等! 並非我們在 Braze 所做的一切都使用 Ruby。 多年來,出於各種原因,我們在一些地方呼籲將事情轉向其他語言和技術。 讓我們看一下其中的三個,只是為了提供一些額外的見解,以了解我們何時依賴 Ruby 和不依賴 Ruby。

1. 寄件人服務

事實證明,Ruby 並不擅長在單個進程中處理高度並發的網絡請求。 這是一個問題,因為當 Braze 代表我們的客戶發送消息時,一些終端服務提供商可能要求每個用戶發送一個請求。 當我們準備好發送 100 條消息時,我們不想等待每條消息都完成後再繼續下一條。 我們寧願並行完成所有這些工作。

輸入我們的“Sender Services”——即用 Golang 編寫的無狀態微服務。 上面示例中的 Ruby 代碼可以將所有 100 條消息發送到其中一個服務,該服務將並行執行所有請求,等待它們完成,然後向 Ruby 返回一個批量響應。 在並發網絡方面,這些服務比我們使用 Ruby 所做的要高效得多。

2. 電流連接器

我們的 Braze Currents 大容量數據導出功能允許 Braze 客戶將數據持續流式傳輸到我們眾多數據合作夥伴中的一個或多個。 該平台由 Apache Kafka 提供支持,流式傳輸是通過 Kafka 連接器完成的。 您可以在技術上用 Ruby 編寫這些,但官方支持的方式是使用 Java。 而且由於對 Java 的高度支持,在 Java 中編寫這些連接器比在 Ruby 中要容易得多。

3.機器學習

如果你曾經在機器學習方面做過任何工作,你就會知道選擇的語言是 Python。 Python 中用於機器學習工作負載的眾多軟件包和工具使對等的 Ruby 支持黯然失色——TensorFlow 和 Jupyter notebook 之類的東西對我們的團隊很有幫助,而這些類型的工具在 Ruby 世界中根本不存在或沒有很好地建立。 因此,在構建利用機器學習的產品元素時,我們傾向於使用 Python。

當語言很重要

顯然,我們在上面有幾個很好的例子,其中 Ruby 不是理想的選擇。 選擇其他語言的原因有很多——我們認為這裡有一些特別有用的考慮因素。

無需轉換成本即可構建新事物

如果您要構建一個全新的系統,使用新的域模型並且不與現有功能緊密耦合集成,那麼如果您願意,您可能有機會使用不同的語言。 尤其是在您的組織正在評估不同機會的情況下,一個較小的、孤立的新建項目可能是嘗試新語言或框架的一個很好的現實世界實驗。

特定任務的語言生態系統和人機工程學

有些任務使用特定的語言或框架要容易得多——我們特別喜歡 Rails 和 Grape 用於開發儀表板功能,但是用 Ruby 編寫機器學習代碼絕對是一場噩夢,因為開源工具根本不存在。 您可能希望使用特定的框架或庫來實現某種功能或集成,有時您的語言選擇會受到影響,因為它幾乎肯定會帶來更輕鬆或更快的開發體驗。

執行速度

有時,您需要優化原始執行速度,而使用的語言會嚴重影響這一點。 很多高頻交易平台和自動駕駛系統都是用 C++ 編寫的,這是有充分理由的; 本機編譯的代碼可能會非常快! 我們的發送器服務利用了 Golang 的並行/並發原語,正是因為這個原因,這些原語在 Ruby 中根本不可用。

開發者熟悉度

另一方面,您可能正在構建一些孤立的東西,或者有一個想要使用的庫,但是您的團隊其他成員完全不熟悉您的語言選擇。 在 Scala 中引入一個非常傾向於函數式編程的新項目可能會給團隊中的其他開發人員帶來熟悉障礙,這最終會導致知識隔離或淨速度下降。 我們發現這在 Braze 尤為重要,因為我們非常強調快速迭代,因此我們傾向於鼓勵使用組織中已經廣泛使用的工具、庫、框架和語言。

最後的想法

如果我能回到過去,告訴自己關於巨型系統中的軟件工程的一件事,那就是:對於大多數工作負載,您的整體架構選擇將比語言選擇更能定義您的擴展限制和速度。 這種洞察力每天都在 Braze 得到證明。

Ruby 和 Rails 是令人難以置信的工具,當它們成為正確架構的系統的一部分時,它們的擴展性非常好。 Rails 也是一個高度成熟的框架,它支持我們在 Braze 的文化,即快速迭代和產生真正的客戶價值。 這些使 Ruby 和 Rails 成為我們的理想工具,我們計劃在未來幾年繼續使用這些工具。

有興趣在 Braze 工作嗎? 我們正在為我們的工程、產品管理和用戶體驗團隊招聘各種職位。 查看我們的職業頁面,了解更多關於我們的空缺職位和文化的信息。