NodeJS: 從 0 開始 Prometheus + Grafana 業務性能指標監控

爲何須要指標監控告警

一個複雜的應用,每每由不少個模塊組成,並且每每會存在各類各樣奇奇怪怪的使用場景,誰也不能保證本身維護的服務永遠不會出問題,等用戶投訴才發現問題再去處理問題就爲時已晚,損失已沒法挽回。html

因此,經過數據指標來衡量一個服務的穩定性和處理效率,是否正常運做,監控指標曲線的狀態,指標出現異常時及時主動告警,這一套工具就十分重要。前端

常見的一些指標,包括但不限於:node

  • QPS
  • 請求處理耗時
  • 進程佔用內存
  • 進程佔用CPU
  • golang 服務的 goroutine
  • nodejs 的 event loop lag
  • 前端應用的 Performance 耗時
  • ...

舉個例子,假如一個服務:mysql

  • 使用內存隨着時間逐漸上漲
  • CPU 佔用愈來愈高
  • 請求耗時愈來愈高,請求成功率降低
  • 磁盤空間頻頻被擠爆

<!--這究竟是人性的扭曲仍是道德的淪喪,-->
一旦服務存在某些缺陷致使這些問題,經過服務日誌,很難直觀快速地察覺到這些指標的變化波動。golang

經過監控和告警手段能夠有效地覆蓋了「發現」和「定位」問題,從而更有效率地排查和解決問題。web

指標監控系統:Prometheus

Prometheus 是一個開源的服務監控系統和時間序列數據庫。sql

工做流能夠簡化爲:數據庫

  1. client 採集當前 機器/服務/進程 的狀態等相關指標數據
  2. Prometheus server 按必定的時間週期主動拉取 client 的指標數據,並存儲到時序數據庫中
  3. 發現指標異常後,經過 alert manager 將告警通知給相關負責人

具體的架構設計以下:
後端

爲何不用 mysql 存儲?

Prometheus 用的是本身設計的時序數據庫(TSDB),那麼爲何不用咱們更加熟悉,更加經常使用的 mysql, 或者其餘關係型數據庫呢?api

假設須要監控 WebServerA 每一個API的請求量爲例,須要監控的維度包括:服務名(job)、實例IP(instance)、API名(handler)、方法(method)、返回碼(code)、請求量(value)。

image

若是以SQL爲例,演示常見的查詢操做:

# 查詢 method=put 且 code=200 的請求量
SELECT * from http_requests_total WHERE code=」200」 AND method=」put」 AND created_at BETWEEN 1495435700 AND 1495435710;

# 查詢 handler=prometheus 且 method=post 的請求量
SELECT * from http_requests_total WHERE handler=」prometheus」 AND method=」post」 AND created_at BETWEEN 1495435700 AND 1495435710;


# 查詢 instance=10.59.8.110 且 handler 以 query 開頭 的請求量
SELECT * from http_requests_total WHERE handler=」query」 AND instance=」10.59.8.110」 AND created_at BETWEEN 1495435700 AND 1495435710;

經過以上示例能夠看出,在經常使用查詢和統計方面,平常監控多用於根據監控的維度進行查詢與時間進行組合查詢。若是監控100個服務,平均每一個服務部署10個實例,每一個服務有20個API,4個方法,30秒收集一次數據,保留60天。那麼總數據條數爲:100(服務)* 10(實例)* 20(API)* 4(方法)* 86400(1天秒數)* 60(天) / 30(秒)= 138.24 億條數據,寫入、存儲、查詢如此量級的數據是不可能在Mysql類的關係數據庫上完成的。 所以 Prometheus 使用 TSDB 做爲 存儲引擎。

時序數據庫(Time Series Database/TSDB)

時序數據庫主要用於指處理帶時間標籤(按照時間的順序變化,即時間序列化)的數據,帶時間標籤的數據也稱爲時序數據。

對於 prometheus 來講,每一個時序點結構以下:

  • metric: 指標名,當前數據的標識,有些系統中也稱爲name。
  • label: 標籤屬性
  • timestamp: 數據點的時間,表示數據發生的時間。
  • value: 值,數據的數值
每一個指標,有多個時序圖;多個時序數據點鏈接起來,構成一個時序圖

image

假如用傳統的關係型數據庫來表示時序數據,就是如下結構:

