vlambda博客
学习文章列表

分布式系统可观测性之应用业务指标监控实践

随着分布式架构逐渐成为了架构设计的主流,可观测性(Observability)一词也日益被人频繁地提起。
2017 年的分布式追踪峰会(2017 Distributed Tracing Summit)结束后,Peter Bourgon 撰写了总结文章《Metrics, Tracing, and Logging》系统地阐述了这三者的定义、特征,以及它们之间的关系与差异。文中将可观测性问题映射到了如何处理指标(metrics)、追踪(tracing)、日志(logging)三类数据上。
其后,Cindy Sridharan 在其著作《Distributed Systems Observability》中,进一步讲到指标、追踪、日志是可观测性的三大支柱(three pillars)。

到了 2018 年, CNCF Landscape 率先出现了 Observability 的概念,将可观测性( Observability )从控制论( Cybernetics )中引入到 IT 领域。在控制论中,可观测性是指系统可以由其外部输出,来推断其内部状态的程度,系统的可观察性越强,我们对系统的可控制性就越强。

可观测性可以解决什么问题?Google SRE Book 第十二章给出了简洁明快的答案:快速排障。

There are many ways to simplify and speed troubleshooting. Perhaps the most fundamental are:

  • Building observability—with both white-box metrics and structured logs—into each component from the ground up

  • Designing systems with well-understood and observable interfaces between components.

Google SRE Book, Chapter 12

而在云原生时代,分布式系统越来越复杂,分布式系统的变更是非常频繁的,每次变更都可能导致新类型的故障。应用上线之后,如果缺少有效的监控,很可能导致遇到问题我们自己都不知道,需要依靠用户反馈才知道应用出了问题。

本文主要讲述如何建立应用业务指标Metrics监控和如何实现精准告警。Metrics 可以翻译为度量或者指标,指的是对于一些关键信息以可聚合的、数值的形式做定期统计,并绘制出各种趋势图表。透过它,我们可以观察系统的状态与趋势。

技术栈选择

我们的应用都是 Spring Boot 应用,并且使用Spring Boot Actuator实现应用的健康检查。从 Spring Boot 2.0 开始,Actuator 将底层改为 Micrometer,提供了更强、更灵活的监测能力。Micrometer支持对接各种监控系统,包括Prometheus。

所以我们选择Micrometer收集业务指标,Prometheus进行指标的存储和查询,通过 Grafana 进行展示,通过阿里云的告警中心实现精准告警。

指标收集

对于整个研发部门来说,应该聚焦在能够实时体现公司业务状态的最核心的指标上。例如 Amazon 和 eBay 会跟踪销售量, Google 和 Facebook 会跟踪广告曝光次数等与收入直接相关的实时指标。

Prometheus默认采用一种名为OpenMetrics的指标协议。OpenMetrics是一种基于文本的格式。下面是一个基于 OpenMetrics 格式的指标表示格式样例。

 1# HELP http_requests_total The total number of HTTP requests.
