我們公司有個面向服務的架構。其中一個服務是字型服務,字型體系和 unicode 編碼範圍(unicode range)提供字型資料,為使用者上傳的字型驗證許可權。我們沒想到這個字型服務會有很高的負載1(負載是指執行緒消耗和 CPU 等待的平均值)。但是去年我們注意到字型服務出人意料地出現高負載,尤其是晚上我們沒什麼流量的時候。幸運的是,我們發現了這一問題的根本原因,並大幅提升了字型服務的效能和系統整體的穩定性。以下內容是我們的優化過程。
用火焰圖除錯
我一個同事從 Netflix 公司的 Brendan Gregg 那裡發現並部署了一個小巧的火焰圖工具。這個工具可以把多個效能檢測工具的資料結合起來,生成一張本地方法和 JVM 方法資源使用情況的圖象。圖裡每一個矩形都代表一個棧幀,矩形的寬度代表資源的使用情況,如 CPU 時間,y 軸代表呼叫棧。你可以簡單地通過找出比較寬的矩形來定位出問題所在。這個工具對於除錯字型服務的效能是非常寶貴的。
我們收集了幾張字型服務高負載狀態下的火焰圖。下面展示了其中一張,圖裡有一處 JVM 部分幾乎到頂。我們很快注意到這些圖的規律。絕大部分時間都是消耗在 libz.so(用於 gzip 壓縮和解壓)上,而 JVM 裡的絕大部分時間都是消耗在 XML 轉義和 UTF-8 編碼上。
為什麼這麼慢
首先,介紹一點關於我們字型服務如何工作的背景。我們將字型資料儲存在 Amazon S3 容器中,每一種字型的每一 unicode 編碼範圍(unicode range)是一個獨立的物件。其他服務會為字型體系、一系列 unicode 編碼範圍(unicode range)或者使用者來請求字型資料。字型服務會下載使用者請求的字型體系中特定 unicode 編碼範圍(unicode range)的資料,並返回包含這所有資料的 XML。
這一功能非常簡單,並沒有什麼明顯密集計算的東西。然而,我們遇到了高負載。我們在火焰圖的幫助下發現 libz、XML 轉義和 UTF8 編碼佔用了 CPU 大量資源。但是為什麼我們花了這麼多時間在編碼和壓縮上呢?記得我剛才說的,晚上負載最高嗎?我們的晚上(美國山區標準時)在亞洲是白天。每當晚上我們本地沒什麼流量的時候,大量其他地區的使用者正在用亞洲語言的 unicode 編碼範圍(unicode range),比如中文、日語和韓語。事實上,相比起來這些類別的字型資料是非常龐大的。這些資料通過 gzip 解壓、UTF-8 解碼然後 XML 轉義、UTF-8 編碼最後 gzip 壓縮。對於體積很小的基本拉丁文類別,這一過程沒什麼。然而,CJK 類別比基本拉丁文類別大了兩個數量級(1MB 對比 60KB)。對於這些體積大的字型類別,這所有的轉換都使得 CPU 吃不消。Gzip 壓縮和解壓相對很耗資源,XML 轉義也沒有那麼快。
怎樣加速
字型服務響應的內容本質上只是來自 S3 的原始資料。字型服務的確做了其他重要的工作,比如許可權驗證,查詢字型關鍵字。但是沒有理由必須讓字型服務從 S3 轉發字型資料。我們的解決方案非常簡單粗暴,直接返回一系列包含字型資料的 S3 物件的連結,而不是通過字型服務下載然後重新編碼字型資料。
這一修改幾乎將字型伺服器的負載降為 0(見圖1)。客戶端伺服器也察覺不到任何影響。儘管我們第一次嘗試非常成功,但我也應該記住,我們部署的同時增加了功能釋出控制,它可以讓我們在 100% 啟用前,先啟用一定比例的請求來測試它能夠正常工作。
結論
通過對生產環境伺服器的監控,我們能夠定位並刪除伺服器上沒必要的功能。下面是我們這次經歷中幾個關鍵的步驟:
- 用火焰圖等效能檢測工具定位那些霸佔 CPU 的功能方法。
- 壓縮和其他編碼也會非常耗資源。
- 如果客戶端能夠直接獲取到資料,那麼直接發給它一個連線而非轉發資料會提升整體的效能。(宣告:這並不是靈丹妙藥,有些情況下會對客戶端效能造成損失,因為它必須要做二次請求)。
#1: 負載是指執行緒消耗和 CPU 等待的平均值。見 Wikipedia。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式