create_time __metric_name__ path value
2020-10-01 00:00:00 http_request_total /home 100
2020-10-01 00:00:00 http_request_total /error 0
2020-10-01 00:00:15 http_request_total /home 120
2020-10-01 00:01:00 http_request_total /home 160
2020-10-01 00:01:00 http_request_total /error 1

指標 request_total{path="/home"} 在 2020-10-01 00:01:00 時的 qps = (160 - 100)/60 = 1 , 同理,
指標 request_total{path="/error"} 在 2020-10-01 00:01:00 時的 qps = 1/60

相比於 MySQL,時序數據庫核心在於時序,其查詢時間相關的數據消耗的資源相對較低,效率相對較高,而剛好指標監控數據帶有明顯的時序特性,因此採用時序數據庫做爲存儲層

數據類型

  • counter: 計數器,只能線性增長,不斷變大,場景:qps
  • gauge:絕對值,非線性,值可大可小,場景:機器溫度變化,磁盤容量,CPU 使用率,
  • histogram:,聚合數據查詢耗時分佈【服務端計算,模糊,不精確】
  • summary:不能聚合查詢的耗時分佈【客戶端計算,精確】

nodejs 指標採集與數據拉取

  • 定義一個 Counter 的數據類型,記錄指標
const reqCounter = new Counter({
  name: `credit_insight_spl_id_all_pv`,
  help: 'request count',
  labelNames: ['deviceBrand','systemType', 'appVersion', 'channel']
})

reqCounter.inc({
  deviceBrand: 'Apple',
  systemType: 'iOS',
  appVersion: '26014',
  channel: 'mepage'
},1)
  • 定義訪問路徑爲 /metrics 的controller
@Get('metrics')
  getMetrics(@Res() res) {
    res.set('Content-Type', register.contentType)
    res.send(register.metrics())
  }
  • Prometheus 主動請求 node client 的 /metrics 接口,得到

當前數據快照

image

promQL

promQL 是 prometheus 的查詢語言,語法十分簡單

基本查詢

查詢指標最新的值:

{__name__="http_request_total", handler="/home"}

# 語法糖:
http_request_total{handler="/home"}

# 等價於 mysql:
select * from http_request_total 
where 
  handler="/home" AND
  create_time=《now()》

區間時間段查詢

查詢過去一分鐘內的數據

# promQL
http_request_total[1m]

# 等價於
SELECT * from http_requests_total 
WHERE create_time BETWEEN 《now() - 1min》 AND 《now()》;

時間偏移查詢

PS: promQL 不支持指定時間點進行查詢,只能經過 offset 來查詢歷史某個點的數據

查詢一個小時前的數據。

# promQL
http_request_total offset 1h

# 等價於
SELECT * from http_requests_total 
WHERE create_time=《now() - 1 hour》;

promQL 查詢函數

根據以上的查詢語法,咱們能夠簡單組合出一些指標數據:

例如,查詢最近一天內的 /home 頁請求數

http_request_total{handler="/home"}  - http_request_total{handler="/home"} offset 1d

那麼實際上面這個寫法很明顯比較不簡潔,咱們可以使用內置 increase 函數來替換:

# 和上述寫法等價
increase(http_request_total{handler="/home"}[1d])

除了 increase 外,還有不少其餘好用的函數,例如,
rate 函數計算 QPS

// 過去的 2 分鐘內平均每秒請求數
rate(http_request_total{code="400"}[2m])

// 等價於
increase(http_request_total{code="400"}[2m]) / 120

指標聚合查詢

除了上述基礎查詢外,咱們可能還須要聚合查詢

假如咱們有如下數據指標:

credit_insight_spl_id_all_pv{url="/home",channel="none"} 
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 
credit_insight_spl_id_all_pv{url="/error",channel="none"} 
credit_insight_spl_id_all_pv{url="/error",channel="mepage"}

將全部指標數據以某個維度進行聚合查詢時,例如:查詢 url="/home" 最近一天的訪問量,channel 是 none仍是mepage 的 /home 訪問量都包括在內。

咱們理所固然地會寫出:

increase(credit_insight_spl_id_all_pv{url="/home"}[1d])

但實際上咱們會得出這樣的兩條指標結果:

credit_insight_spl_id_all_pv{url="/home",channel="none"} 233
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 666

