Categories
程式開發

Salesforce如何連續分析數以萬計的生產服務器


本文最初發佈於Salesforce工程博客,經原作者授權由InfoQ中文站翻譯並分享。

Salesforce CRM應用程序是一個運行在JVM上的多租戶單體應用,整個生產環境運行在數万台服務器上。這些主機每天處理數十億個請求,為不同的租戶處理報告查詢和各種同步或異步任務。每天都有成千上萬的工程師對單庫源代碼進行修改。此外,對底層數據以及Salesforce CRM之上的用戶自定義擴展進行的大量更改,可能會導致我們在生產環境中觀察不同服務器/集群時看到不一致的行為。

挑戰和解決方案

當功能中斷或性能退化時,準確確定在那天的關鍵點上發生了什麼,對於調試非最佳行為至關重要。單個單體應用程序負責處理這麼多不同租戶的多樣化要求,每個工作線程都會有豐富的上下文數據(例如,租戶ID、用戶、URL),為了能夠迅速查明生產環境的問題,必須維護好這些數據。

我們的團隊在Salesforce主要職責是,確保在任何時候都可以從每個生產服務器獲得正確的診斷數據。我們開發了一個完全自主的應用程序性能管理系統,不斷地從所有生產服務器捕獲性能分析和診斷數據。

我們遇到了一些Salesforce應用程序特有的問題,但是,我們相信這些問題的解決方案可能對構建類似系統的工程師有用。這篇文章簡要地描述了一些主要的挑戰和我們採用的解決方案。

擴展性

挑戰

代理在數万個JVM上運行。每個數據中心都有很多主機,從每個主機都會捕獲大量的數據,再加上跨數據中心的網絡延遲,這意味著我們無法有效地將所有數據持久存儲到一個集中化的存儲解決方案。在所有服務器上,每秒寫次數達300多萬次,每一次有幾KB的數據,總計超過每秒1GB。這種速率對於單個網絡或存儲解決方案來說太高了(在合理的成本下)。

解決方案

將負載分佈到多個數據中心,然後從一個可以訪問所有數據中心並知道如何將請求路由到特定數據中心的中心站點協調檢索。用戶請求指定需要哪些主機集群的性能分析數據,中心站點將請求路由到正確數據中心內的相應服務器。這為調查工程師提供了良好的體驗,依靠一個中心站點查看整個站點的所有性能分析數據。我們在存儲中維護一個路由查找表(可以由系統管理員在運行時修改),將集群映射到相應的數據中心。

容錯

挑戰

對於JVM性能分析來說,許多最重要或最有趣的時間段都是在極端狀況下,要么是故障原因,要么是故障徵兆。當慢速請求堆積、客戶工作負載模式改變或貪婪作業分配太多大對象時,內存和CPU會急劇增加。在緊急關頭,JVM可能會宕機或終止,或丟失網絡連接;因此,如果只是將性能分析數據緩衝到內存中,而不是立即將其持久化到某種形式的永久存儲中,那麼性能分析數據可能會丟失。

解決方案

為了避免丟失最重要的數據,同時保持批量保存數據的能力,需要以彈性的方式緩存數據。當應用程序受到威脅時,將緩衝區保存在內存中可能容易丟失數據,因此,我們實現了一個磁盤上的循環緩衝區。從JVM捕獲樣本(線程轉儲和相關上下文)後,立即將它們保存在本地磁盤上。這樣,在服務器或網絡故障時,可以防止在將數據轉發到集中存儲之前丟失緩存數據。為了防止在長時間停機時對磁盤空間產生負面影響,緩衝區的循環特性是必須的,因此,緩衝區會根據配置的時間間隔覆蓋。

多語言運行時支持

挑戰

Salesforce為Apex編程語言提供了一個定制的解釋器,客戶可以使用該解釋器來添加特定於其組織的自定義業務邏輯。在處理生產環境的問題時,能夠分析用戶定義的擴展很有價值,這可以減少服務成本或響應時間。我們的解決方案必須能夠捕獲和表示性能分析和可觀測數據,而不用管底層的語言是什麼。此外,Salesforce運行著各種基於JVM的服務,其中許多都是很好的性能分析候選對象。因此,我們的解決方案必須能夠適應各種各樣的JVM,而不能僅僅適用於CRM的單體應用。

解決方案

我們的系統設計沒有對所使用的編程語言做任何假設。底層實現語言作為另一個元數據附加到所有性能分析數據點,允許用戶基於該語言進行查詢。此外,為了能夠支持不同的語言和環境,實現多語言支持,用於抽象堆棧跟踪和元數據的數據結構是以一種通用且足夠靈活的方式表示的。

上下文元數據

挑戰

通常情況下,領域專屬上下文會觸發調查和調試:報告花費的時間比它應該花費的時間長、請求失敗或花費的時間太長、特定於租戶的性能問題或加載了畸形數據的頁面。允許調試工程師使用這個領域專屬上下文來驅動他們的調查,可以極大地加快得出結論的過程。

如果工程師已經知道花費很長時間才能加載的URL,那麼搜索與該URL相關的堆棧跟踪信息可以讓他們快速地將視野縮小到相關數據。這樣就省去了許多常見的起始步驟,比如根據線程ID搜索日誌和交叉引用URL。

解決方案

