Metrics:如何让线上应用更加透明?

1
上期我们结合《SRE Google 运维解密》,对监控系统进行了一次脉络梳理,知道一旦离开了监控系统,我们就没法辨别一个服务是不是在正常提供服务,就如同线上的服务在随风裸奔。
文章分享最后,我们把 Google 十余年的监控实践,也尝试进行简单梳理,对于后期落地实践有一定参考意义。

不过,虽然对监控系统有了脉络上的了解,但是我们也知道,如果没有一套设计周全的监控指标体系,也就如同蒙着眼睛在狂奔,
本期就好好说说:
指标监控的类库 Metrics。


2

Metrics 是啥?
简单去说,Metrics 是一款监控指标的度量类库,提供了一种功能强大的工具包,帮助开发者来完成自定义的监控工作。
再通俗点,Metrics 类库是搬砖党的福音。


Metrics 的几种度量类型
?在看框架源码时,时不时会看到一些 Meter、Guage、Counter、Histogram 等关键词,到底这些词说的都是啥?


为了更好的熟读源码,就借助  Metrics
定义的几种度量类型,逐个进行解密。

Meter
主要用于统计系统中某一个事件的速率,可以反应系统当前的处理能力,帮助我们判断资源是否已经不足。
可以很方便帮助我们统计,每秒请求数(TPS)、每秒查询数(QPS)、最近 1 分钟平均每秒请求数、最近 5 分钟平均每秒请求数、最近 15 分钟平均每秒请求数等。

Guage
是最简单的度量指标,只有一个简单的返回值,通常用来记录一些对象或者事物的瞬时值。
通过 Gauge 可以完成自定义的度量类型,可以用于衡量一个待处理队列中任务的个数,以及目前内存使用量等等场景。

Counter
是累计型的度量指标,内部用 Gauge 封装了 AtomicLong。
主要用它来统计队列中 Job 的总数;
错误出现次数;
服务请求数等等场景。

Histogram
是统计数据的分布情况的度量指标,提供了最小值,最大值,中间值,还有中位数,75 百分位,90 百分位,95 百分位,98 百分位,99 百分位,和 99.9 百分位的值。
使用的场景,例如统计流量最大值、最小值、平均值、中位值等等。

Timer
本质是 Histogram 和 Meter 的结合,可以很方便的统计请求的速率和处理时间,例如磁盘读延迟统计,以及接口调用的延迟等信息的统计等等场景。

Metrics 类库中还有啥?


3
说了那么多 Metrics 类库的概念,也说的那么强大,不妨撸码实践,谈谈虚实。

Metrics 中基本度量类型的实践


如脑图所示,主要分两步走,先引入相关依赖,然后写代码反复进行体会。

Meter 代码实践(详细看代码呗)。

import com.codahale.metrics.ConsoleReporter;

import com.codahale.metrics.Meter;

import com.codahale.metrics.MetricRegistry;


