1. 程式人生 > 實用技巧 >16丨案例:效能監控工具之Grafana-Prometheus-Exporters

16丨案例:效能監控工具之Grafana-Prometheus-Exporters

在本模組中,我將把幾個常用的監控部分給梳理一下。前面我們提到過,在效能監控圖譜中,有作業系統、應用伺服器、中介軟體、佇列、快取、資料庫、網路、前端、負載均衡、Web伺服器、儲存、程式碼等很多需要監控的點。

顯然這些監控點不能在一個專欄中全部覆蓋並一一細化,我只能找最常用的幾個,做些邏輯思路的說明,同時也把具體的實現描述出來。如果你遇到了其他的元件,也需要一一實現這些監控。

在本篇中,主要想說明白下圖的這個監控邏輯。

這應該是現在最流行的一套監控邏輯了吧。

我今天把常見的使用Grafana、Prometheus、InfluxDB、Exporters的資料展示方式說一下,如果你剛進入效能測試領域,也能有一個感性的認識。

有測試工具,有監控工具,才能做後續的效能分析和瓶頸定位,所以有必要把這些工具的邏輯跟你擺一擺。

所有做效能的人都應該知道一點,不管資料以什麼樣的形式展示,最要緊的還是看資料的來源和含義,以便做出正確的判斷。

我先說明一下JMeter和node_exporter到Grafana的資料展示邏輯。至於其他的Exporter,我就不再解釋這個邏輯了,只說監控分析的部分。

JMeter+InfluxDB+Grafana的資料展示邏輯

一般情況下,我們用JMeter做壓力測試時,都是使用JMeter的控制檯來檢視結果。如下圖所示:

或者裝個外掛來看結果:

或者用JMeter來生成HTML:

這樣看都沒有問題,我們在前面也強調過,對於壓力工具來說,我們最多隻關心三條曲線的資料:TPS(T由測試目標定義)、響應時間、錯誤率。這裡的錯誤率還只是輔助排查問題的曲線,沒有問題時,只看TPS和響應時間即可。

不過採取以上三種方式有幾個方面的問題。

  1. 整理結果時比較浪費時間。
  2. 在GUI用外掛看曲線,做高併發時並不現實。
  3. 在場景執行時間比較長的時候,採用生成HTML的方式,會出現消耗記憶體過大的情況,而實際上,在生成的結果圖中,有很多生成的圖我們並不是那麼關注。
  4. 生成的結果儲存之後再檢視比較麻煩,還要一個個去找。

那麼如何解決這幾個問題呢?

用JMeter的Backend Listener幫我們實時傳送資料到InfluxDB或Graphite可以解決這樣的問題。Graphite Backend Listener的支援是在JMeter 2.13版本,InfluxdDB Backend Listener的支援是在JMeter 3.3的版本,它們都是用非同步的方式把資料傳送出來,以便檢視。

其實有這個JMeter傳送給InfluxDB的資料之後,我們不需要看上面的那些HTML資料,也可以直觀地看到系統性能的效能趨勢。並且這樣儲存下來的資料,在測試結束後想再次檢視也比較方便比對。

JMeter+InfluxDB+Grafana的結構如下:

在這個結構中,JMeter傳送壓力到伺服器的同時,統計下TPS、響應時間、執行緒數、錯誤率等資訊。預設每30秒在控制檯輸出一次結果(在jmeter.properties中有一個引數#summariser.interval=30可以控制)。配置了Backend Listener之後,將統計出的結果非同步傳送到InfluxDB中。最後在Grafana中配置InfluxDB資料來源和JMeter顯示模板。

然後就可以實時檢視JMeter的測試結果了,這裡看到的資料和控制檯的資料是一樣。

但如果這麼簡單就說完了,這篇文章也就沒價值了。下面我們來說一下,資料的傳輸和展示邏輯。

JMeter中Backend Listener的配置

下面我們就InfluxDB的Backend Listener做個說明。它的配置比較簡單,在指令碼中加上即可。

我們先配置好influxdb Url、application等資訊,application這個配置可以看成是場景名。

那麼JMeter如何將資料發給InfluxDB呢?請看原始碼中的關鍵程式碼,如下所示:

    private void addMetrics(String transaction, SamplerMetric metric) {
        // FOR ALL STATUS
        addMetric(transaction, metric.getTotal(), metric.getSentBytes(), metric.getReceivedBytes(), TAG_ALL, metric.getAllMean(), metric.getAllMinTime(),
                metric.getAllMaxTime(), allPercentiles.values(), metric::getAllPercentile);
        // FOR OK STATUS
        addMetric(transaction, metric.getSuccesses(), null, null, TAG_OK, metric.getOkMean(), metric.getOkMinTime(),
                metric.getOkMaxTime(), okPercentiles.values(), metric::getOkPercentile);
        // FOR KO STATUS
        addMetric(transaction, metric.getFailures(), null, null, TAG_KO, metric.getKoMean(), metric.getKoMinTime(),
                metric.getKoMaxTime(), koPercentiles.values(), metric::getKoPercentile);
​
​
        metric.getErrors().forEach((error, count) -> addErrorMetric(transaction, error.getResponseCode(),
                    error.getResponseMessage(), count));
    }

從這段程式碼可以看出,站在全域性統計的視角來看,這裡把JMeter執行的統計結果,比如事務的Total請求、傳送接收位元組、平均值、最大值、最小值等,都加到metric中,同時也會把成功和失敗的事務資訊新增到metric中去。

在原始碼中,還有更多的新增metric的步驟,你有興趣的話,也可以看一下JMeter原始碼中的InfluxdbBackendListenerClient.java

儲存了metric之後,再使用InfluxdbMetricsSender傳送到Influxdb中去。傳送關鍵程式碼如下:

   @Override
    public void writeAndSendMetrics() {
 ........
        if (!copyMetrics.isEmpty()) {
            try {
                if(httpRequest == null) {
                    httpRequest = createRequest(url);
                }
                StringBuilder sb = new StringBuilder(copyMetrics.size()*35);
                for (MetricTuple metric : copyMetrics) {
                    // Add TimeStamp in nanosecond from epoch ( default in InfluxDB )
                    sb.append(metric.measurement)
                        .append(metric.tag)
                        .append(" ") //$NON-NLS-1$
                        .append(metric.field)
                        .append(" ")
                        .append(metric.timestamp+"000000") 
                        .append("\n"); //$NON-NLS-1$
                }


                StringEntity entity = new StringEntity(sb.toString(), StandardCharsets.UTF_8);
                
                httpRequest.setEntity(entity);
                lastRequest = httpClient.execute(httpRequest, new FutureCallback<HttpResponse>() {
                    @Override
                    public void completed(final HttpResponse response) {
                        int code = response.getStatusLine().getStatusCode();
                        /*
                         * HTTP response summary 2xx: If your write request received
                         * HTTP 204 No Content, it was a success! 4xx: InfluxDB
                         * could not understand the request. 5xx: The system is
                         * overloaded or significantly impaired.
                         */
                        if (MetricUtils.isSuccessCode(code)) {
                            if(log.isDebugEnabled()) {
                                log.debug("Success, number of metrics written: {}", copyMetrics.size());
                            } 
                        } else {
                            log.error("Error writing metrics to influxDB Url: {}, responseCode: {}, responseBody: {}", url, code, getBody(response));
                        }
                    }
                    @Override
                    public void failed(final Exception ex) {
                        log.error("failed to send data to influxDB server : {}", ex.getMessage());
                    }
                    @Override
                    public void cancelled() {
                        log.warn("Request to influxDB server was cancelled");
                    }
                });               
 ........
            }
        }
    }

通過writeAndSendMetrics,就將所有儲存的metrics都發給了InfluxDB。

InfluxDB中的儲存結構

然後我們再來看下InfluxDB中如何儲存:

> show databases
name: databases
name
----
_internal
jmeter
> use jmeter
Using database jmeter
>
> show MEASUREMENTS
name: measurements
name
----
events
jmeter
> select * from events where application='7ddemo'
name: events
time                application text                title
----                ----------- ----                -----
1575255462806000000 7ddemo      Test Cycle1 started ApacheJMeter
1575256463820000000 7ddemo      Test Cycle1 ended   ApacheJMeter
..............
n> select * from jmeter where application='7ddemo' limit 10
name: jmeter
time                application avg                count countError endedT hit max maxAT meanAT min minAT pct90.0            pct95.0           pct99.0 rb responseCode responseMessage sb startedT statut transaction
----                ----------- ---                ----- ---------- ------ --- --- ----- ------ --- ----- -------            -------           ------- -- ------------ --------------- -- -------- ------ -----------
1575255462821000000 7ddemo                                          0              0     0          0                                                                                     0               internal
1575255467818000000 7ddemo      232.82352941176472 17    0                 17  849              122       384.9999999999996  849               849     0                               0           all    all
1575255467824000000 7ddemo      232.82352941176472 17                          849              122       384.9999999999996  849               849     0                               0           all    0_openIndexPage
1575255467826000000 7ddemo      232.82352941176472 17                          849              122       384.9999999999996  849               849                                                 ok     0_openIndexPage
1575255467829000000 7ddemo                                          0              1     1          1                                                                                     1               internal
1575255472811000000 7ddemo      205.4418604651163  26    0                 26  849              122       252.6              271.4             849     0                               0           all    all
1575255472812000000 7ddemo                                          0              1     1          1                                                                                     1               internal
1575255472812000000 7ddemo      205.4418604651163  26                          849              122       252.6              271.4             849                                                 ok     0_openIndexPage
1575255472812000000 7ddemo      205.4418604651163  26                          849              122       252.6              271.4             849     0                               0           all    0_openIndexPage
1575255477811000000 7ddemo      198.2142857142857  27    0                 27  849              117       263.79999999999995 292.3500000000001 849     0                               0           all    all

這段程式碼也就是說,在InfluxDB中,建立了兩個MEASUREMENTS,分別是events和jmeter。這兩個各自存了資料,我們在介面中配置的testtile和eventTags放在了events這個MEASUREMENTS中。在模板中這兩個值暫時都是不用的。

在jmeter這個MEASUREMENTS中,我們可以看到application和事務的統計資訊,這些值和控制檯一致。

在Grafana中顯示的時候,就是從這個表中取出的資料,根據時序做的曲線。

Grafana中的配置

有了JMeter傳送到InfluxDB中的資料,下面就來配置一下Grafana中的展示。首先,要配置一個InfluxDB資料來源。如下所示:

在這裡配置好URL、Database、User、Password之後,直接點選儲存即可。

然後新增一個JMeter dashboard,我們常用的dashboard是Grafana官方ID為5496的模板。匯入進來後,選擇好對應的資料來源。

然後就看到介面了。

這時還沒有資料,我們稍後做個示例,看下JMeter中的資料怎麼和這個介面的資料對應起來。

我們先看下圖中兩個重要的資料查詢語句吧。

TPS曲線:

SELECT last("count") / $send_interval FROM "$measurement_name" WHERE ("transaction" =~ /^$transaction$/ AND "statut" = 'ok') AND $timeFilter GROUP BY time($__interval)

上面這個就是Total TPS了,在這裡稱為throughput。關於這個概念,我在第一篇中就已經有了說明,這裡再次提醒,概念的使用在團隊中要有統一的認識,不要受行業內一些傳統資訊的誤導。

這裡取的資料來自MEASUREMENTS中成功狀態的所有事務。

響應時間曲線:

SELECT mean("pct95.0") FROM "$measurement_name" WHERE ("application" =~ /^$application$/) AND $timeFilter GROUP BY "transaction", time($__interval) fill(null)

這裡是用95 pct內的響應時間畫出來的曲線。

整體展示出來的效果如下:

資料比對

首先,我們在JMeter中配置一個簡單的場景。10個執行緒,每個執行緒迭代10次,以及兩個HTTP請求。

也就是說,這時會產生10x10x2=200次請求。我們用JMeter跑起來看一下。

看到了吧,這個請求數和我們預想的一樣。下面我們看一下Grafana中展示出來的結果。

還有針對每個事務的統計情況。

至此,JMeter到Grafana的展示過程就完成了。以後我們就不用再儲存JMeter的執行結果了,也不用等著JMeter輸出HTML了。

node_exporter+Prometheus+Grafana的資料展示邏輯

對效能測試來說,在常用的Grafana+Prometheus+Exporter的邏輯中,第一步要看的就是作業系統資源了。所以在這一篇中,我們將以node_exporter為例來說明一下作業系統抽取資料的邏輯,以便知道監控資料的來源,至於資料的含義,我們將在後續的文章中繼續描述。

首先,我們還是要畫一個圖。

現在node_exporter可以支援很多個作業系統了。官方列表如下:

當然不是說只支援這些,你也可以擴充套件自己的Exporter。

配置node_exporter

node_exporter目錄如下:

[root@7dgroup2 node_exporter-0.18.1.linux-amd64]# ll
total 16524
-rw-r--r-- 1 3434 3434    11357 Jun  5 00:50 LICENSE
-rwxr-xr-x 1 3434 3434 16878582 Jun  5 00:41 node_exporter
-rw-r--r-- 1 3434 3434      463 Jun  5 00:50 NOTICE

啟動:

[root@7dgroup2 node_exporter-0.18.1.linux-amd64]#./node_exporter --web.listen-address=:9200 &

是不是很簡潔?如果想看更多的功能 ,可以檢視下它的幫助。

配置Prometheus

先下載Prometheus:

[root@7dgroup2 data]# wget -c https://github.com/prometheus/prometheus/releases/download/v2.14.0/prometheus-2.14.0.linux-amd64.tar.gz
..........
100%[=============================================================================================>] 58,625,125   465KB/s   in 6m 4s


2019-11-29 15:40:16 (157 KB/s) - ‘prometheus-2.14.0.linux-amd64.tar.gz’ saved [58625125/58625125]


[root@7dgroup2 data]

解壓之後,我們可以看到目錄結構如下:

[root@7dgroup2 prometheus-2.11.1.linux-amd64]# ll
total 120288
drwxr-xr-x. 2 3434 3434     4096 Jul 10 23:26 console_libraries
drwxr-xr-x. 2 3434 3434     4096 Jul 10 23:26 consoles
drwxr-xr-x. 3 root root     4096 Nov 30 12:55 data
-rw-r--r--. 1 3434 3434    11357 Jul 10 23:26 LICENSE
-rw-r--r--. 1 root root       35 Aug  7 23:19 node.yml
-rw-r--r--. 1 3434 3434     2770 Jul 10 23:26 NOTICE
-rwxr-xr-x. 1 3434 3434 76328852 Jul 10 21:53 prometheus
-rw-r--r--  1 3434 3434     1864 Sep 21 09:36 prometheus.yml
-rwxr-xr-x. 1 3434 3434 46672881 Jul 10 21:54 promtool
[root@7dgroup2 prometheus-2.11.1.linux-amd64]#

prometheus.yml中新增如下配置,以取資料:

  - job_name: 's1'
    static_configs:
    - targets: ['172.17.211.143:9200']

啟動:

[root@7dgroup2 data]# ./prometheus --config.file=prometheus.yml &

這樣就行了嗎?當然不是。根據上面的流程圖,我們還需要配置Grafana。

配置Grafana

首先配置一個數據源,非常簡單。如下所示:

再配置一個node_exporter的模板,比如我這裡選擇了官方模板(ID:11074),展示如下:

資料邏輯說明

說明完上面的過程之後,對我們做效能測試和分析的人來說,最重要的,就是要知道資料的來源和含義了。

拿上面圖中的CPU使用率來說吧(因為CPU使用率是非常重要的一個計數器,所以我們今天先拿它來開刀)。

我們先點一下title上的edit,看一下它的query語句。

avg(irate(node_cpu_seconds_total{instance=~"$node",mode="system"}[30m])) by (instance)
avg(irate(node_cpu_seconds_total{instance=~"$node",mode="user"}[30m])) by (instance)
avg(irate(node_cpu_seconds_total{instance=~"$node",mode="iowait"}[30m])) by (instance)
1 - avg(irate(node_cpu_seconds_total{instance=~"$node",mode="idle"}[30m])) by (instance)

這些都是從Prometheus中取出來的資料,查詢語句讀了Prometheus中node_cpu_seconds_total的不同的模組資料。

下面我們來看一下,node_exporter暴露出來的計數器。

這些值和top一樣,都來自於/proc/目錄。下面這張圖是top資料,我們可以比對一下。

到此,我們就瞭解到了作業系統中監控資料的取值邏輯了,也就是從作業系統本身的計數器中取出值來,然後傳給Prometheus,再由Grafana中的query語句查出相應的資料,最後由Grafana展示在介面上。