我們的實現會針對每個線程收集領域專屬上下文和它收集的堆棧跟踪信息。我們將流程設計得足夠通用,以便每個經過性能分析的服務都可以確定將哪些相關信息映射到每個線程抽樣:一個JVM處理請求,這些請求可能附加了URL、HTTP方法和請求參數;一個JVM運行批處理作業,這些作業可能附加了作業名稱、ID和作業類型。領域專屬上下文與標準性能分析元數據一起存儲,完全索引,並允許在查詢時添加適當的過濾器,以避免額外的干擾。

此外,我們還允許對堆棧跟踪信息進行深度搜索,用戶可以在其中通過正則表達式查找包含特定棧幀的抽樣數據。這是最有用和最受歡迎的查詢參數之一,因為在許多情況下,開發人員只對某些代碼路徑感興趣,而不是對應用程序中執行的所有代碼的抽樣數據感興趣。特性團隊知道其模塊的入口點(接口/API),因此,他們可以使用這些知識驗證其特性在生產環境中的行為,從而提供一個反饋循環來識別潛在的進一步優化機會。

高線程數

挑戰

為了及時處理請求或活動的激增,Salesforce應用程序維護著大量的活動線程池。這將導致一定百分比的活動線程始終處於空閒狀態,不做任何重要的工作。各個服務器上各種類似的情況會使這種影響成倍地增加,因此,任何給定的線程轉儲都至少包含1500個單獨的線程。來自這些線程的數據將很快淹沒我們的存儲基礎設施。

解決方案

拋棄捕獲的那些空閒線程的數據!我們的目標不是在給定時刻完美地表示每個線程,而是表示在給定時刻正在進行的工作。過濾掉那些無所事事地等待工作的線程,或者是在檢查一個值時休眠的線程,可以讓我們更頻繁地分析數據並更長時間地保存數據。在給定的JVM中,我們能夠過濾掉99%的初始線程轉儲。供參考:在整個生產環境中,我們平均每分鐘丟棄數億的堆棧跟踪信息,每分鐘僅保留500萬跟踪信息供以後使用。

壓縮和去重

挑戰

代碼庫經常變化,但是整體來看,它們的變化並不大。此外,在特定的時期內,最常執行的代碼基本上沒有變化。因此,線程轉儲包含大量的重複數據。這就提出了一個挑戰,將性能分析數據直接捕獲並存儲會導致巨大但合理的存儲需求。

解決方案

將數據分割成離散的部分,並跟踪數據的關係,使我們能夠盡可能減少存儲線程轉儲時的重複。選擇正確的存儲解決方案和正確的存儲模式至關重要,這樣構建的系統可以減少重複,從而使存儲這些數據成為可能。我們在HBase上使用Apache Phoenix構建了相關的表。每個數據中心只存儲每個堆棧跟踪信息幀一次,並且為了實現快速查找建立了索引。堆棧跟踪信息也只存儲一次。堆棧跟踪信息以棧幀ID列表的形式存儲,並根據它們的散列ID值建立索引。在給定的線程轉儲中,各個線程抽樣被存儲為時間序列事件,堆棧跟踪信息記錄為對散列ID值的引用。所有這些結合起來,使我們減少了堆棧跟踪信息(任何給定線程轉儲的最大部分)存儲的空間佔用。

為了減少HBase集群上的寫入負載,我們保留了可配置的堆棧跟踪信息和棧幀緩存。我們可以檢查這些緩存,看看堆棧跟踪信息是否已經寫入,而不是寫入每條堆棧跟踪信息或棧幀。這些緩存消除了99%的棧幀寫入和75%的堆棧跟踪信息寫入。

回歸識別

挑戰

對應用程序(例如新版本)進行更改後的回歸檢測和識別是我們希望解決的主要問題之一。回歸可能是由於對特定代碼路徑的調用頻率增加,或者是由於子系統的運行時性能下降。

解決方案

通過將性能分析數據保存較長的時間(我們目前的TTL為90天),我們的系統支持在不同的版本、庫和平台升級以及對軟件的其他(較長時間有效)更改之間進行比較分析。

可以在運行時進行短時間(少於一小時)的比較分析。這些數據被表示為火焰圖和樹形差異圖。這種可視化使得工程師可以很容易地識別代碼路徑的差異,在他們調查的時間段內找出罪魁禍首。

此外,對於時間跨度較大的查詢,因為通過如此大量的數據在運行時確定回歸是不可行的,所以我們開發了一種通用的作業和報錶框架,用戶可以定義和調度在所有數據集上執行的作業,作業結果會被持久化,並且可以從UI上查看。作業框架內置支持與Salesforce其他內部工具的交互,使我們能夠將性能分析數據與日誌、系統級和應用級監控數據、站點可靠性工具等關聯起來。

未來工作

我們正在尋求集成Java Flight Recorder,以添加CPU性能分析以及提供更高的採樣率支持。這將使我們可以準確地度量在不同代碼路徑上花費的CPU週期的數量,幫助我們關聯成本與系統中單個組件的服務價值。此外,我們正在考慮與Async Profiler集成,以便為非JVM應用程序提供更好的支持。

我們的下一篇博文將介紹使用診斷數據收集代理捕獲可觀測數據的方法。該文將詳細介紹我們如何設計一個低開銷、可配置的代理來從生產服務器收集系統和應用程序診斷數據。

原文鏈接:

How to Continuously Profile Tens of Thousands of Production Servers