基於Prometheus的node服務監控

語言: CN / TW / HK

1 背景介紹

對於服務的監控,概括來說需要採集sdk、上報、儲存、圖形化展示、監控報警。先介紹2個工具, Prometheus是一套成熟且流行的系統和服務監控系統,它幾乎滿足了監控的所有能力。 Grafana, 它和Prometheus相比更側重的是圖形化展示,有強大、靈活的儀表盤體系,我們會把基於Prometheus收集的資料作為資料來源匯入到Grafana,關於他們更詳細介紹可去官網檢視。

作為web服務的開發者,當然不需要我們去搭建一整套,會有專門的團隊來維護監控報警體系。但是我們需要掌握採集端SDK的工作和監控的配置,這就需要理解Prometheus中各種指標的型別, 對於監控的配置就又需要掌握如何去寫PromQL,它是Prometheus自己的查詢語法,下面就開始幹吧!

2 環境搭建

2.1 初始化一個node專案

先初始化一個node專案 看一下采集端的效果, node的Prometheus的sdk是 prom-client mkdir node-demo cd node-demo npm init yarn add express @types/express prom-client

ts-node index.ts 啟動服務 import express from 'express' import {register, collectDefaultMetrics} from 'prom-client' const app = express() const port = 8080 collectDefaultMetrics() app.get('/metrics', async (req, res) => { res.send(await register.metrics()) }) app.get('/', (req, res) => { res.send('Hello World!') }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })

訪問http://localhost:8080/metrics,如果能看到下面這樣的文字就證明ok了。

image.png

雖然我們生產環境的Prometheus和Grafana不需要我們自己搭建,但是對於學習這套體系,自己在本地搭建一套是很有幫助的,因為生產環境的Prometheus和Grafana不會收集你本地服務的指標,所以可以自己收集本地啟動的服務指標進行學習,如果不需要學習PromQL的話可以選做。

2.2 docker安裝Prometheus和Grafana

對於安裝Prometheus和Grafana, 用docker安裝的方式最便捷。

  • 1 登入docker賬戶,如果沒有的先註冊。
  • 2 拉映象

bash docker pull prom/prometheus docker pull grafana/grafana

2.3 建立prometheus.yml配置檔案

在自己喜歡的位置建立prometheus.yml檔案,用於配置Prometheus。我的位置~/prometheus/prometheus.yml 檔案內容設定如下:

```yaml

my global config

global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s).

scrape_configs: # The job name is added as a label job=<job_name> to any timeseries scraped from this config. - job_name: "node-demo" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["192.168.1.42:8080"] ```

這裡我設定了一個node-demo的job, targets中是我的主機ip和我要啟動的node服務的埠。你按照自己主機的ip進行替換。

2.4 啟動容器

啟動,注意prometheus.yml配置成你自己的檔案位置。

sh docker run -d  --name prometheus -p 9090:9090 -v ~/prometheus/prometheus.yml:/etc/prometheus prom/prometheus/prometheus.yml docker run -d --name grafana -p 3000:3090 grafana/grafana

2.5 圖形化初試

image.png 容器啟動後我們訪問http://localhost:9090訪問Prometueus, 輸入up點選Execute,如果看到如下圖的 結果就證明啟動的都沒問題,這裡的up就是Prometheus自己的查詢語言PromQL

上面的Status選單展示的都是prometheus.yml檔案中的內容,Targets中也可以檢視我們配置的node-demo的健康狀態。 image.png

接下來和Grafana打通,訪問http://localhost:3000, 預設的使用者名稱密碼都是adminimage.png 登入之後點選 DATA SOURCES

image.png

選擇Prometheus image.png

修改URL和name

image.png

點選Save&test 如果看到提示Data source is working 則證明成功。 image.png

點選Explore, 進入探索頁面,上面自動選擇建立的資料來源,輸入up點選 Run query 可如下結果則證明成功 image.png