import java.util.concurrent.TimeUnit;
/** * Meters(TPS 计算器) * 示例: * 例如:每秒请求数(TPS) * 例如:最近 1 分钟平均每秒请求数 * 例如:最近 5 分钟平均每秒请求数 * 例如:最近 15分钟平均每秒请求数 * * @author 一猿小讲 */ public class MeterApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ private final MetricRegistry metrics = new MetricRegistry();
/** * Meters 本身是一个自增计数器,统计系统中某一个事件的速率 */ private final Meter requests = metrics.meter("requests");
/** * 处理请求 */ public void handleRequest() { requests.mark(); // etc System.out.println("处理请求handleRequest"); }
/** * 启动指标报告 * (采用控制台输出的形式) */ public void startReport() { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); reporter.start(1, TimeUnit.SECONDS); }
/** * 等待 2 分钟 */ static void wait120Seconds() { try { Thread.sleep(120 * 1000); } catch (InterruptedException e) { } }
/** * 程序入口 * * @param args */ public static void main(String[] args) { MeterApp meterApp = new MeterApp(); // 启动监控指标报告展示 meterApp.startReport(); // 处理 20 笔请求,观察指标 for (int i = 0; i < 20; i++) { meterApp.handleRequest(); } // 等待 120 秒 wait120Seconds(); } }

运行结果如下,体会 Meter 结果背后的概念。


Gauge 代码实践
(详细看代码呗)




import com.codahale.metrics.ConsoleReporter;

import com.codahale.metrics.Gauge;

import com.codahale.metrics.MetricRegistry;


import java.util.Queue; import java.util.Random; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit;
/** * Gauges 最简单的度量指标 * 示例:衡量一个待处理队列中任务的个数; * * @author 一猿小讲 */ public class GaugeApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ private final MetricRegistry metrics = new MetricRegistry();
/** * 任务队列 */ private static final Queue jobQueue = new LinkedBlockingQueue();

/** * 处理 */ public void handle() { // 向 mertics 注册 Gauge 指标监控 metrics.register(MetricRegistry.name(GaugeApp.class, "jobQueue", "size"), new Gauge() { public Integer getValue() { return jobQueue.size(); } });
// 模拟向队列中放入任务 while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } jobQueue.add(new Random().nextInt(10) + "-Job"); } }
public static void main(String[] args) { GaugeApp gaugeApp = new GaugeApp(); // 启动监控指标报告展示 gaugeApp.startReport(); // 注册Gauge指标监控,并模拟添加任务到队列 gaugeApp.handle(); }
/** * 启动指标报告 * (采用控制台输出的形式) */ void startReport() { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); reporter.start(1, TimeUnit.SECONDS); } }

运行结果如下,
体会 Gauge 结果背后的概念。


Counter 代码实践 (详细看代码呗)



import com.codahale.metrics.ConsoleReporter;

import com.codahale.metrics.Counter;

import com.codahale.metrics.MetricRegistry;


import java.util.Queue; import java.util.Random; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit;
/** * Counters 累计型的度量指标 * 示例:统计一个待处理队列中任务的个数; * * @author 一猿小讲 */ public class CounterApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ private final MetricRegistry metrics = new MetricRegistry();
/** * 任务队列 */ private static final Queue<String> jobQueue = new LinkedBlockingQueue<String>();
/** * 累计型的度量指标 */ private final Counter pendingJobs = metrics.counter("pending-jobs.size");
/** * 向队列中添加任务 * * @param job */ public void addJob(String job) { pendingJobs.inc(); jobQueue.offer(job); }
/** * 从队列中取出任务 * * @return */ public String takeJob() { pendingJobs.dec(); return jobQueue.poll(); }
/** * 处理 */ public void handle() { Random random = new Random(); // 模拟向队列中放入任务 while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
String jobId; if (random.nextInt(10) > 8) { jobId = takeJob(); System.out.println(String.format("取出的任务ID为%s", jobId)); } else { jobId = random.nextInt(100) + "-Job"; addJob(jobId); System.out.println(String.format("向队列中加入任务,ID为%s", jobId)); } } }
/** * 启动指标报告 * (采用控制台输出的形式) */ void startReport() { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); reporter.start(1, TimeUnit.SECONDS); }
/** * 程序入口 * @param args */ public static void main(String[] args) { CounterApp counterApp = new CounterApp(); // 启动监控指标报告展示 counterApp.startReport(); // 并模拟生产/消费任务到队列 counterApp.handle(); } }

运行结果如下,
体会 Counter 结果背后的概念。

Histogram 


代码实践 (详细看代码呗)




import com.codahale.metrics.ConsoleReporter;

import com.codahale.metrics.Histogram;

import com.codahale.metrics.MetricRegistry;


import java.util.Random; import java.util.concurrent.TimeUnit;
/** * Histogram 统计数据的分布情况 * 示例: 响应字节的最大值、最小值、平均值、中位值。 * * @author 一猿小讲 */ public class HistogramApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ private final MetricRegistry metrics = new MetricRegistry();
/** * Histogram 统计数据的分布情况,向 metrics 注册并获取 Histogram 监控 */ private final Histogram responseSizes = metrics.histogram("response-sizes");
/** * 处理请求 */ public void handle() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // etc responseSizes.update(new Random().nextInt(100)); } }
/** * 启动指标报告 * (采用控制台输出的形式) */ public void startReport() { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); reporter.start(1, TimeUnit.SECONDS); }
/** * 程序入口 * * @param args */ public static void main(String[] args) { HistogramApp histogramApp = new HistogramApp(); // 启动监控指标报告展示 histogramApp.startReport(); // 处理请求,观察指标 histogramApp.handle(); } }

运行结果如下,
体会
Histogram 结
果背后的概念。

Timer 


代码实践 (详细看代码呗)




import com.codahale.metrics.ConsoleReporter;

import com.codahale.metrics.MetricRegistry;

import com.codahale.metrics.Timer;


import java.util.Random; import java.util.concurrent.TimeUnit;
/** * Timer 是 Histogram 和 Meter 的结合,可以比较方便地统计请求的速率和处理时间。 * 应用场景: * 例如:磁盘读延迟统计; * 例如:接口调用的延迟等信息的统计。 * * @author 一猿小讲 */ public class TimerApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ private final MetricRegistry metrics = new MetricRegistry();
/** * 向 metrics 注册并获取 Timer 监控 */ private final Timer responses = metrics.timer("responses");
/** * 处理请求 */ public void handle() {
Timer.Context context; Random random = new Random();
while (true) { context = responses.time(); // 业务逻辑处理 etc try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } context.stop(); } }
/** * 启动指标报告 * (采用控制台输出的形式) */ void startReport() { ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); reporter.start(1, TimeUnit.SECONDS); }
/** * 等待 2 分钟 */ static void wait120Seconds() { try { Thread.sleep(120 * 1000); } catch (InterruptedException e) { } }
/** * 程序入口 * * @param args */ public static void main(String[] args) { TimerApp timerApp = new TimerApp(); // 启动监控指标报告展示 timerApp.startReport(); // 处理请求,观察指标 timerApp.handle(); // 等它 2 分钟 wait120Seconds(); } }

运行结果如下,
体会
Timer 结
果背后的概念。

Metrics Reporter 代码实践


Metrics 提供了 Reporter 接口来展示获取到的指标数据,可以通过 JMX、Console、CSV、SLF4J、HTTP、Graphite 等方式来报告展示指标值。
本次以 JMXReporter 为例进行代码实践体验。

import com.codahale.metrics.Meter;

import com.codahale.metrics.MetricRegistry;

import com.codahale.metrics.jmx.JmxReporter;


/** * JMXReporter 体验 * * @author 一猿小讲 */ public class JMXReporterApp {
/** * MetricRegistry 是 Metrics 的核心,用于存放应用中所有 metrics 的容器 * 所有度量工具都要注册到 MetricRegistry 实例中才可以使用 */ static final MetricRegistry metrics = new MetricRegistry();
/** * 启动 JMXReporter */ static void startReport() { JmxReporter reporter = JmxReporter.forRegistry(metrics).build(); reporter.start(); }
/** * 等待 2 分钟 */ static void wait120Seconds() { try { Thread.sleep(120 * 1000); } catch (InterruptedException e) { } }
/** * 程序入口 * * @param args */ public static void main(String[] args) { // 启动监控指标报告展示 startReport();
// Meters(TPS 计算器) Meter requests = metrics.meter("requests"); requests.mark();
// 等 2 分钟 wait120Seconds(); } }

代码运行成功后,在控制台输入 jconsole,效果如下。

Metrics-healthchecks 代码实践


Metrics 提供了 metrics-healthchecks 模块,可以对运行服务进行健康检查。

import com.codahale.metrics.health.HealthCheck;

import com.codahale.metrics.health.HealthCheckRegistry;


import java.util.Map;
/** * 应用健康检查初体验 * * @author 一猿小讲 */ public class HealthCheckApp {
public static void main(String[] args) { HealthCheckRegistry healthChecks = new HealthCheckRegistry(); healthChecks.register("MySQL", new DatabaseHealthCheck(new Database())); final Map results = healthChecks.runHealthChecks(); for (Map.Entry entry : results.entrySet()) { if (entry.getValue().isHealthy()) { System.out.println(entry.getKey() + " is healthy"); } else { System.err.println(entry.getKey() + " is UNHEALTHY: " + entry.getValue().getMessage()); final Throwable e = entry.getValue().getError(); if (e != null) { e.printStackTrace(); } } } } }
class DatabaseHealthCheck extends HealthCheck { private final Database database;
public DatabaseHealthCheck(Database database) { this.database = database; }
@Override public HealthCheck.Result check() { if (database.isConnected()) { return HealthCheck.Result.healthy(); } else { return HealthCheck.Result.unhealthy("Cannot connect to " + database.getUrl()); } } }

class Database { public boolean isConnected() { return false; }
public String getUrl() { return "jdbc:localhost:3306"; } }

运行程序,控制台输出如下。
4
Metrics 类库分享就到这里,希望你能有所收获。
鉴于线上跑的每一个应用,都需要配备一套监控系统,如果能借用 Metrics 类库简单实现监控,何乐而不为呢?
鉴于开源的监控轮子与日俱增,我们在设计相关监控系统的时候,如果能提前了解规范,并按照其规范设计,那么与开源轮子将会无缝对接。

好了,本次的分享就到这里,希望你们能够喜欢。
下期我们将钻到框架源码里,去透彻分析 Metrics 的应用与展示,敬请期待。

推荐阅读:

如何做监控?Google SRE 解密

这些技术轮子,让监控落地成为现实!

监控实战Prometheus+Grafana