作者简介:
赵君|南京爱福路汽车科技有限公司基础设施部云原生工程师,过来始终从事 java 相干的架构和研发工作。目前次要负责公司的云原生落地相干工作,负责 F6 基础设施和业务外围利用全面上云和云原生化革新。
徐航|南京爱福路汽车科技有限公司基础设施部云原生工程师,过来始终负责数据库高可用以及相干运维和调优工作。目前次要负责研发效力 DevOps 的落地以及业务零碎云原生可观测性的革新。
随着分布式架构逐步成为了架构设计的支流,可观测性(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 格局的指标示意格局样例。
# HELP http_requests_total The total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="post",code="200"} 1027
http_requests_total{method="post",code="400"} 3
# Escaping in label values:
msdos_file_access_time_seconds{path="C:\DIR\FILE.TXT",error="Cannot find file:\n"FILE.TXT""} 1.458255915e9
# Minimalistic line:
metric_without_timestamp_and_labels 12.47
# A weird metric from before the epoch:
something_weird{problem="division by zero"} +Inf -3982045
# A histogram, which has a pretty complex representation in the text format:
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
# Finally a summary, which has a complex representation, too:
# HELP rpc_duration_seconds A summary of the RPC duration in seconds.
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds{quantile="0.9"} 9001
rpc_duration_seconds{quantile="0.99"} 76656
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
指标的数据由指标名(metric_name),一组 key/value 标签(label_name=label_value),数字类型的指标值(value),工夫戳组成。
metric_name ["{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"`} [","] "}"
] 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 的具体实现如下。
- 引入 Spring Boot Actuator 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer.version}</version>
</dependency>
- 进行初始配置
Actuator 默认开启了一些指标的收集,比方 system, jvm, http,能够通过配置敞开它们。其实仅仅是咱们须要敞开,因为咱们曾经接了 jmx exporter 了。
management.metrics.enable.jvm=false
management.metrics.enable.process=false
management.metrics.enable.system=false
如果不心愿 Web 利用的 Actuator 治理端口和利用端口重合的话,能够应用 management.server.port 设置独立的端口。这是好的实际,能够看到黑客针对 actuator 的攻打,然而换了端口号,不裸露公网问题会少很多。
1management.server.port=xxxx
- 配置 spring bean
TimedAspect 的 Tags.empty() 是成心的,避免产生太长的 class 名称对 prometheus 造成压力。
@PropertySource(value = {"classpath:/micrometer.properties"})
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {return new TimedAspect(registry, (pjp) -> Tags.empty());
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {return new CountedAspect(registry);
}
@Bean
public PrometheusMetricScrapeEndpoint prometheusMetricScrapeEndpoint(CollectorRegistry collectorRegistry) {return new PrometheusMetricScrapeEndpoint(collectorRegistry);
}
@Bean
public PrometheusMetricScrapeMvcEndpoint prometheusMvcEndpoint(PrometheusMetricScrapeEndpoint delegate) {return new PrometheusMetricScrapeMvcEndpoint(delegate);
}
}
利用接入时,引入 micrometer-spring-boot-starter 依赖
<dependency>
<groupId>xxx</groupId>
<artifactId>micrometer-spring-boot-starter</artifactId>
</dependency>
当初,就能够通过拜访 http://ip:port/actuator/prome…,来查看 Micrometer 记录的数据。
自定义业务指标
Micrometer 内置了 Counted 和 Timed 两个 annotation。能够通过在对应的办法上加上 @Timed 和 @Counted 注解,来收集办法的调用次数,工夫和是否产生异样等信息。
@Timed
如果想要记录打印办法的调用次数和工夫,须要给 print 办法加上 @Timed 注解,并给指标定义一个名称。
@Timed(value = "biz.print", percentiles = {0.95, 0.99}, description = "metrics of print")
public String print(PrintData printData) {}
在 print 办法上加上 @Timed 注解之后,Micrometer 会记录 print 办法的调用次数 (count),办法调用最大耗时(max),办法调用总耗时(sum) 三个指标。percentiles = {0.95, 0.99} 示意计算 p95,p99 的申请工夫。记录的指标数据如下。
biz_print_seconds_count{exception="none"} 4.0
biz_print_seconds_sum{exception="none"} 7.783213927
biz_print_seconds_max{exception="none"} 6.14639717
biz_print_seconds{exception="NullPointerException"} 0.318767104
biz_print_seconds{exception="none",quantile="0.95",} 0.58720256
biz_print_seconds{exception="none",quantile="0.99",} 6.157238272
@Timed 注解反对配置一些属性:
- value:必填,指标名
- extraTags:给指标定义标签,反对多个,格局 {“key”, “value”, “key”, “value”}
- percentiles:小于等于 1 的数,计算工夫的百分比散布,比方 p95,p99
- histogram:记录办法耗时的 histogram 直方图类型指标
@Timed 会记录办法抛出的异样。不同的异样会被记录为独立的数据。代码逻辑是先 catch 办法抛出的异样,记录下异样名称,而后再抛出办法自身的异样:
try {return pjp.proceed();
} catch (Exception ex) {exceptionClass = ex.getClass().getSimpleName();
throw ex;
} finally {
try {sample.stop(Timer.builder(metricName)
.description(timed.description().isEmpty() ? null : timed.description())
.tags(timed.extraTags())
.tags(EXCEPTION_TAG, exceptionClass)
.tags(tagsBasedOnJoinPoint.apply(pjp))
.publishPercentileHistogram(timed.histogram())
.publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles())
.register(registry));
} catch (Exception e) {// ignoring on purpose}
}
@Counted
如果不关怀办法执行的工夫,只关怀办法调用的次数,甚至只关怀办法调用产生异样的次数,应用 @Counted 注解是更好的抉择。recordFailuresOnly = true 示意只记录异样的办法调用次数。
@Timed(value = "biz.print", recordFailuresOnly = true, description = "metrics of print")
public String print(PrintData printData) {}
记录的指标数据如下。
biz_print_failure_total{class="com.xxx.print.service.impl.PrintServiceImpl",exception="NullPointerException",method="print",result="failure",} 4.0
counter 是一个递增的数值,每次办法调用后,会自增 1。
private void record(ProceedingJoinPoint pjp, Counted counted, String exception, String result) {counter(pjp, counted)
.tag(EXCEPTION_TAG, exception)
.tag(RESULT_TAG, result)
.register(meterRegistry)
.increment();}
private Counter.Builder counter(ProceedingJoinPoint pjp, Counted counted) {Counter.Builder builder = Counter.builder(counted.value()).tags(tagsBasedOnJoinPoint.apply(pjp));
String description = counted.description();
if (!description.isEmpty()) {builder.description(description);
}
return builder;
}
Gauge
Gauge 用来形容在一个范畴内继续稳定的变量。Gauge 通常用于变动的测量值,例如雪花算法的 workId,打印的模板 id,线程池工作队列数等。
- 注入 PrometheusMeterRegistry
- 结构 Gauge。给指标命名并赋值。
@Autowired
private PrometheusMeterRegistry meterRegistry;
public void buildGauge(Long workId) {Gauge.builder("biz.alphard.snowFlakeIdGenerator.workId", workId, Long::longValue)
.description("alphard snowFlakeIdGenerator workId")
.tag("workId", workId.toString())
.register(meterRegistry).measure();}
记录的指标数据如下。
biz_alphard_snowFlakeIdGenerator_workId{workId="2"} 2
配置 SLA 指标
如果想要记录指标工夫数据的 sla 散布,Micrometer 提供了对应的配置:
management.metrics.distribution.sla[biz.print]=300ms,400ms,500ms,1s,10s
记录的指标数据如下。
biz_print_seconds_bucket{exception="none",le="0.3",} 1.0
biz_print_seconds_bucket{exception="none",le="0.4",} 3.0
biz_print_seconds_bucket{exception="none",le="0.5",} 10.0
biz_print_seconds_bucket{exception="none",le="0.6",} 11.0
biz_print_seconds_bucket{exception="none",le="1.0",} 11.0
biz_print_seconds_bucket{exception="none",le="10.0",} 12.0
biz_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 采集利用的指标数据,咱们须要做两件事:
- 利用通过 service 暴露出 actuator 端口,并增加 label: monitor/metrics
apiVersion: v1
kind: Service
metadata:
name: print-svc
labels:
monitor/metrics: ""
spec:
ports:
- name: custom-metrics
port: xxxx
targetPort: xxxx
protocol: TCP
type: ClusterIP
selector:
app: print-test
- 增加 ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: metrics
labels:
app: metric-monitor
spec:
namespaceSelector:
any: true
endpoints:
- interval: 15s
port: custom-metrics
path: "/manage/prometheusMetric"
selector:
matchLabels:
monitor/metrics: ""
Prometheus 会定时拜访 service 的 endpoints (http://podip:port/manage/prom…),拉取利用的 metrics,保留到本人的时序数据库。
Prometheus 存储的数据是文本格式,尽管 Prometheus 也有 Graph,然而不够炫酷,而且性能无限。还须要有一些可视化工具去展现数据,通过规范易用的可视化大盘去获知以后零碎的运行状态。比拟常见的解决方案就是 Grafana。Prometheus 内置了弱小的时序数据库,并提供了 PromQL 的数据查询语言,能对时序数据进行丰盛的查问、聚合以及逻辑运算。通过在 Grafana 配置 Prometheus 数据源和 PromQL,让 Grafana 去查问 Prometheus 的指标数据,以图表的模式展现进去。
- grafana 配置 Prometheus 数据源
- 增加看板,配置数据源,query 语句,图表款式
- 能够在一个 dasborad 增加多个看板,形成监控大盘。
精准告警
任何零碎都不是完满的,当出现异常和故障时,能在第一工夫发现问题且疾速定位问题起因就尤为重要。但要想做到以上这两点,只有数据收集是不够的,须要依赖欠缺的监控和告警体系,迅速反馈并收回告警。
咱们最后的计划是,基于 Prometheus operator 的 PrometheusRule 创立告警规定,Prometheus servers 把告警发送给 Alertmanager,Alertmanager 负责把告警发到钉钉群机器人。然而这样运行一段时间之后,咱们发现这种形式存在一些问题。SRE 团队和研发团队负责人收到的告警太多,所有的告警都发到一个群里,关上群音讯,满屏的告警题目,告警级别,告警值。其中有须要运维解决的零碎告警,有须要研发解决的利用告警,信息太多,很难疾速筛选出高优先级的告警,很难疾速转派告警到对应的解决人。所以咱们心愿利用告警能够精准发送到利用归属的研发团队。
通过一段时间的调研,咱们最终抉择阿里云的《ARMS 告警运维核心》来负责告警的治理。ARMS 告警运维核心反对接入 Prometheus 数据源,反对增加钉钉群机器人作为联系人。
- 收集研发团队的钉钉群机器人的 webhook 地址,创立机器人作为联系人。
- 给每个研发团队别离配置告诉策略,告诉策略筛选告警信息里的 team 字段,并绑定对应的钉钉群机器人联系人。\
通过这个形式,实现了利用的告警间接发送到对应的研发团队,节俭了信息筛选和二次转派的工夫,进步了告警解决效率。
成果如下:
ARMS 告警运维核心反对接入 grafana,zabbix,arms 等多种数据源,具备告警分派和认领,告警汇总去重,通过降级告诉形式对长时间没有解决的告警进行屡次揭示,或降级告诉到领导,保障告警及时解决。