3 Prometheus中的指標

3.1 指標初探

下面回到服務指標的學習,學完之後可以看懂這些文字 image.png 先看一下指標的形式 ```

HELP nodejs_heap_space_size_total_bytes Process heap space size total from Node.js in bytes.

TYPE nodejs_heap_space_size_total_bytes gauge nodejs_heap_space_size_total_bytes{space="read_only"} 176128

nodejs_heap_space_size_total_bytes{space="old"} 54087680 nodejs_heap_space_size_total_bytes{space="code"} 2990080 nodejs_heap_space_size_total_bytes{space="map"} 1056768 nodejs_heap_space_size_total_bytes{space="large_object"} 21299200 nodejs_heap_space_size_total_bytes{space="code_large_object"} 0 nodejs_heap_space_size_total_bytes{space="new_large_object"} 0 nodejs_heap_space_size_total_bytes{space="new"} ``#HELP是一個指標的描述文案,可以解釋這個指標的功能#TYPE是一個指標的型別描述,前面的代表這個指標的名稱nodejs_heap_space_size_total_bytes空格後邊的代表這個指標的型別,gauge不帶#的就是指標的真實值,它的形式是指標名+值{}中的內容代表了這個指標的label,也就是這個指標還可以再分屬性,比如這個指標是nodejs 堆記憶體的大小,裡邊還可以根據space屬性分成read_only,old`等等。 所有的指標都是這種形式。

3.2 指標型別

Prometheus中有四種指標型別,Counter(計數器)、Gauge(儀表盤)、Histogram(直方圖)、Summary(摘要)。

  • Counter Counter是隻增不減的指標, 一般在定義Counter型別指標的名稱時推薦使用_total作為字尾。 比如預設指標中的 process_cpu_user_seconds_total process_cpu_system_seconds_total process_cpu_seconds_total 統計的就是cpu在該node服務上花費的cpu時間,它是一個累積增長的指標。 如果我們想要統計CPU的使用率 就可以通過1分鐘之內CPU的增長時間差除以1分鐘得到。 再比如如果我們統計請求的QPS也是同樣的道理。

  • Gauge Counter是可增可減的指標,側重於反應指標的實時狀態,比如預設指標中的 nodejs_heap_size_total_bytes 81444864 nodejs_heap_size_used_bytes 79206776 統計的就是node中推記憶體的大小,它顯然不是一直增長的是可增可減的指標。

  • Histogram Histogram是直方圖的意思,它不是單純一個值的指標,是一個複合的指標,可以統計一個值在各種區間之間的分佈情況。看demo如下: ``` const h = new Histogram({ name: 'test_histogram', help: 'Example of a histogram', labelNames: ['code'], buckets: [0.1, 0.2, 0.3, 0.4, 0.5, 1], });

h.labels('200').observe(0.4); h.labels('200').observe(0.6); h.observe({ code: '200' }, 0.4);

register.metrics().then(str => console.log(str)); ``` 輸出結果:

```

HELP test_histogram Example of a histogram

TYPE test_histogram histogram

test_histogram_bucket{le="0.1",code="200"} 0 test_histogram_bucket{le="0.2",code="200"} 0 test_histogram_bucket{le="0.3",code="200"} 0 test_histogram_bucket{le="0.4",code="200"} 2 test_histogram_bucket{le="0.5",code="200"} 2 test_histogram_bucket{le="1",code="200"} 3 test_histogram_bucket{le="+Inf",code="200"} 3 test_histogram_sum{code="200"} 1.4 test_histogram_count{code="200"} 3 `` 可以看到拿到的結果中有總的次數test_histogram_count有總的值test_histogram_sum, 有設定的幾個區間的值,顯示的值是小於該區間值的數量,例如test_histogram_bucket{le="0.5",code="200"} 2`表示值小於0.5的有2個,那麼也就能計算出0.5-1的個數是 3-2=1。

