Dubbo Metrics 釋出新版本 2.0.1 | Dubbo 的度量統計基礎設施
對服務進行實時監控,瞭解服務當前的執行指標和健康狀態,是微服務體系中不可或缺的環節。Metrics 作為微服務的重要元件,為服務的監控提供了全面的資料基礎。 近日,Dubbo Metrics 釋出了2.0.1版本,本文將為您探祕 Dubbo Metrics 的起源,及 7 大改進。
Dubbo Metrics 的起源
Dubbo Metrics(原Alibaba Metrics)是阿里巴巴集團內部廣泛使用的度量埋點基礎類庫,有 Java 和 Node.js 兩個版本,目前開源的是 Java 版本。內部版本誕生於2016年,經歷過近三年的發展和雙十一的考驗,已經成為阿里巴巴集團內部微服務監控度量的事實標準,覆蓋了從系統、JVM、中介軟體到應用各層的度量,並且在命名規則、資料格式、埋點方式和計算規則等方面,形成了一套統一的規範。
Dubbo Metrics 的程式碼是基於 Dropwizard Metrics 衍生而來,版本號是3.1.0,當時決定 fork 到內部進行定製化開發的主要原因有兩個。
一是社群的發展非常緩慢,3.1.0之後的第3年才更新了下一個版本,我們擔心社群無法及時響應業務需求;另一個更重要的原因是當時的3.1.0還不支援多維度的 Tag,只能基於 a.b.c 這樣傳統的指標命名方法,這就意味著 Dropwizard Metrics 只能在單維度進行度量。然後,在實際的業務過程中,很多維度並不能事先確定,而且在大規模分散式系統下,資料統計好以後,需要按照各種業務維度進行聚合,例如按機房、分組進行聚合,當時的 Dropwizard 也無法滿足,種種原因使得我們做了一個決定,內部fork一個分支進行發展。
Dubbo Metrics 做了哪些改進
相對於 Dropwizard Metrics ,Dubbo Metrics 做的改進主要有以下幾個方面:
一、引入基於 Tag 的命名規範
如前文所描述,多維度 Tag 在動態埋點,資料聚合等方面相對於傳統的 metric 命名方法具有天然的優勢,這裡舉一個例子,要統計一個 Dubbo 服務 DemoService 呼叫次數和 RT,假設這個服務叫做 DemoService,那麼按照傳統的命名方式,通常會命名為 dubbo.provider.DemoService.qps
和 dubbo.provider.DemoService.rt
。如果只有1個服務的話,這種方法並無太大的問題,但是如果一個微服務應用同時提供了多個 Dubbo 的 Service,那麼要聚合統計所有Service 的 QPS 和 RT 就比較困難了。由於 metric 資料具有天然的時間序列屬性,因此資料非常適合儲存到時間序列資料庫當中,要統計所有的 Dubbo 服務的 QPS,那麼就需要查詢所有名稱為dubbo.provider.*.qps的指標,然後對他們進行求和。由於涉及到模糊搜尋,這對於絕大部分資料庫的實現都是比較費時的。如果要統計更加詳細的 Dubbo 方法級別的 QPS 和 RT,那麼實現上就會更加複雜了。
通過引入Tag的概念,可以很好的解決了這個問題,我們將 metrics 分為兩個部分,Key 和 Tag。
-
Metric Key:用英文點號分隔的字串,來表徵這個指標的含義;
-
Metric Tag:定義了這個指標的不同切分維度,可以是單個,也可以是多個;
-
tag key:用於描述維度的名稱;
-
tag value:用於描述維度的值;
-
同時,考慮到一個公司所有微服務產生的所有指標,都會統一彙總到同一個平臺進行處理,因此Metric Key 的命名方式為應當遵循同一套規範,避免發生命名衝突,其格式為 appname.category[.subcategory]*.suffix
-
appname: 應用名;
-
category: 這個指標在該應用下的分類,多個單詞用'_'連線,字母採用小寫;
-
subcategory: 這個指標在該應用下的某個分類下的子分類,多個單詞用'_'連線,字母採用小寫;
-
suffix: 這個關鍵的字尾描述了這個指標所度量的具體型別,可以是計數,速率,或者是分佈;
在上述例子中,同樣的指標可以命名為 dubbo.provider.service.qps{service="DemoService"}
,其中前面部分的名稱是固定的,不會變化,括號裡面的Tag,可以動態變化,甚至增加更多的維度,例如增加 method 維度 dubbo.provider.service.qps{service="DemoService",method="sayHello"}
,也可以是機器的 IP、機房資訊等。這樣的資料儲存是時間序列資料庫親和的,基於這些資料可以輕鬆實現任意維度的聚合,篩選等操作。
P.s. 2017年12月底,Dropwizard Metrics4.0 開始支援 Tag,Dubbo Metrics 中 ag 的實現參考了Dropwizard。spring-boot 2.0中提供的 MicroMeter 和 Prometheus 也均已引入了 Tag 的概念。
二、新增精準統計功能
Dubbo Metrics 的精準統計是和 Dropwizard,或者其他開源專案埋點統計庫實現不太一樣的地方。下面分別通過時間視窗的選擇和吞吐率統計方式這兩個緯度進行闡述。
在統計吞吐率(如 QPS)的時候,Dropwizard的實現方式是滑動視窗+指數加權移動平均,也就是所謂的EWMA,在時間視窗上只提供1分鐘、5分鐘、15分鐘的選擇。
固定視窗 vs 滑動視窗
在資料統計的時候,我們需要事先定義好統計的時間視窗,通常有兩種確立時間視窗的方法,分別是固定視窗和滑動視窗。
固定時間視窗指的是以絕對時間為參考座標系,在一個絕對時間視窗內進行統計,無論何時訪問統計資料,時間視窗不變;而滑動視窗則是以當前時間為參考系,從當前時間往前推一個指定的視窗大小進行統計,視窗隨著時間,資料的變化而發生變化。
固定視窗的優點在於:一是視窗不需要移動,實現相對簡單;二是由於不同的機器都是基於相同的時間視窗,叢集聚合起來比較容易,只需按照相同的時間視窗聚合即可。其缺點是:如果視窗較大,實時性會受到影響,無法立即得到當前視窗的統計資訊。例如,如果視窗為1分鐘,則必須等到當前1分鐘結束,才能得到這1分鐘內的統計資訊。
滑動視窗的優點在於實時性更好,在任意時刻,能都看到當前時刻往前推演一個時間視窗內統計好的資訊。相對而言,由於不同機器的採集時刻不同,要把不同機器上的資料聚合到一起,則需要通過所謂的 Down-Sampling 來實現。即按照固定時間視窗把視窗內收集到的資料應用到某一個聚合函式上。舉個例子來說,假設叢集有5臺機器,按照15秒的頻率按照平均值進行 Down-Sampling,若在00:00~00:15的時間視窗內,在00:01,00:03,00:06,00:09,00:11各收集到一個指標資料,則把這5個點的加權平均認為是00:00這個點的經過 Down- Sampling 之後的平均值。
但在我們的實踐過程中,滑動視窗仍會遇到了以下問題:
-
很多業務場景都要求精確的時間視窗的資料,比如在雙11的時候,想知道雙11當天0點第1秒建立了多少訂單,這種時候 Dropwizard 的滑動視窗很明顯就不適用了。
-
Dropwizard 提供的視窗僅僅是分鐘級,雙11的場景下需要精確到秒級。
-
叢集資料聚合的問題,每臺機器上的滑動時間視窗可能都不一樣,資料採集的時間也有間隔,導致聚合的結果並不準確。
為了解決這些問題,Dubbo Metrics 提供了 BucketCounter 度量器,可以精確統計整分整秒的資料,時間視窗可以精確到1秒。只要每臺機器上的時間是同步的,那麼就能保證叢集聚合後的結果是準確的。同時也支援基於滑動視窗的統計。
瞬時速率(Rate) vs 指數移動加權平均(EWMA)
經過多年的實踐,我們逐漸發現,使用者在觀察監控的時候,首先關注的其實是叢集資料,然後才是單機資料。然而單機上的吞吐率其實並沒有太大意義。怎麼理解呢?
比如有一個微服務,共有2臺機器,某個方法在某一段時間內產生了5次呼叫,所花的時間分別是機器1上的[5,17],機器2上的[6,8,8](假設單位為毫秒)。如果要統計叢集範圍內的平均 RT,一種方法可以先統計單機上的平均 RT,然後統計整體的平均 RT,按照這種方法,機器1上平均 RT 為11ms,機器2的平均 RT 為7.33ms,兩者再次平均後,得到叢集平均 RT 為9.17ms,但實際的結果是這樣嗎?
如果我們把機器1和機器2上的資料整體拉到一起整體計算的話,會發現實際的平均 RT 為(5+17+6+8+8)/5=8.8ms,兩者相差很明顯。而且考慮到計算浮點數的精度丟失,以及叢集規模擴大,這一誤差會愈加明顯。因此,我們得出一個結論:單機上的吞吐率對於叢集吞吐率的計算沒有意義,僅在在單機維度上看才是有意義的。
而 Dropwizard 提供的指數加權移動平均其實也是一種平均,同時考慮了時間的因素,認為距離當前時間越近,則資料的權重越高,在時間拉的足夠長的情況下,例如15分鐘,這種方式是有意義的。而通過觀察發現,其實在較短的時間視窗內,例如1s、5s,考慮時間維度的權重並沒有太大的意義。因此在內部改造的過程中,Dubbo Metrics 做了如下改進:
-
提供瞬時速率計算,反應單機維度的情況,同時去掉了加權平均,採用簡單平均的方式計算
-
為了叢集聚合需要,提供了時間視窗內的總次數和總 RT 的統計,方便精確計算叢集維度的吞吐率
三、極致效能優化
在大促場景下,如何提升統計效能,對於 Dubbo Metrics 來說是一個重要話題。在阿里的業務場景下,某個統計介面的 QPS 可能達到數萬,例如訪問 Cache 的場景,因此這種情況下 metrics 的統計邏輯很可能成為熱點,我們做了一些針對性的優化:
高併發場景下,資料累加表現最好的就是 java.util.concurrent.atomic.LongAdder
,因此幾乎所有的操作最好都會歸結到對這個類的操作上。
避免呼叫 LongAdder#reset
當資料過期之後,需要對資料進行清理,之前的實現裡面為了重用物件,使用了 LongAdder#reset
進行清空,但實測發現 LongAdder#reset
其實是一個相當耗費cpu的操作,因此選擇了用記憶體換 CPU,當需要清理的時候用一個新的 LongAdder 物件來代替。
去除冗餘累加操作
某些度量器的實現裡面,有些統計維度比較多,需要同時更新多個 LongAdder,例如 DropwizardMetrics的 meter 實現裡面計算1分/5分/15分移動平均,每次需要更新3個 LongAdder,但實際上這3次更新操作是重複的,只需要更新一次就行了。
RT為0時避免呼叫Add方法
大多數場景下對 RT 的統計都以毫秒為單位,有些時候當 RT 計算出來小於1ms的時候,傳給metrics的 RT 為0。當我們發現 JDK 原生的 LongAdder 並沒有對 add(0)
這個操作做優化,即使輸入為0,還是把邏輯都走一遍,本質上呼叫的是 sun.misc.Unsafe.UNSAFE.compareAndSwapLong
。如果這個時候,metrics 判斷 RT 為0的時候不對計數器進行 Add 操作,那麼可以節省一次 Add 操作。這對於併發度較高的中介軟體如分散式快取很有幫助,在我們內部某個應用實測中發現,在30%的情況下,訪問分散式快取的 RT 都是0ms。通過這個優化可以節約大量無意義的更新操作。
QPS 和 RT 合併統計
只需要對一個Long的更新,即可實現同時對呼叫次數和時間進行統計,已經逼近理論上的極限。
經過觀測發現,通常對於中介軟體的某些指標,成功率都非常高,正常情況下都在100%。為了統計成功率,需要統計成功次數和總次數,這種情況下幾乎一樣多,這樣造成了一定的浪費,白白多做了一次加法。而如果反過來,只統計失敗的次數,只有當失敗的情況才會更新計數器,這樣能大大降低加法操作。
事實上,如果我們把每一種情況進行正交拆分,例如成功和失敗,這樣的話,總數可以通過各種情況的求和來實現。這樣能進一步確保一次呼叫只更新一次計數。
但別忘了,除了呼叫次數,還有方法執行 RT 要統計。還能再降低嗎?
答疑是可以的!假設 RT 以毫秒為單位進行統計,我們知道1個 Long 有64個bits(實際上Java裡面的Long是有符號的,所以理論上只有63個 bits 可用),而 metrics 的一個統計週期最多隻統計60s的資料,這64個 bits 無論怎樣用都是用不掉的。那能不能把這63個 bits 拆開來,同時統計 count 和 RT 呢?實際上是可以的。
我們把一個 Long 的63個 bits 的高25位用來表示一個統計週期內的總 count,低38位用於表示總RT。
------------------------------------------ |1 bit|25 bit|38 bit | | signed bit |total count |total rt | ------------------------------------------
當一次呼叫過來來的時候,假設傳過來的 RT 是n,那麼每次累加的數不是1,也不是n,而是
1 * 2^38 + n
這麼設計主要有一下幾個考慮:
-
count是每呼叫一次加一,RT 是每呼叫一次加N的操作,如果 count 在高位的話,每次加一,實際是一個固定的常數,而如果rt放在高位的話,每次都加的內容不一樣,所以每次都要計算一次;
-
25 bits 最多可以表示 2^25 = 33554432 個數,所以1分鐘以內對於方法呼叫的統計這種場景來說肯定是不會溢位的;
-
RT 可能會比較大,所以低位給了38bits, 2^38=274877906944 基本也是不可能超的。
如果真的overflow了怎麼辦?
由於前面分析過,幾乎不可能overflow,因此這個問題暫時沒有解決,留待後面在解決。
無鎖 BucketCounter
在之前的程式碼中,BucketCounter 需要確保在多執行緒併發訪問下保證只有一個執行緒對 Bucket 進行更新,因此使用了一個物件鎖,在最新版本中,對 BucketCounter 進行了重新實現,去掉了原來實現中的鎖,僅通過 AtomicReference 和 CAS 進行操作,本地測試發現效能又提升了15%左右。
四、全面的指標統計
Dubbo Metrics 全面支援了從作業系統,JVM,中介軟體,再到應用層面的各級指標,並且對統一了各種命名指標,可以做到開箱即用,並支援通過配置隨時開啟和關閉某類指標的收集。目前支援的指標,主要包括:
-
作業系統
支援Linux/Windows/Mac,包含CPU/Load/Disk/Net Traffic/TCP。
-
JVM
支援classload, GC次數和時間, 檔案控制代碼,young/old區佔用,執行緒狀態, 堆外記憶體,編譯時間,部分指標支援自動差值計算。
-
中介軟體
-
Tomcat: 請求數,失敗次數,處理時間,傳送接收位元組數,執行緒池活躍執行緒數等;
-
Druid: SQL 執行次數,錯誤數,執行時間,影響行數等;
-
Nginx: 接受,活躍連線數,讀,寫請求數,排隊數,請求QPS,平均 RT 等;
後續會陸續新增對Dubbo/Nacos/Sentinel/Fescar等的支援。
五、REST支援
Dubbo Metrics 提供了基於 JAX-RS 的 REST 介面暴露,可以輕鬆查詢內部的各種指標,既可以獨立啟動HTTP Server提供服務(預設提供了一個基於Jersey+ sun Http server的簡單實現),也可以嵌入已有的HTTP Server進行暴露指標。具體的介面可以參考這裡:
https://github.com/dubbo/metrics/wiki/query-from-http
六、單機資料落盤
資料如果完全存在記憶體裡面,可能會出現因為拉取失敗,或者應用本身抖動,導致資料丟失的可能。為了解決該問題,metrics引入了資料落盤的模組,提供了日誌方式和二進位制方式兩種方式的落盤。
-
日誌方式預設通過JSON方式進行輸出,可以通過日誌元件進行拉取和聚合,檔案的可讀性也比較強,但是無法查詢歷史資料;
-
二進位制方式則提供了一種更加緊湊的儲存,同時支援了對歷史資料進行查詢。目前內部使用的是這種方式。
七、易用性和穩定性優化
-
將埋點的API和實現進行拆分,方便對接不用的實現,而使用者無需關注;
-
支援註解方式進行埋點;
-
借鑑了日誌框架的設計,獲取度量器更加方便;
-
增加Compass/FastCompass,方便業務進行常用的埋點,例如統計qps,rt,成功率,錯誤數等等指標;
-
Spring-boot-starter,即將開源,敬請期待;
-
支援指標自動清理,防止長期不用的指標占用記憶體;
-
URL 指標收斂,最大值保護,防止維度爆炸,錯誤統計導致的記憶體。
如何使用
使用方式很簡單,和日誌框架的Logger獲取方式一致。
Counter hello = MetricManager.getCounter("test", MetricName.build("test.my.counter")); hello.inc();
支援的度量器包括:
-
Counter(計數器)
-
Meter(吞吐率度量器)
-
Histogram(直方分佈度量器)
-
Gauge(瞬態值度量器)
-
Timer(吞吐率和響應時間分佈度量器)
-
Compass(吞吐率, 響應時間分佈, 成功率和錯誤碼度量器)
-
FastCompass(一種快速高效統計吞吐率,平均響應時間,成功率和錯誤碼的度量器)
-
ClusterHistogram(叢集分位數度量器)
後續規劃
-
提供Spring-boot starter
-
支援Prometheus,Spring MicroMeter
-
對接Dubbo,Dubbo 中的資料統計實現替換為 Dubbo Metrics
-
在 Dubbo Admin 上展示各種 metrics 資料
-
對接 Dubbo 生態中的其他元件,如Nacos, Sentinel, Fescar等
參考資料
Dubbo Metrics @Github:
https://github.com/dubbo/metrics
Wiki:
https://github.com/dubbo/metrics/wiki (持續更新)