image

並不是咱們預期中的:

credit_insight_spl_id_all_pv{url="/home"} 899

而要是咱們想要獲得這樣的聚合查詢結果,就須要用到 sum by

# 聚合 url="/home" 的數據
sum(increase(credit_insight_spl_id_all_pv{url="/home"}[1d])) by (url)
# 得出結果:
credit_insight_spl_id_all_pv{url="/home"} 899    # 全部 channel 中 /home 頁訪問量累加值


# 聚合全部的 url 則能夠這樣寫:
sum(increase(credit_insight_spl_id_all_pv{}[1d])) by (url)
# 得出結果:
credit_insight_spl_id_all_pv{url="/home"} 899  
credit_insight_spl_id_all_pv{url="/error"} 7


# 等價於 mysql
SELECT url, COUNT(*) AS total FROM credit_insight_spl_id_all_pv 
WHERE create_time between <now() - 1d> and <now()>
GROUP BY url;

指標時序曲線

以上的全部例子的查詢數值,其實都是最近時間點的數值,

而咱們更關注的是一個時間段的數值變化。

要實現這個原理也很簡單,只須要在歷史的每一個時間點都執行一次指標查詢,

# 假現在天7號
# 6號到7號的一天訪問量
sum(increase(credit_insight_spl_id_all_pv{}[1d] )) by (url) 

# 5號到6號的一天訪問量 offset 1d 
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 1d)) by (url) 

# 4號到5號的一天訪問量
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 2d)) by (url)

而 Prometheus 已經內置了時間段查詢功能,並對此優化處理。

可經過 /api/v1/query_range 接口進行查詢,獲的 grpah:
image

Prometheus 查詢瓶頸

數據存儲:

指標數據有 「Writes are vertical,reads are horizontal」 的(垂直寫,水平讀)模式:
「Writes are vertical,reads are horizontal」 的意思是 tsdb 一般按固定的時間間隔收集指標並寫入,會 「垂直」 地寫入最近全部時間序列的數據,而讀取操做每每面向必定時間範圍的一個或多個時間序列,「橫向」 地跨越時間進行查詢
  • 每一個指標(metric)根據指標數量不一樣,有 labelA labelB labelC * ... 個時序圖
  • 每一個時序圖(time series)的一個點時序是 [timestamp, value], 例如 [1605607257, 233]。[時間戳-值] 能夠肯定圖上的一個點,一個時間區間內的全部點連成一個時序曲線圖。
  • 由於 Prometheus 每隔 15s 採集一次數據,因此 時序點的時間間距是 15s,即1分鐘有60/15=4個時序點,1小時就有 4 * 60 = 240 個時序點。

image

而 Prometheus 的默認查詢 sample 上限是 5000w

image

因此,若是指標的時序圖數量過大,容許查詢的時間區間相對就會較小了

一個圖表查詢時序數量的影響因素有 3 個,分別是:

  1. 查詢條件的時序數量(n)
  2. 查詢的時間區間(time)
  3. 圖表曲線每一個時序點之間的間隔(step)

credit_insight_spl_id_all_pv 指標爲例,該指標總共大約有 n = 163698 種時序,
image

假如 step = 15s,若是搜索該指標過去 time = 60m 的所有時序圖,那麼,須要搜索的例子要
163698 * 60 * (60/15) = 39287520,將近 4kw,是能夠搜出來的。

但若是搜的是過去 90m 的數據,163698 * 90 * 4 = 58931280,超過了 5000w,你就發現數據請求異常:
Error executing query: query processing would load too many samples into memory in query execution
image

因此,目測可得一個圖的查詢時序點數量公式是:total = n * time / step, time 和 step 的時間單位必須一致,total 必須不超過 5000w。

反推一下得出,time < 5000w / n * step 。要擴大搜索時間範圍,增大 step ,或者下降 n 便可作到。

  • step 不變, 下降 n 【指定label值可減小搜索條件的結果數】 : credit_insight_spl_id_all_pv{systemType="Android", systemVersion="10"},n = 18955

image

  • 增大 step 到 30s, n 不變:

image

固然,通常狀況下,咱們的 n 值只有幾百,而 step 基本是大於 60s 的,因此通常狀況下都能查詢 2 個多月以上的數據圖。

可視化平臺: Grafana