看一個預設指標中的垃圾回收的指標利用的是histogram ```

TYPE nodejs_gc_duration_seconds histogram

nodejs_gc_duration_seconds_bucket{le="0.001",kind="major"} 0 nodejs_gc_duration_seconds_bucket{le="0.01",kind="major"} 2 nodejs_gc_duration_seconds_bucket{le="0.1",kind="major"} 2 nodejs_gc_duration_seconds_bucket{le="1",kind="major"} 2 nodejs_gc_duration_seconds_bucket{le="2",kind="major"} 2 nodejs_gc_duration_seconds_bucket{le="5",kind="major"} 2 nodejs_gc_duration_seconds_bucket{le="+Inf",kind="major"} 2 nodejs_gc_duration_seconds_sum{kind="major"} 0.008220480993390084 nodejs_gc_duration_seconds_count{kind="major"} 2 ```

  • Summary 彙總指標,它和Histogram比較相似,也是複合指標,但是用的場景不多,預設的指標中沒有使用Summary的,可能是因為它不支援聚合操作,只能統計單例項的指標。看例子:

``` const h = new Summary({ name: 'test_summary', help: 'Example of a summary', labelNames: ['code'], percentiles: [0.1, 0.3, 0.4, 0.5, 1], }); h.labels('200').observe(0.2); h.labels('200').observe(0.4);

h.labels('200').observe(0.5); h.labels('200').observe(1);

register.metrics().then(str => console.log(str)); 輸出結果:

HELP test_summary Example of a summary

TYPE test_summary summary

test_summary{quantile="0.1",code="200"} 0.2 test_summary{quantile="0.3",code="200"} 0.33999999999999997 test_summary{quantile="0.4",code="200"} 0.41000000000000003 test_summary{quantile="0.5",code="200"} 0.45 test_summary{quantile="1",code="200"} 1 test_summary_sum{code="200"} 2.1 test_summary_count{code="200"} 4 `` 可以看到它也是統計了總數和總的和,但是和histogram的區別是它統計的是百分比的分佈情況,比如quantile="0.4"表示的是40%的值小於0.41000000000000003`所以它是在客戶端經過計算的,不是簡單的增加,這種計算就不能夠實現多個例項的聚合。而histogram的區間是可以多例項聚合的。

4 prom-client的使用

4.1 基本使用

Prometheus的nodejs客戶端是prom-client,在環境配置的時候已經安裝上了。

Registery是一個註冊源的概念,是多個指標的容器,最後可以通過它對外暴露資料。prom-client提供了它的建構函式同時也內建了一個預設的registry,所以我們可以這麼引用 import {registry, Registry} from 'prom-client'; 所有的指標在建立的時候都可以指定registry,如果不指定的話預設就是內建的registry。 原始碼在lib/metric.js

image.png

prom-client內建了一個collectDefaultMetrics方法,用於收集推薦的指標。

collectDefaultMetrics可選地接受具有以下條目的配置物件:

  • prefix指標名稱的可選字首。預設值:無字首。
  • register應該向哪些指標註冊。預設值:全域性預設登錄檔。
  • gcDurationBuckets帶有用於 GC 持續時間直方圖的自定義儲存桶。GC 持續時間直方圖的預設儲存桶是[0.001, 0.01, 0.1, 1, 2, 5](以秒為單位)。
  • eventLoopMonitoringPrecision以毫秒為單位的取樣率。必須大於零。預設值:10。

確保collectDefaultMetrics只執行一次。 import {collectDefaultMetrics} from 'prom-client'; collectDefaultMetrics() 我們在環境配置中看到的指標都是該方法產生的。

接下來就是如果拿到監控的指標資料。registry物件提供了2個全量獲取async方法metrics獲取文字內容,getMetricsAsJSON獲取JSON格式。在你需要的路由上返回即可。

4.2 自定義指標

