• Posts tagged "监控"

Blog Archives

使用Micrometer进行业务指标上报

[TOC]

什么是指标

指标,metric,是指对事物的度量。通常是对事物的属性进行采样测量,记录下属性的值。采样结果记录为:<指标名,时间戳,值>,例如对当前大气温度的测量<weather.temperature, 2018-07-25T00:00:00Z, 30>。但上面的记录很笼统,无法反应是哪个地区的气温,无法满足按省市区进行汇总、细化的统计分析(即多维分析)的需求。因此一般地,会把记录格式拓展为<指标名,标签集,时间戳,值>,标签集是形如city: shenzhen, district: nanshan这样的代表维度的标签名值对。值可以是整数、浮点数、字符串。大多数的指标存储系统(时序数据库)只支持数值类型的值,因此本文也只讨论数值类型。

指标上报/表示格式

时序数据库是以<指标名,标签集,时间戳,值>作为一行存储一次采样的数据。类似以下:

sys.cpu.user host=webserver01        1356998400  50
sys.cpu.user host=webserver01,cpu=0  1356998400  1
sys.cpu.user host=webserver01,cpu=1  1356998400  0
sys.cpu.user host=webserver01,cpu=2  1356998400  2
sys.cpu.user host=webserver01,cpu=3  1356998400  0
...
sys.cpu.user host=webserver01,cpu=63 1356998400  1

实际时序数据库的存储并非以上述文本格式,比上面要复杂得多。

指标定义

指标定义是指指标的元数据,用于帮助对指标数据进行解读。一般包括:
* metric_name,指标名,如web.response.size
* type,指标类型, 如Gauge/Counter等
* interval,上报(采样)间隔,
* unit_name,字符串形式单位,如Byte/Bit
* description,指标的描述
* tags,指标支持的标签(及取值范围),如app.web.request支持的tag可能包括area,host,instance分表机房、主机和实例

指标类型

根据指标监控的对象个数、监视目的和含义的不同,指标类型大体可以分为Gauge、Counter、DistributionSummary、Timer四种。

Gauge 刻度

1a7f4299-8026-4197-835f-52ad109277b1
Gauge是监视单个对象属性的当前状态值,时序数据库存储Guage指标的每次采样记录(时间,值)。

sys.load host=webserver01,cpu=0  1356998400  11
sys.load host=webserver01,cpu=0  1356998401  21
sys.load host=webserver01,cpu=0  1356998402  15
...

sys.cpu.user host=webserver01,cpu=0就是webserver01的0号cpu的负载属性。

Gauge类型的指标值是随时间连续变化的。Gauge指标的时间序列图(非采样)类似下图,可以看出数值随时间上下波动,而且是连续的。
gague_unsmapled_ts
像CPU当前负载、消息队列中当前的消息数、JVM当前使用内存大小,这些都是典型的Gauge指标。Gauge是对单个对象属性的度量。

聚合指标

聚合指标是对多个对象的度量进行过统计处理的后产生的指标。

Counter 计数器

计数就是监视对象、事件发生的次数。其中一种办法就是每次检测到一个对象就上报一个记录,那么就是如下

web.request.accept host=host1 1356998400 1
web.request.accept host=host1 1356998401 1
web.request.accept host=host1 1356998401 1
web.request.accept host=host1 1356998402 1

指标值1代表一次请求,可以看出同一时间(戳)可能有多次请求发生。通过sum()就可以计算出总数。
另一种办法是利用一个计数器,记下所有请求的累计数,再对这个计数器采样上报,这就是Counter类型。
419SoKslhU

如果把Counter的值进行上报(不清零),展示出来会得到下图的效果。
counter_ts
Counter类型的指标值是随时间单调递增,而且是跳跃式增加的。像nginx启动以来一共处理的请求数、用户认证服务中认证不通过的次数,这些都是Counter指标。

Counter指标本质上是对象个数聚合后产生的Gauge指标。Gauge数值有升有降,而Counter只数值是一直上升的。

所有聚合指标都是有意义的Gauge指标。

Counter指标又可以分为两种:每次上报后清零,每次上报后不清零。

复合指标

复合指标是对一组对象\事件的属性的复合统计。在上报和存储时是以多个聚合指标联合构成的。
以web服务处理请求为例,一个请求响应就是一个事件,响应时间就是监控的指标。如果把所有请求的响应时间上报后进行展示,会得到下图这样的散点图。
QQ20180725-134113@2x
这样的散点图无法看出某一时间段的请求数量,无法看出平均响应时间,无法知晓大于1s和小于1s的响应的数量。也就是散点图无法以一个数值来说明事件发展的整体趋势。因此我们需要对这些响应先进行预处理(统计),生产成多个新的指标,逻辑上新指标都是原指标的组成部分。

