以前說要聊聊監控,這篇來填坑了。golang
《踩坑記:Goroutine泄漏》開篇那張截圖,展現了單個服務進程啓動的 Goroutine 數量;除此以外,咱們的服務進程在後臺還採集了不少其餘指標,例如:算法
(當前存活在堆上的對象所佔空間)
這些數據是哪兒來的呢?runtime 包給咱們提供了一些API,例如 runtime.NumGoroutine() 能夠得到當前 Goroutine 數量,而 runtime.ReadMemStats() 則返回一個 MemStats 類型,給咱們提供了內存相關的一系列監控指標。數據庫
如下摘取 MemStats 中的一些成員,略做解釋:後端
TotalAllocapi
HeapAlloc服務器
StackInUse數據結構
PauseTotalNs架構
NumGC併發
還有不少指標沒有在這裏列出,感興趣的同窗能夠查看參考資料 runtime.MemStats [1]。框架
Go Runtime 的這些性能指標,反應了其運行狀態,能夠幫助咱們排查性能問題:例如上篇《踩坑記:Goroutine泄漏》咱們是經過 Goroutine 的上漲發現有泄漏;而在《踩坑記:go服務內存暴漲》,咱們其實也能夠藉助 HeapAlloc 來實錘是否有內存泄漏(若是有內存泄漏的話,HeapAlloc也應該是不斷增加,與進程的 RSS 保持同步)。
服務自己的性能指標也很重要,例如接口 QPS、延遲、cache命中率等也很重要。例如在咱們的微服務框架中,就採集了每次請求的延遲、請求成功/失敗等信息,基於這些信息配置的報警能夠幫助咱們快速發現下游服務的異常。
實際工做中,還須要關注業務指標 —— 例如點擊率、轉化率、交易量等等,須要結合自身業務的特定設計合理的指標體系。
有指標還遠遠不夠,還須要想辦法採集下來,供後續查詢和監控使用。
對於通常的業務數據,咱們可能會考慮使用 MySQL 等 RDBMS 來存儲,可是對於這類指標每每數據量很是龐大,於是在採集、存儲、查詢上都須要特殊考量。
例如一個佔地5萬平方米的數據中心,可能部署了10萬臺服務器。若是每秒採集一次 CPU 佔用率,那就達到 10w QPS 了,更況且除了機器自己的指標,還有大量服務的性能指標、業務指標等。
好在這些指標有一個很重要的共同點:它們都是定時採樣的,所以也被稱「時序數據」(time series,時間序列)或「度量」(metric)。
以CPU佔用率爲例,咱們能夠取名爲 "sys.cpu" ,它可能包含多個 tag,例如 ip、datacenter,那麼一次典型的採集以下所示:
# NAME TIMESTAMP VAL TAG1 TAG2 put sys.cpu 1356998400 35 ip=10.0.0.1 datacenter=sh
在這裏 sys.cpu {ip=10.0.0.1, datacenter=sh} 就是一個時間序列。
針對其時序特色,咱們能夠爲其設計專用數據結構,而且經過下降採樣頻率(例如30s一個採樣點)來下降負載。不少開源項目就是這麼作的,例如 OpenTSDB, Prometheus, influxdb, StatsD 等,都實現了一個時序數據庫(Time Series DB,TSDB)。
以 OpenTSDB 爲例,它會將時序數據保存在 HBase 中,每一行保存某個時間序列一整個小時的數據,具體而言就是
ROW KEY = <名稱><時間><tag k1><v1><k2><v2>...
COLUMN FAMILY
從上述存儲方式咱們能夠看到,相比於 RDBMS ,TSDB 經過定製化的數據結構,可以大幅提升對時序數據的採集、存儲和查詢效率。
在具體實現/使用中還有一些點值得關注:
關於 OpenTSDB 的更多細節,感興趣的同窗能夠參考其官網[2],這裏不過多展開。
基於 TSDB 提供的 API ,咱們就能夠實現必要的監控和報警。
一個經常使用的工具是 Grafana [3],支持各類 TSDB 做爲數據源,並實現了一整套圖表工具用於展現,方便建立各種看板,對於排查問題很是有幫助:
不只如此,Grafana 從 4.0 版開始,還增長了一個 Alert 模塊,能夠很方便地配置報警規則,且支持郵件等常見報警方式(還可經過 API 擴展);不過其規則的靈活度不夠,不能承載很複雜的報警需求。
好比有這麼一個 metric:svc.thoughput{success=1或0},用於記錄累計請求數,而且加上了 tag "success" 用來區分請求成功/失敗。
一個常見的監控需求是,針對 QPS 的異常波動進行報警,但因爲晚高峯和凌晨的 QPS 差異很大,不能只是設置一個簡單的閾值;又或者,咱們但願基於錯誤率進行報警,這就須要計算:
svc.thoughput{success=0} / svc.thoughput{}
這些需求對於 Grafana 來講就超綱了。
所以咱們基於開源項目 Bosun[4] 進行二次開發,以支持複雜的報警需求。它是 Stack Exchange 開發的一個監控報警系統,其特色是實現了一套基於對 metrics 進行計算的表達式。
之前述 QPS 異常報警爲例,雖然日內 QPS 會有顯著的波動,可是一般日間的請求量倒是相對穩定的:
如上圖所示,凌晨、中午、晚上因爲用戶做息帶來了明顯的低谷和高峯,而表明 T 日和 T - 1 日數據的黃線和綠線則有至關程度的重合;所以咱們能夠設置這樣的報警規則:若是日同比降幅超過 30% 則表示異常。
使用 bosun 表達式,實現這樣的規則就很簡單了:
# 當日過去 30 分鐘 QPS $today = avg(q("sum:rate:svc.thoughput{}", "31m", "1m")) # 前日同一時間段 QPS $yesterday = avg(q("sum:rate:svc.thoughput{}", "1471m", "1441m")) warn = ($today / $yesterday) < 0.7
注:
bosun 表達式還提供了不少更復雜的玩法。例如,採集時添加一個 tag "api",用於區分具體是哪一個接口的請求,而後咱們只要簡單地將 svc.thoughput{} 改爲 svc.thoughput{api=*} 就能同時監控全部接口的 QPS 了;又或者咱們能夠用 epoch() 獲取當前時間戳,以針對夜間使用更寬鬆的閾值。
對 bosun 感興趣的同窗,能夠看一下它的官網[4]。這裏順便吐槽一下,它的文檔實在寫得不咋地,尤爲是表達式的那部分,不少方法只提供了描述、沒有樣例。
雖然 bosun 已經很強大,可是仍然不能知足全部場景。其根本缺陷在於,規則仍然須要咱們從過去的經驗中總結 —— 有多少人工,纔有多少智能。
仍是以 QPS 爲例,雖然咱們經過監控日同比變化率,繞過了日內的波動,可是卻繞不過周內的波動 —— 週一早晨的請求量每每會低於週日同時間段。固然咱們也能夠在表達式裏再加上相應的判斷,但還有法定節假日的狀況呢?表達式過於複雜,也會致使報警規則難以維護。
若是咱們可以基於過去的數據,學習到異常點(離羣點)的特徵,那就能較好地解決這一類問題。
用於檢測異常點的方法有不少,在具體實踐中,咱們採用了適用於孤立森林算法(Isolation Forest),它一般更適用於連續型、結構化數據(如時序數據)。
孤立森林算法有兩個前提:1) 異常數據在總樣本中的佔比較小;2) 異常點的特徵與正常點差別很大。於是,若是在數據空間某個區域裏點的分佈很稀疏,咱們就能夠認爲該區域中的點爲異常點。
基於這倆前提,算法提出了一個頗有意思的訓練思路。假設從數據點分佈在一個二維平面上:
很直觀地,數據點密集的區域,所需切割次數會顯著高於稀疏區域;找到了稀疏區域,也就肯定了離羣點。
具體實踐中:
這個算法我本身沒有實現過,這一節只能先裝到這裏了。感興趣的同窗能夠閱讀參考材料[5],文中內容詳實,還有一個對武林外傳的人物性格進行訓練、生成決策樹的例子,頗有意思。
照例小結一下:
限於各類緣由,有些細節未能在文中展開(好比咱們基於 OpenTSDB 實現的時序數據服務在架構上作了不少改造,以及生產中的具體案例);並且除了時序數據以外,咱們還有不少其餘監控報警的方案,感興趣的同窗不如投個簡歷,到廠裏來慢慢看:
↓↓↓ 長期招聘 ↓↓↓