prom-client內建的指標不能完全滿足我們的監控需求,我們都需要自定義指標,自定義指標的命名規範。 上面講到的四個指標都可以通過prom-client實現

import {Counter, Gauge, Histogram, Summary} form 'prom-client' - Counter只能是增加的所以 只提供一個inc方法使用者記錄增加的值。例如內建的CPU使用時間統計

const cpuUserUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_USER_SECONDS, help: 'Total user CPU time spent in seconds.', registers, labelNames, // Use this one metric's `collect` to set all metrics' values. collect() { const cpuUsage = process.cpuUsage(); const userUsageMicros = cpuUsage.user - lastCpuUsage.user; const systemUsageMicros = cpuUsage.system - lastCpuUsage.system; lastCpuUsage = cpuUsage; cpuUserUsageCounter.inc(labels, userUsageMicros / 1e6); cpuSystemUsageCounter.inc(labels, systemUsageMicros / 1e6); cpuUsageCounter.inc(labels, (userUsageMicros + systemUsageMicros) / 1e6); }, }); const cpuSystemUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_SYSTEM_SECONDS, help: 'Total system CPU time spent in seconds.', registers, labelNames, }); const cpuUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_SECONDS, help: 'Total user and system CPU time spent in seconds.', registers, labelNames, }); - Gauge是可增可減,所以提供了inc用於增加,dec用於減少,set直接設定具體的值。 startTimer則是用於方便我們計算時間差,而不需要自己手動去計算,原始碼如下: /** * Start a timer * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep * @returns {function} - Invoke this function to set the duration in seconds since you started the timer. * @example * var done = gauge.startTimer(); * makeXHRRequest(function(err, response) { * done(); //Duration of the request will be saved * }); */ startTimer(labels) { const start = process.hrtime(); return endLabels => { const delta = process.hrtime(start); const value = delta[0] + delta[1] / 1e9; this.set(Object.assign({}, labels, endLabels), value); return value; }; }

所有指標都可以傳一個collect方法,會在該指標被收集資料時執行,所以我們可以在這個方法中獲取指標值然後賦值。 例如收集記憶體就是在collect方法中執行:

const collect = () => { const memUsage = safeMemoryUsage(); if (memUsage) { heapSizeTotal.set(labels, memUsage.heapTotal); heapSizeUsed.set(labels, memUsage.heapUsed); if (memUsage.external !== undefined) { externalMemUsed.set(labels, memUsage.external); } } };

執行collect的原始碼在各指標的get方法中:

async get() { if (this.collect) { const v = this.collect(); if (v instanceof Promise) await v; } ... }

  • Histogram 同樣有startTimer,它的設定方法是observe,它的引數多在了可以設定buckets 自定義值的統計區間。 所有的指標都可以定義labelNamesregisters。 只要確保你定義的指標程式碼能夠在服務啟動的時候執行一次就可以,執行多次會丟擲錯誤。

5 PromQL

Prometheus 提供了一種稱為 PromQL(Prometheus Query Language)的功能性查詢語言,讓使用者可以實時選擇和聚合時間序列資料。

  • 5.1 時間序列的理解 我們定義的各種指標是定時被Prometheus抓取的,那麼它的儲存結構就是以時間為橫軸的資料。 我們在prometheus中輸入一個指標的時候 獲取的是最新一次的值

image.png

如果我們加上一個時間範圍[1m]代表獲取1分鐘內的資料。 下圖可以看到每一個指標可以獲取4個值,因為我們是15秒抓取一次資料。

image.png

可以理解成是點資料和時間段資料的區別。

當時點資料的時候我們切換到Graph面板就會展示以時間做為橫軸,值作為縱軸的圖表。 image.png

  • 5.2 即時向量選擇器 我們一個指標中可能定義了label,只有指標名稱是無法區分label,即時向量選擇器用於對label進行選擇。 {}用於寫label的選擇器,支援的語法有:
  • =:選擇與提供的字串完全相等的標籤。
  • !=:選擇不等於提供的字串的標籤。
  • =~:選擇與提供的字串進行正則表示式匹配的標籤。
  • !~:選擇與提供的字串不匹配的標籤。

