性能测试实战30讲00016
在本模块中,我将把几个常用的监控部分给梳理一下。前面我们提到过,在性能监控图谱中,有操作系统、应用服务器、中间件、队列、缓存、数据库、网络、前端、负载均衡、Web服务器、存储、代码等很多需要监控的点。
显然这些监控点不能在一个专栏中全部覆盖并一一细化,我只能找最常用的几个,做些逻辑思路的说明,同时也把具体的实现描述出来。如果你遇到了其他的组件,也需要一一实现这些监控。
在本篇中,主要想说明白下图的这个监控逻辑。
这应该是现在最流行的一套监控逻辑了吧。
我今天把常见的使用Grafana、Prometheus、InfluxDB、Exporters的数据展示方式说一下,如果你刚进入性能测试领域,也能有一个感性的认识。
有测试工具,有监控工具,才能做后续的性能分析和瓶颈定位,所以有必要把这些工具的逻辑跟你摆一摆。
所有做性能的人都应该知道一点,不管数据以什么样的形式展示,最要紧的还是看数据的来源和含义,以便做出正确的判断。
我先说明一下JMeter和node_exporter到Grafana的数据展示逻辑。至于其他的Exporter,我就不再解释这个逻辑了,只说监控分析的部分。
JMeter+InfluxDB+Grafana的数据展示逻辑
一般情况下,我们用JMeter做压力测试时,都是使用JMeter的控制台来查看结果。如下图所示:
或者装个插件来看结果:
或者用JMeter来生成HTML:
这样看都没有问题,我们在前面也强调过,对于压力工具来说,我们最多只关心三条曲线的数据:TPS(T由测试目标定义)、响应时间、错误率。这里的错误率还只是辅助排查问题的曲线,没有问题时,只看TPS和响应时间即可。
不过采取以上三种方式有几个方面的问题。
- 整理结果时比较浪费时间。
- 在GUI用插件看曲线,做高并发时并不现实。
- 在场景运行时间比较长的时候,采用生成HTML的方式,会出现消耗内存过大的情况,而实际上,在生成的结果图中,有很多生成的图我们并不是那么关注。
- 生成的结果保存之后再查看比较麻烦,还要一个个去找。
那么如何解决这几个问题呢?
用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