DistributionSummary

DistributionSummary是用于跟踪事件的分布情况,有多个指标组成:
* count,事件的个数,聚合指标,如响应的个数
* sum,综合,聚合指标,如响应大小的综合
* histogram,分布,聚合指标,包含le标签用于区分bucket,例如web.response.size.historgram{le=512} = 99,表示响应大小不超过512(Byte)的响应个数是99个。一般有多个bucket,如le=128,le=256,le=512,le=1024,le=+Inf等。
每个bucket展示为一条时间序列,会得到类似下面的图。
QQ20180725-184823@2x

  • percentile(quantile),百分位数,聚合指标,包含percentile标签用于区分不同的百分位,例如web.response.size.percentile{p=90) = 512,表示90%的响应大小都小于512。一般有多个percentile,如p50,p75,p90,p99。
    每个百分位展示为一条时间序列,会得到类似下面的图。
    QQ20180725-191921@2x

Timer

Timer是DistributionSummary的特化,专门用于计时类的指标,可以对记录的时间值(duration)进行单位换算。

指标怎么展示?

采样什么的图标进行展示指标要根据指标的实际意义和使用场景决定。

展示最新

即只展示最新的采样点的数值。如下图展示进程的累计运行时长
QQ20180725-161758@2x

展示历史

时间序列,是指唯一指标名+唯一标签名值组合的所有采样数据点组成的序列。一般采用折线图,横坐标表示时间,纵坐标表示数值,一个图上可以展示多条时间序列,方便对比。
key-pg-classi
通过time shift,可以再同一个图展示一个时间序列的两个时间段。下图展示今日(红色)和昨日(蓝色)的接口访问量。
QQ20180725-163952@2x

Counter与Rate

Counter类型(不清零型)的时间序列,如果不做处理,展示出来就是一个单调增长的折线,如下图所示。
counter_ts
实际生产中我们希望知道某个时间段的请求速率,因此需要做上述时间序列做转换。通常是对时间段(period)第一个数据点和最后一个数据点进行delta计算,delta值作为该时间段的rate。转换后的时间序列类似下图,可以看出每天早上7点左右请求数有明显的增加。
QQ20180725-163952@2x
时序数据库提供rate()聚合函数用户上述需求的计算,rate()实现比delta要复杂很多,要处理计数器清零、反转、数据点丢失等情况。rate可以看做是原时间序列函数的一阶求导。

什么是业务指标?

按指标的来源进行分类。纵向实现了自底向上各层级的监控,包括网络、服务器、系统层、应用层、业务层,如下图所示:
836418C4-0D04-4A6F-AD92-21DF75E819A5
* 网络监控,包括机房出口VIP是否存活,流量是否正常,机房间专线流量和质量是否正常,以及网络设备及流量是否正常等
* 服务器监控,包括服务器是否宕机,服务器硬件是否有异常等。
* 系统监控,包括CPU、内存、磁盘、网卡等各项指标。
* 应用监控,包括:端口连通性,进程存活,QPS,连接数等指标。常用的开源中间件如nginx、redis、mysql等的监控数据也算是这一类的。
* 业务监控,包括业务关心的各项指标,例如登录验证成功/失败次数、某类型业务错误的次数、订单数等,业务指标通常与业务逻辑需求有关。

怎么上报业务指标?

通过在项目中使用micrometer库进行打点,由micrometer异步上报到监控系统。下面以使用spring boot 1.5的api-gatway项目为例。

micrometer简介

  • spring boot 2的默认指标库
  • 支持指标的多维,即Tag
  • 支持上报到Atlas、Graphite、Influx、Prometheus、Datadog等监控系统,可以看做Metric版的SL4J,屏蔽了各监控系统命名差异、Counter上报方式差异、percentile计算差异。
  • 支持Gauge、Counter、DistributionSummary、Timer(LongTimer)等Metric
  • 内置了一些指标,如JVM指标
  • 提供大量针对后端监控系统的spring boot starter,配置简单,文档齐全

引入micrometer包

    <properties>
        <micrometer.version>1.0.5</micrometer.version>
    </properties>

    <dependencies>
        ...
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-spring-legacy</artifactId>
            <version>${micrometer.version}</version>
        </dependency>
        <dependency>
            <groupId>com.yunzhijia.devops</groupId>
            <artifactId>micrometer-registry-falcon</artifactId>
            <version>1.0.5</version>
        </dependency>
        ...
    </dependencies>

    <repositories>
        ...
        <repository>
            <id>nexus-release</id>
            <url>http://192.168.0.22/nexus/content/repositories/cloudhub-release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
        ...
    </repositories>

