在內部構建中央日誌記錄服務
已發表: 2022-03-10我們都知道調試對於提高應用程序性能和功能的重要性。 BrowserStack 每天在高度分佈式的應用程序堆棧上運行一百萬個會話! 每個都涉及多個移動部分,因為客戶端的單個會話可以跨越多個地理區域的多個組件。
如果沒有正確的框架和工具,調試過程可能是一場噩夢。 在我們的案例中,我們需要一種方法來收集在每個流程的不同階段發生的事件,以便深入了解會話期間發生的所有事情。 使用我們的基礎架構,解決這個問題變得很複雜,因為每個組件在處理請求的生命週期中可能有多個事件。
這就是為什麼我們開發了自己的內部中央日誌記錄服務工具 (CLS) 來記錄會話期間記錄的所有重要事件。 這些事件可幫助我們的開發人員識別會話中出現問題的條件,並幫助跟踪某些關鍵產品指標。
調試數據的範圍從 API 響應延遲等簡單的事情到監控用戶的網絡健康狀況。 在本文中,我們分享了構建 CLS 工具的故事,該工具每天從 100 多個組件可靠、大規模地收集 70G 的相關時序數據,並使用兩個 M3.large EC2 實例。
內部建設的決定
首先,讓我們考慮一下為什麼我們在內部構建了 CLS 工具,而不是使用現有的解決方案。 我們的每個會話平均發送 15 個事件,從多個組件到服務 - 轉化為每天大約 1500 萬個事件總數。
我們的服務需要能夠存儲所有這些數據。 我們尋求一個完整的解決方案來支持跨事件的事件存儲、發送和查詢。 當我們考慮第三方解決方案(例如 Amplitude 和 Keen)時,我們的評估指標包括成本、處理高並行請求的性能和易於採用。 不幸的是,我們無法在預算內找到滿足我們所有要求的合適方案 - 儘管好處包括節省時間和最大限度地減少警報。 雖然這需要額外的努力,但我們決定自己開發一個內部解決方案。
技術細節
在我們組件的架構方面,我們概述了以下基本要求:
- 客戶表現
不影響發送事件的客戶端/組件的性能。 - 規模
能夠並行處理大量請求。 - 服務表現
快速處理髮送給它的所有事件。 - 洞察數據
每個記錄的事件都需要有一些元信息,以便能夠唯一地標識組件或用戶、帳戶或消息,並提供更多信息以幫助開發人員更快地調試。 - 可查詢接口
開發人員可以查詢特定會話的所有事件,幫助調試特定會話、構建組件健康報告或生成有意義的系統性能統計數據。 - 更快、更容易採用
與現有或新組件輕鬆集成,不會給團隊帶來負擔並佔用他們的資源。 - 低維護
我們是一個小型工程團隊,因此我們尋求一種解決方案來最大限度地減少警報!
構建我們的 CLS 解決方案
決策 1:選擇要公開的接口
在開發 CLS 時,我們顯然不想丟失任何數據,但我們也不希望組件性能受到影響。 更不用說防止現有組件變得更加複雜的額外因素,因為它會延遲整體採用和發布。 在確定我們的界面時,我們考慮了以下選擇:
- 將事件存儲在每個組件的本地 Redis 中,作為後台處理器將其推送到 CLS。 但是,這需要對所有組件進行更改,並為尚未包含 Redis 的組件引入 Redis。
- 發布者 - 訂閱者模型,其中 Redis 更接近 CLS。 當每個人都發布事件時,我們再次考慮到組件在全球範圍內運行的因素。 在高流量期間,這會延遲組件。 此外,此寫入可能會間歇性地跳到五秒鐘(僅由於互聯網)。
- 通過 UDP 發送事件,這對應用程序性能的影響較小。 在這種情況下,數據將被發送和遺忘,但是,這裡的缺點是數據丟失。
有趣的是,我們在 UDP 上的數據丟失率不到 0.1%,這對於我們考慮構建這樣的服務來說是可以接受的。 我們能夠讓所有團隊相信,這樣的損失是值得的,並繼續利用 UDP 接口來監聽所有發送的事件。
雖然一個結果是對應用程序性能的影響較小,但我們確實遇到了一個問題,因為 UDP 流量不允許來自所有網絡,主要來自我們的用戶 - 導致我們在某些情況下根本沒有收到任何數據。 作為一種解決方法,我們支持使用 HTTP 請求記錄事件。 來自用戶端的所有事件都將通過 HTTP 發送,而從我們的組件記錄的所有事件都將通過 UDP 發送。
決策 2:技術堆棧(語言、框架和存儲)
我們是一家紅寶石商店。 但是,我們不確定 Ruby 是否會成為我們特定問題的更好選擇。 我們的服務必須處理大量傳入請求,以及處理大量寫入。 使用全局解釋器鎖,在 Ruby 中實現多線程或併發將是困難的(請不要冒犯 - 我們喜歡 Ruby!)。 所以我們需要一個解決方案來幫助我們實現這種並發。
我們還熱衷於在我們的技術堆棧中評估一種新語言,這個項目似乎非常適合嘗試新事物。 那時我們決定試一試 Golang,因為它提供了對並發和輕量級線程和 go-routines 的內置支持。 每個記錄的數據點都類似於一個鍵值對,其中“鍵”是事件,“值”作為其關聯值。
但是只有一個簡單的鍵和值不足以檢索與會話相關的數據——它還有更多的元數據。 為了解決這個問題,我們決定任何需要記錄的事件都會有一個會話 ID 以及它的鍵和值。 我們還添加了時間戳、用戶 ID 和記錄數據的組件等額外字段,以便更容易獲取和分析數據。
現在我們決定了我們的有效負載結構,我們必須選擇我們的數據存儲。 我們考慮過 Elastic Search,但我們也希望支持密鑰的更新請求。 這會觸發整個文檔被重新索引,這可能會影響我們寫入的性能。 MongoDB 作為數據存儲更有意義,因為它更容易根據將添加的任何數據字段查詢所有事件。 這很容易!
決策 3:數據庫大小很大,查詢和歸檔很糟糕!
為了減少維護,我們的服務必須處理盡可能多的事件。 鑑於 BrowserStack 發布功能和產品的速度,我們確信我們的活動數量會隨著時間的推移以更高的速度增加,這意味著我們的服務必須繼續保持良好的性能。 隨著空間的增加,讀取和寫入需要更多時間——這可能會對服務的性能造成巨大影響。
我們探索的第一個解決方案是將某個時期的日誌從數據庫中移出(在我們的例子中,我們決定為 15 天)。 為此,我們每天創建一個不同的數據庫,讓我們無需掃描所有書面文檔即可查找早於特定時期的日誌。 現在我們不斷地從 Mongo 中刪除超過 15 天的數據庫,同時保留備份以防萬一。
唯一剩下的部分是用於查詢會話相關數據的開發人員界面。 老實說,這是最容易解決的問題。 我們提供了一個 HTTP 接口,人們可以在其中查詢 MongoDB 中相應數據庫中與會話相關的事件,以獲取具有特定會話 ID 的任何數據。
建築學
讓我們談談服務的內部組件,考慮以下幾點:
- 如前所述,我們需要兩個接口——一個通過 UDP 偵聽,另一個通過 HTTP 偵聽。 因此,我們構建了兩台服務器,每個接口也各一台,用於監聽事件。 一旦事件到達,我們就會對其進行解析以檢查它是否具有必需的字段——這些是會話 ID、鍵和值。 如果沒有,則丟棄數據。 否則,數據將通過 Go 通道傳遞到另一個 goroutine,其唯一職責是寫入 MongoDB。
- 這裡一個可能的問題是寫入 MongoDB。 如果寫入 MongoDB 的速度比接收數據的速度慢,則會產生瓶頸。 反過來,這會使其他傳入事件餓死,並意味著數據丟失。 因此,服務器應該快速處理傳入的日誌並準備好處理即將到來的日誌。 為了解決這個問題,我們將服務器分成兩部分:第一部分接收所有事件並將它們排隊等待第二部分,第二部分處理並將它們寫入 MongoDB。
- 對於排隊,我們選擇了 Redis。 通過將整個組件分成這兩部分,我們減少了服務器的工作量,給它處理更多日誌的空間。
- 我們使用 Sinatra 服務器編寫了一個小型服務來處理使用給定參數查詢 MongoDB 的所有工作。 當開發人員需要特定會話的信息時,它會向他們返回 HTML/JSON 響應。
所有這些進程都在單個m3.large實例上愉快地運行。
功能請求
隨著我們的 CLS 工具隨著時間的推移越來越多的使用,它需要更多的功能。 下面,我們將討論這些以及它們是如何添加的。
缺少元數據
隨著 BrowserStack 中組件數量的逐漸增加,我們對 CLS 的要求也越來越高。 例如,我們需要能夠記錄來自缺少會話 ID 的組件的事件。 否則,獲得一個會以影響應用程序性能和在我們的主服務器上產生流量的形式給我們的基礎設施帶來負擔。
我們通過使用其他鍵(例如終端和用戶 ID)啟用事件記錄來解決此問題。 現在,無論何時創建或更新會話,CLS 都會收到會話 ID 以及相應的用戶和終端 ID 的通知。 它存儲了一個可以通過寫入 MongoDB 的過程來檢索的映射。 每當檢索到包含用戶或終端 ID 的事件時,都會添加會話 ID。
處理垃圾郵件(其他組件中的代碼問題)
CLS 也面臨處理垃圾郵件事件的常見困難。 我們經常發現組件中的部署會生成大量發送到 CLS 的請求。 其他日誌將在此過程中受到影響,因為服務器變得太忙而無法處理這些日誌並且重要的日誌被丟棄了。
在大多數情況下,記錄的大部分數據都是通過 HTTP 請求進行的。 為了控制它們,我們在 nginx 上啟用了速率限制(使用 limit_req_zone 模塊),它會阻止來自我們發現的任何 IP 的請求,在很短的時間內命中超過一定數量的請求。 當然,我們確實會利用所有被阻止 IP 的健康報告並通知負責的團隊。
規模 v2
隨著我們每天的會話增加,記錄到 CLS 的數據也在增加。 這影響了我們的開發人員每天運行的查詢,很快我們遇到的瓶頸就是機器本身。 我們的設置包括運行上述所有組件的兩台核心機器,以及一組用於查詢 Mongo 並跟踪每個產品的關鍵指標的腳本。 隨著時間的推移,機器上的數據大量增加,腳本開始佔用大量 CPU 時間。 即使在嘗試優化 Mongo 查詢之後,我們總是會遇到同樣的問題。
為了解決這個問題,我們添加了另一台機器來運行健康報告腳本和查詢這些會話的接口。 該過程涉及啟動一台新機器並設置在主機上運行的 Mongo 的從屬設備。 這有助於減少我們每天看到的由這些腳本引起的 CPU 峰值。
結論
隨著數據量的增加,為像數據記錄這樣簡單的任務構建服務可能會變得複雜。 本文討論了我們探索的解決方案,以及解決此問題時面臨的挑戰。 我們對 Golang 進行了試驗,看看它與我們的生態系統的契合程度,到目前為止,我們已經很滿意了。 我們選擇創建內部服務而不是支付外部服務費用非常划算。 直到很久以後,我們也不必將我們的設置擴展到另一台機器 - 當我們的會話量增加時。 當然,我們開發 CLS 的選擇完全基於我們的需求和優先級。
如今,CLS 每天處理多達 1500 萬個事件,構成多達 70 GB 的數據。 這些數據用於幫助我們解決客戶在任何會話期間面臨的任何問題。 我們還將這些數據用於其他目的。 鑑於每個會話的數據提供的關於不同產品和內部組件的見解,我們已經開始利用這些數據來跟踪每個產品。 這是通過提取所有重要組件的關鍵指標來實現的。
總而言之,我們在構建自己的 CLS 工具方面取得了巨大成功。 如果這對您有意義,我建議您考慮這樣做!