2# TYPE http_requests_total counter
3http_requests_total{method="post",code="200"1027
4http_requests_total{method="post",code="400"}    3
5
6# Escaping in label values:
7msdos_file_access_time_seconds{path="C:\\DIR\\FILE.TXT",error="Cannot find file:\n\"FILE.TXT\""1.458255915e9
8
9# Minimalistic line:
10metric_without_timestamp_and_labels 12.47
11
12# A weird metric from before the epoch:
13something_weird{problem="division by zero"} +Inf -3982045
14
15# A histogram, which has a pretty complex representation in the text format:
16# HELP http_request_duration_seconds A histogram of the request duration.
17# TYPE http_request_duration_seconds histogram
18http_request_duration_seconds_bucket{le="0.05"24054
19http_request_duration_seconds_bucket{le="0.1"33444
20http_request_duration_seconds_bucket{le="0.2"100392
21http_request_duration_seconds_bucket{le="0.5"129389
22http_request_duration_seconds_bucket{le="1"133988
23http_request_duration_seconds_bucket{le="+Inf"144320
24http_request_duration_seconds_sum 53423
25http_request_duration_seconds_count 144320
26
27# Finally a summary, which has a complex representation, too:
28# HELP rpc_duration_seconds A summary of the RPC duration in seconds.
29# TYPE rpc_duration_seconds summary
30rpc_duration_seconds{quantile="0.01"3102
31rpc_duration_seconds{quantile="0.05"3272
32rpc_duration_seconds{quantile="0.5"4773
33rpc_duration_seconds{quantile="0.9"9001
34rpc_duration_seconds{quantile="0.99"76656
35rpc_duration_seconds_sum 1.7560473e+07
36rpc_duration_seconds_count 2693

指标的数据由指标名(metric_name),一组 key/value 标签(label_name=label_value),数字类型的指标值(value),时间戳组成。

1metric_name [
2  "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}"
3] value [ timestamp ]

Meter

Micrometer提供了多种度量类库(Meter),Meter是指一组用于收集应用中的度量数据的接口。Micrometer 中,Meter的具体类型包括:Timer, Counter, Gauge, DistributionSummary, LongTaskTimer, FunctionCounter, FunctionTimer, and TimeGauge.

  • Counter用来描述一个单调递增的变量,如某个方法的调用次数,缓存命中/访问总次数等。支持配置recordFailuresOnly,即只记录方法调用失败的次数。Counter的指标数据,默认有四个label:class, method, exception, result。

  • Timer会同时记录totalcount, sumtime, maxtime三种数据,有一个默认的label: exception。

  • Gauge用来描述在一个范围内持续波动的变量。Gauge通常用于变动的测量值,比如队列中的消息数量,线程池任务队列数等。

  • DistributionSummary用于统计数据分布。

应用接入流程

为了方便微服务应用接入,我们封装了micrometer-spring-boot-starter。micrometer-spring-boot-starter的具体实现如下。

  1. 引入 Spring Boot Actuator 依赖

 1<dependency>
2  <groupId>org.springframework.boot</groupId>
3  <artifactId>spring-boot-starter-actuator</artifactId>
4</dependency>
5
6<dependency>
7  <groupId>io.micrometer</groupId>
8  <artifactId>micrometer-registry-prometheus</artifactId>
9  <version>${micrometer.version}</version>
10</dependency>
  1. 进行初始配置

Actuator默认开启了一些指标的收集,比如system, jvm, http,可以通过配置关闭它们。其实仅仅是我们需要关闭,因为我们已经接了jmx exporter了。

1management.metrics.enable.jvm=false
2management.metrics.enable.process=false
3management.metrics.enable.system=false

如果不希望 Web 应用的 Actuator 管理端口和应用端口重合的话,可以使用 management.server.port 设置独立的端口。这是好的实践,可以看到黑客针对actuator的攻击,但是换了端口号,不暴露公网问题会少很多。

1management.server.port=xxxx
  1. 配置spring bean

TimedAspect的Tags.empty()是故意的,防止产生太长的class名称对prometheus造成压力。

 1@PropertySource(value = {"classpath:/micrometer.properties"})
2@Configuration
3public class MetricsConfig {
4
5    @Bean
6    public TimedAspect timedAspect(MeterRegistry registry) {
7        return new TimedAspect(registry, (pjp) -> Tags.empty());
8    }
9
10    @Bean
11    public CountedAspect countedAspect(MeterRegistry registry) {
12        return new CountedAspect(registry);
13    }
14
15    @Bean
16    public PrometheusMetricScrapeEndpoint prometheusMetricScrapeEndpoint(CollectorRegistry collectorRegistry) {
17        return new PrometheusMetricScrapeEndpoint(collectorRegistry);
18    }
19
20    @Bean
21    public PrometheusMetricScrapeMvcEndpoint prometheusMvcEndpoint(PrometheusMetricScrapeEndpoint delegate) {
22        return new PrometheusMetricScrapeMvcEndpoint(delegate);
23    }
24
25}

应用接入时,引入micrometer-spring-boot-starter依赖

1<dependency>
2  <groupId>xxx</groupId>
3  <artifactId>micrometer-spring-boot-starter</artifactId>
4</dependency>

现在,就可以通过访问http://ip:port/actuator/prometheus,来查看Micrometer记录的数据。

自定义业务指标

Micrometer内置了Counted 和 Timed 两个annotation。可以通过在对应的方法上加上@Timed和@Counted注解,来收集方法的调用次数,时间和是否发生异常等信息。

@Timed
如果想要记录打印方法的调用次数和时间,需要给print方法加上@Timed注解,并给指标定义一个名称。

1@Timed(value = "biz.print", percentiles = {0.950.99}, description = "metrics of print")
2public String print(PrintData printData) {
3
4}

在print方法上加上@Timed注解之后,Micrometer会记录print方法的调用次数(count),方法调用最大耗时(max),方法调用总耗时(sum)三个指标。percentiles = {0.95, 0.99}表示计算p95, p99的请求时间。记录的指标数据如下。

1biz_print_seconds_count{exception="none"4.0
2biz_print_seconds_sum{exception="none"7.783213927
3biz_print_seconds_max{exception="none"6.14639717
4biz_print_seconds{exception="NullPointerException"0.318767104
5biz_print_seconds{exception="none",quantile="0.95",} 0.58720256
6biz_print_seconds{exception="none",quantile="0.99",} 6.157238272

@Timed注解支持配置一些属性:

  • value:必填,指标名

  • extraTags: 给指标定义标签,支持多个,格式  {"key", "value", "key", "value"}

  • percentiles: 小于等于1的数,计算时间的百分比分布,比如p95,p99

  • histogram:记录方法耗时的 histogram 直方图类型指标

@Timed会记录方法抛出的异常。不同的异常会被记录为独立的数据。代码逻辑是先catch方法抛出的异常,记录下异常名称,然后再抛出方法本身的异常:

 1try {
2    return pjp.proceed();
3catch (Exception ex) {
4    exceptionClass = ex.getClass().getSimpleName();
5    throw ex;
6finally {
7    try {
8        sample.stop(Timer.builder(metricName)
9                    .description(timed.description().isEmpty() ? null : timed.description())
10                    .tags(timed.extraTags())
11                    .tags(EXCEPTION_TAG, exceptionClass)
12                    .tags(tagsBasedOnJoinPoint.apply(pjp))
13                    .publishPercentileHistogram(timed.histogram())
14                    .publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles())
15                    .register(registry));
16    } catch (Exception e) {
17        // ignoring on purpose
18    }
19}

@Counted
如果不关心方法执行的时间,只关心方法调用的次数,甚至只关心方法调用发生异常的次数,使用@Counted注解是更好的选择。recordFailuresOnly = true表示只记录异常的方法调用次数。

1@Timed(value = "biz.print", recordFailuresOnly = true, description = "metrics of print")
2public String print(PrintData printData) {
3
4}

记录的指标数据如下。

1biz_print_failure_total{class="com.xxx.print.service.impl.PrintServiceImpl",exception="NullPointerException",method="print",result="failure",} 4.0

counter是一个递增的数值,每次方法调用后,会自增1。

 1private void record(ProceedingJoinPoint pjp, Counted counted, String exception, String result) {
2    counter(pjp, counted)
3            .tag(EXCEPTION_TAG, exception)
4            .tag(RESULT_TAG, result)
5            .register(meterRegistry)
6            .increment();
7}
8
9private Counter.Builder counter(ProceedingJoinPoint pjp, Counted counted) {
10    Counter.Builder builder = Counter.builder(counted.value()).tags(tagsBasedOnJoinPoint.apply(pjp));
11    String description = counted.description();
12    if (!description.isEmpty()) {
13        builder.description(description);
14    }
15    return builder;
16}

Gauge
Gauge用来描述在一个范围内持续波动的变量。Gauge通常用于变动的测量值,例如雪花算法的workId,打印的模板id,线程池任务队列数等。

  1. 注入PrometheusMeterRegistry

  2. 构造Gauge。给指标命名并赋值。

1@Autowired
2private PrometheusMeterRegistry meterRegistry;
3
4public void buildGauge(Long workId) {
5    Gauge.builder("biz.alphard.snowFlakeIdGenerator.workId", workId, Long::longValue)
6            .description("alphard snowFlakeIdGenerator workId")
7            .tag("workId", workId.toString())
8            .register(meterRegistry).measure();
9}

记录的指标数据如下。

1biz_alphard_snowFlakeIdGenerator_workId{workId="2"} 2

配置SLA指标

如果想要记录指标时间数据的sla分布,Micrometer提供了对应的配置:

1management.metrics.distribution.sla[biz.print]=300ms,400ms,500ms,1s,10s

记录的指标数据如下。

1biz_print_seconds_bucket{exception="none",le="0.3",} 1.0
2biz_print_seconds_bucket{exception="none",le="0.4",} 3.0
3biz_print_seconds_bucket{exception="none",le="0.5",} 10.0
4biz_print_seconds_bucket{exception="none",le="0.6",} 11.0
5biz_print_seconds_bucket{exception="none",le="1.0",} 11.0
6biz_print_seconds_bucket{exception="none",le="10.0",} 12.0
7biz_print_seconds_bucket{exception="none",le="+Inf",} 12.0

存储查询

我们使用Prometheus进行指标数据的存储和查询。Prometheus采用拉取式采集(Pull-Based Metrics Collection)。Pull就是Prometheus主动从目标系统中拉取指标,相对地,Push 就是由目标系统主动推送指标。Prometheus 官方解释选择 Pull 的原因。

Pulling over HTTP offers a number of advantages:

  • You can run your monitoring on your laptop when developing changes.

  • You can more easily tell if a target is down.

  • You can manually go to a target and inspect its health with a web browser.

Overall, we believe that pulling is slightly better than pushing, but it should not be considered a major point when considering a monitoring system.

Prometheus也支持Push的采集方式,就是Pushgateway。

For cases where you must push, we offer the Pushgateway.

为了让Prometheus采集应用的指标数据,我们需要做两件事:

  1. 应用通过service暴露出actuator端口,并添加label: monitor/metrics

 1apiVersion: v1
2kind: Service
3metadata:
4  name: print-svc
5  labels:
6    monitor/metrics: ""
7spec:
8  ports:
9  - name: custom-metrics
10    port: xxxx
11    targetPort: xxxx
12    protocol: TCP
13  type: ClusterIP
14  selector:
15    app: print-test
  1. 添加ServiceMonitor

 1apiVersion: monitoring.coreos.com/v1
2kind: ServiceMonitor
3metadata:
4  name: metrics
5  labels:
6    app: metric-monitor
7spec:
8  namespaceSelector:
9    any: true
10  endpoints:
11  - interval: 15s
12    port: custom-metrics
13    path: "/manage/prometheusMetric"
14  selector:
15    matchLabels:
16      monitor/metrics: ""

Prometheus会定时访问service的endpoints(http://podip:port/manage/prometheusMetric),拉取应用的metrics,保存到自己的时序数据库。

Prometheus存储的数据是文本格式,虽然Prometheus也有Graph,但是不够炫酷,而且功能有限。还需要有一些可视化工具去展示数据,通过标准易用的可视化大盘去获知当前系统的运行状态。比较常见的解决方案就是 Grafana。Prometheus内置了强大的时序数据库,并提供了PromQL 的数据查询语言,能对时序数据进行丰富的查询、聚合以及逻辑运算。通过在Grafana配置Prometheus数据源和PromQL,让Grafana去查询Prometheus的指标数据,以图表的形式展示出来。

  1. grafana配置Prometheus数据源

分布式系统可观测性之应用业务指标监控实践
  1. 添加看板,配置数据源,query语句,图表样式

分布式系统可观测性之应用业务指标监控实践
  1. 可以在一个dasborad添加多个看板,构成监控大盘。

分布式系统可观测性之应用业务指标监控实践

分布式系统可观测性之应用业务指标监控实践

精准告警

任何系统都不是完美的,当出现异常和故障时,能在第一时间发现问题且快速定位问题原因就尤为重要。但要想做到以上这两点,只有数据收集是不够的,需要依赖完善的监控和告警体系,迅速反应并发出告警。
我们最初的方案是,基于Prometheus operator的PrometheusRule创建告警规则, Prometheus servers把告警发送给Alertmanager,Alertmanager负责把告警发到钉钉群机器人。但是这样运行一段时间之后,我们发现这种方式存在一些问题。SRE团队和研发团队负责人收到的告警太多,所有的告警都发到一个群里,打开群消息,满屏的告警标题,告警级别,告警值。其中有需要运维处理的系统告警,有需要研发处理的应用告警,信息太多,很难快速筛选出高优先级的告警,很难快速转派告警到对应的处理人。所以我们希望应用告警可以精准发送到应用归属的研发团队。

经过一段时间的调研,我们最终选择阿里云的《ARMS 告警运维中心》来负责告警的管理。ARMS 告警运维中心支持接入Prometheus数据源,支持添加钉钉群机器人作为联系人。

分布式系统可观测性之应用业务指标监控实践
  1. 给每个研发团队分别配置通知策略,通知策略筛选告警信息里的team字段,并绑定对应的钉钉群机器人联系人。

分布式系统可观测性之应用业务指标监控实践

通过这个方式,实现了应用的告警直接发送到对应的研发团队,节省了信息筛选和二次转派的时间,提高了告警处理效率。

效果如下:

ARMS 告警运维中心支持接入grafana,zabbix,arms等多种数据源,具有告警分派和认领,告警汇总去重,通过升级通知方式对长时间没有处理的告警进行多次提醒,或升级通知到领导,保证告警及时解决。