例如:

nodejs_heap_space_size_total_bytes{space="large_object"} nodejs_heap_space_size_total_bytes{space!="large_object"} nodejs_heap_space_size_total_bytes{space=~"new.*"} 其中正則表示式的匹配可以認為是 /^$/完全匹配, 例如:

image.png

image.png

  • 5.3 範圍向量選擇器 是選擇一定範圍內的多個樣本。持續時間可以寫到[]中,支援的單位包括:
  • ms- 毫秒
  • s- 秒
  • m- 分鐘
  • h- 小時
  • d- 天 - 假設一天總是 24 小時
  • w- 周 - 假設一週總是 7 天
  • y- 年 - 假設一年總是 365d

例如: nodejs_heap_space_size_total_bytes{space=~"new.*"}[1m] - 5.4 偏移修改器(不常用) offset修飾符允許更改查詢中各個瞬間和範圍向量的時間偏移量。 例如 nodejs_heap_space_size_total_bytes[1m] 得到的時間是1656819991.26nodejs_heap_space_size_total_bytes[1m] offset 1m 得到的時間是1656819931.333 向後偏移了一分鐘

  • @修飾符(不常用) 預設指標獲取的都是當前時間的指標值,而@修飾符可以指定獲取哪一個時間點的值,例如:

nodejs_heap_space_size_total_bytes @1656820184 - 運算 支援的全量運算參考文件 這裡說一些常用的運算。 算術運算 Prometheus 中存在以下二元算術運算子: - +(新增) - -(減法) - *(乘法) - /(分配) - %(模數) - ^(冪/冪)

比如記憶體預設單位是byte 我們展示的時候展示MB就可以

nodejs_heap_size_total_bytes/1024/1024 - 常用的聚合運算 因為我們的服務一般都是多例項的,所以需要在統計的時候把所有例項的資料聚合到一起。 - sum(計算維度總和) - min(選擇最小尺寸) - max(選擇最大尺寸) - avg(計算尺寸的平均值) - group(結果向量中的所有值都是 1) - stddev(計算維度上的總體標準偏差) - stdvar(計算維度上的總體標準方差) - count(計算向量中的元素個數) - count_values(計算具有相同值的元素個數) - bottomk(樣本值的最小 k 個元素) - topk(按樣本值計算的最大 k 個元素) - quantile(在維度上計算 φ-quantile (0 ≤ φ ≤ 1))

舉例:

sum(nodejs_heap_space_size_total_bytes) by (space) topk(2, nodejs_heap_space_size_total_bytes)

  • 常用的函式 全量函式參見文件, 這裡講解3種計算增長率的方法用到的函式及他們的區別。

  • increase()計算區間向量中時間序列的增量,它只計算增量,所以想要計算增長率則需要手動的去除以時間。 increase(http_requests_total[1m])/60

  • rate()方法用於計算區間向量時間範圍內的增長率,秒為單位。 rate(http_requests_total[1m]) 它計算增長率的方式是時間範圍內最後的樣本和第一個樣本的差值除以時間,所以可能會出現中間某一個時間內增長率高而無法統計到,可能被整個時間範圍給平均了,所以一種方法是把時間範圍設定的短一些,第二種就是使用irate

  • irate()也是計算區間向量時間範圍內的增長率,但是他是瞬時增長率。基於時間範圍內最後兩個資料點計算。 所以總結就是irate更靈敏而rate側重時間段內的趨勢。

6 總結

本文講解了整體監控需要的工作,介紹了PrometheusGrafana的功能和docker方式安裝、Prometheus中的四種指標、prom-client的基本使用和自定義指標,最後還講解了常用的PromQL語法。

  • 如果覺得有用請幫忙點個贊🙏。
  • 我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