io.micrometer:micrometer-spring-legacy是micrometer官方提供的spring boot 1.5的stater。com.yunzhijia.devops:micrometer-registry-falcon是自定义的micrometer插件,专门用于按格式要求上报到云之家的监控系统。

配置

application.properties里配置云之家的监控系统的上报接口的地址、上报时间间隔。

management.metrics.export.falcon.url=http://localhost:6060/api/push
management.metrics.export.falcon.hostTag=instance
management.metrics.export.falcon.step=30s

新建MicrometerConfig.java

@Configuration
public class MicrometerConfig {

    private static Logger logger = LoggerFactory.getLogger(MicrometerConfig.class);

    @Value("${spring.application.name}")
    private String appName;

    private String getInstanceIp() {
        String addr = null;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            addr = ip.getHostAddress();
        } catch (UnknownHostException e) {

        }
        logger.info("ip addr: {}", addr);
        return addr;
    }

    @Bean
    MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> {
            registry.config().commonTags("app", appName);
            String addr = getInstanceIp();
            {
                registry.config().commonTags("instance", addr);
            }
            registry.config().meterFilter(
                    new MeterFilter() {
                        @Override
                        public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {

                            if (id.getType() == Meter.Type.TIMER) {
                                return DistributionStatisticConfig.builder()
                                        .percentilesHistogram(true)
                                        .percentiles(0.5, 0.75, 0.90)
                                        .sla(Duration.ofMillis(50).toNanos(),
                                                Duration.ofMillis(100).toNanos(),
                                                Duration.ofMillis(200).toNanos(),
                                                Duration.ofMillis(500).toNanos(),
                                                Duration.ofSeconds(1).toNanos(),
                                                Duration.ofSeconds(2).toNanos(),
                                                Duration.ofSeconds(5).toNanos(),
                                                Duration.ofSeconds(10).toNanos())
                                        .minimumExpectedValue(Duration.ofMillis(1).toNanos())
                                        .maximumExpectedValue(Duration.ofSeconds(10).toNanos())
                                        .build()
                                        .merge(config);
                            } else {
                                return config;
                            }
                        }
                    });
        };
    }

    @Bean(name = "requestCounter")
    Counter requestCounter() {
        return Counter.builder(MetricDef.C_REQUEST)
                .register(Metrics.globalRegistry);
    }

    @Bean(name = "methodTypeRpcCounter")
    Counter methodTypeRpcCounter() {
        return Counter.builder(MetricDef.C_METHOD_TYPE).tags("type", "rpc")
                .register(Metrics.globalRegistry);
    }

    @Bean(name = "methodTypeHttpCounter")
    Counter methodTypeHttpCounter() {
        return Counter.builder(MetricDef.C_METHOD_TYPE).tags("type", "http")
                .register(Metrics.globalRegistry);
    }

上述代码配置了
1. 所有Metric的公共标签app和instance,app表示来自哪个服务,instance表示来自服务的实例(所有服务端口不重叠,故用IP表示实例)。
2. 对Timer进行设置,percentilesHistogram(true)表示上报分布的所有bucket;统计和上报的百分位数包括0.5,0.75,0.90;sla()额外添加了[50ms,100ms,200ms,500ms,1s,2s,5s,10s]这些bucket;并限定统计的耗时范围在0至10s之间。
3. 定义了3个不同的Counter,总请求数,rpc和http类型接口的请求数。定义为bean,之后利用Spring注入的方式可以减少Counter对象的查找耗时。

打点统计

在相应的业务代码文件引入counter,并打点。

@Component
public class MethodExistedPreFilter extends ZuulFilter{
    ...

    @Autowired
    @Qualifier("methodTypeRpcCounter")
    Counter methodTypeRpcCounter;

    @Autowired
    @Qualifier("methodTypeHttpCounter")
    Counter methodTypeHttpCounter;

    ...

    @Override
    public Object run() {

        ...
        if (StringUtils.equalsIgnoreCase(routeVo.getProtocol(), "rpc")) {
            methodTypeRpcCounter.increment();

        } else {
            methodTypeHttpCounter.increment();
        }
        ...
    }

    ...

}

QQ20180725-194449@2x

什么不该/不需要上报?

  • 内部服务之间的相互调用的次数、耗时不需要上报。因为已经由PinPoint统一进行采集和上报了,指标监控系统回去PinPoint拉取这些数据。
  • 如果tag值是无限的,不能上报。比如你可能想按appId统计接口调用次数,don’t do that.