效能監控工具之Grafana+Prometheus+Exporters

絲瓜呆呆發表於2021-06-12

在本模組中,我將把幾個常用的監控部分給梳理一下。前面我們提到過,在效能監控圖譜中,有作業系統、應用伺服器、中介軟體、佇列、快取、資料庫、網路、前端、負載均衡、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 中顯示的時候,就是從這個表中取出的資料,根據時序做的曲線。

相關文章