grafana 是一個開源的,高度可配置的數據圖表分析,監控,告警的平臺,也是一款前端可視化的產品。

image

自定義圖表

grafana 內置提供多種圖表模板,具體是如下類型:

Prometheus 做爲數據源的狀況下,通常用的 graph 類型畫時序圖比較多。

對於一些基礎的數據大盤監控,這些圖表類型已經足夠知足咱們的需求。

但對於複雜的需求,這些類型沒法知足咱們的須要時,咱們安裝 pannel 插件,來更新可用的圖表類型,也能夠根據官方文檔 build a panel plugin 開發本身的前端圖表 panel。

圖表配置

在時序圖表配置場景下,咱們須要核心關注配置的有:

  1. promQL: 查詢語句
  2. Legend: 格式化圖例文本
  3. step/interval: 採集點間隔,每隔一段時間,採集一次數據。
    一條曲線的數據點數量 = 圖表時長 / 採樣間隔。例如查看最近24小時的數據,採樣 間隔5min,數據點數量=24*60/5=288。
    採集間隔時間越短,採樣率越大,圖表數據量越大,曲線越平滑。 採集間隔默認自動計算生成,也能夠自定義配置。
  4. metric time range: 每一個點的數據統計時間區間時長。
    以QPS爲例,圖表上每一個時間點的數據的意義是:在這時間點上,過去n秒間的訪問量。

從上圖能夠看到,

  • 若是採樣間隔 > 統計區間時長: 數據採樣率 < 100%。未能採集到的數據丟棄,不會再圖表上展現。採樣率太小可能會錯誤異常的數據指標。
  • 若是採樣間隔 == 統計區間時長,採樣率100%。
  • 若是採樣間隔 < 統計區間時長,數據被重複統計,意義不大。

自定義變量

爲了實現一些經常使用的篩選過濾場景,grafana 提供了變量功能

  • 變量配置:變量配置有多種方式(Type),能夠自定義選項,也能夠根據prometheus 指標的 label 動態拉取。

image

  • 變量使用:變量經過 $xxx 形式去引用。

image

告警

除了 Prometheus 自己能夠配置告警表達式以外:

image

grafana 也能夠配置告警:

數據源

Prometheus 一般用於後端應用的指標數據實時上報,主要用於異常告警,問題排查,因此數據存在時效性,咱們不會關注幾個月前的一個已經被排查並 fixed 的指標異常波動告警。

可是,要是咱們將 Prometheus 用於業務指標監控,那麼咱們可能會關注更久遠的數據。

例如咱們可能想要看過去一個季度的環比同比增加,用 Prometheus 做爲數據源就不合適,由於 Prometheus 是時序數據庫,更多關注實時數據,數據量大,當前數據保存的時效設定只有 3 個月。

那麼這個時候可能咱們要維護一個長期的統計數據,可能就須要存儲在 mysql 或者其餘存儲方式。

grafana 不是 Prometheus 的專屬產品,還支持多種數據源,包括但不限於:

  • 常見數據庫

    • MySql
    • SQL Server
    • PostgreSQL
    • Oracle
  • 日誌、文檔數據庫

    • Loki
    • Elasticsearch
  • 時序數據庫

    • Prometheus
    • graphite
    • openTSDB
    • InfluxDB
  • 鏈路追蹤

    • Jaeger
    • Zipkin
  • ....

若是沒有本身須要的數據源配置,還能夠安裝 REST API Datasource Plugin, 經過 http 接口查詢做爲數據源

總結

瞭解 grafana 的高度可配置性設計後,有值得思考的幾點:

  • 關注其設計思想,若是要本身實現一個相似的可視化的 web app,本身會怎麼設計?
  • 本身要作一個高度可配置化的功能,又應該怎麼設計?
  • 深刻到業務,例如咱們經常使用的 admin 管理 系統,一些經常使用的業務功能是否能夠高度可配置化?業務強關聯的如何作到配置與業務的有機結合?

等等這些,其實都是值得咱們去思考的。

此外,Prometheus 和 grafana 都有些進階的玩法,你們有興趣也能夠去探索下。

參考文章

  1. Prometheus 的數據存儲實現【理論篇】
  2. prometheus tsdb 的存儲與索引
  3. query processing would load too many samples into memory in query execution
相關文章
相關標籤/搜索