1. 介绍
Metrics 是 dropwizard.io 开源的一款 Java 工具包,为开发者提供了多种计算指标的工具类,用于单个 JVM 进程指标监控。通过适配器方式支持 Jetty、Logback、Log4j、Apache HttpClient 和 graphite 等开源库的指标监控。
Metrics 提供监控的工具:
-
Gauges:指标当前值。
-
Counters:指标自增自减。
-
Histograms: 指标分布情况,最大、最小、TP99 等。
-
Meters:指标频率,例如 TPS。
-
Timers:Histograms 和 Meters 结合使用。
-
Health Checks:服务健康状况监控。
指标结果输出方式:
-
ConsoleReporter:控制台
-
CsvReporter:CSV 文件
-
Slf4jReporter:Logback、Log4j 等日志输出
-
JmxReporter:基于 JMX 的输出
-
GangliaReporter:监控工具 Ganglia
-
GraphiteReporter:监控工具 Graphite
2. 实际应用
account 是 SDMK 的核心模块,提供用户管理、角色管理、权限管理、用户授权和用户鉴权功能,在 account 内部使用 Metrics 作为服务度量的工具。
Metrics 应用的具体场景:
-
Meters:统计鉴权接口的吞吐量。
-
Gauges:定时输出缓存键的数量。
-
Counters:统计鉴权接口失败的次数。
-
Histograms: 统计鉴权接口的最大、最小、平均的响应时间。
-
Timers:统计鉴权接口响应时间的分布,同时输出吞吐量信息。
-
Health Checks:account 模块是否存活。
3. Gauges 的使用
添加 maven 依赖:
<dependencies>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.2.3</version>
</dependency>
</dependencies>
复制代码
为了使输出的结果更加直观,使用 ConsoleReporter,代码如下:
public class MetricsGaugesTest extends Test0Abstract {
private HashMap<String, String> appMap;
private ConcurrentMap<String, String> tokenMap;
@Autowired
private ApiLoginService apiLoginService;
@Before
public void init() {
appMap = new HashMap<>(8);
appMap.put("b8a424e934264e769d450647b6ba62ca","fe72444d2ee94077b6949b647bdd3a14");
appMap.put("998bf8bd21c54571bdedef4d4d49cb87", "4e62603c33b84761a9289062edadc526");
appMap.put("ab19e506da564fb7a82a4bd2c8d237bd","c085ca4ade374f04aedc0f6b16422d0a");
appMap.put("073d546d30444d4eaed8344c7f42c782","df91daba26164123a7c595518ac6e517");
appMap.put("779f6078eace46ca9a21c8bee9bdb932","ec2a1d29087f49448f9507ef7b728ebf");
appMap.put("cda9f6e06a04440e8214c09ba171e991","dac3bbf93ed146238a9b3d68a7357e11");
tokenMap = new ConcurrentHashMap<>();
}
@Test
public void login() throws Exception {
MetricRegistry metrics = new MetricRegistry();
ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();
reporter.start(1, TimeUnit.SECONDS);
metrics.register(MetricRegistry.name(MetricsGaugesTest.class, "logon","size"),
new Gauge<Integer>() {
@Override
public Integer getValue() {
return tokenMap.size();
}
});
for (Map.Entry<String, String> entry : appMap.entrySet()) {
ApiLoginResult result = apiLoginService.login(entry.getKey(),entry.getValue());
if (result.getLogin()) {
tokenMap.put(result.getToken(),entry.getKey());
TimeUnit.SECONDS.sleep(1);
}
}
}
}
复制代码
运行后输出:
17-11-23 15:37:31=============================================================
-- Gauges----------------------------------------------------------------------
metrics.MetricsGaugesTest.logon.sizevalue = 0
17-11-23 15:37:32=============================================================
-- Gauges----------------------------------------------------------------------
metrics.MetricsGaugesTest.logon.sizevalue = 0
17-11-23 15:37:33 =============================================================
-- Gauges----------------------------------------------------------------------
metrics.MetricsGaugesTest.logon.sizevalue = 1
复制代码
3.1 MetricRegistry 层级结构
Metrics 监控的工具继承 Metric 接口,MetricRegistry 通过 ConcurrentMap 来管理监控工具的注册,注册时需要提供唯一限定名称和具体工具,限定名称可以使用 MetricRegistry 的静态方法 name 来生成,下面是 MetricRegistry 类重要的注册方法:
public <Textends Metric> Tregister(String name, T metric) throws IllegalArgumentException {
if (metric instanceof MetricSet) {
registerAll(name, (MetricSet) metric);}
else {
final Metric existing = metrics.putIfAbsent(name, metric);
if (existing == null) {
onMetricAdded(name, metric);
} else {
throw new IllegalArgumentException("A metric named " + name +" already exists");}}
return metric;
}
private void registerAll(String prefix,MetricSet metrics) throws IllegalArgumentException {
for (Map.Entry<String, Metric> entry :metrics.getMetrics().entrySet()) {
if (entry.getValue() instanceof MetricSet) {
registerAll(name(prefix, entry.getKey()), (MetricSet) entry.getValue());
} else {
register(name(prefix, entry.getKey()), entry.getValue());
}
}
}
复制代码
3.2 ConsoleReporter 层级结构
Metrics 需要指定监控的输出方式,本程序中使用了最简单的 ConsoleReporter,初始化 ConsoleReporter 需要设置统计的周期,这里设置的是 1s。
Reporter 最核心的是 ScheduledReporter 抽象基类,在这个抽象基类中定义了子类的构造方式,定义了通用的方法:开始统计、结束统计和根据不同工具产生的结果输出。
Reporter 之所以可以周期性的输出,实际上底层用到的就是 ScheduledExecutorService 类,通过 Executors.newSingleThreadScheduledExecutor 进行创建。
开始统计时,使用了 ScheduledExecutorService 的 scheduleAtFixedRate 方法,scheduleAtFixedRate 方法可以有效的保证方法执行的周期,上次运行不会影响后续的运行,即:scheduledExecutionTime(n)=firstExecuteTime+n*periodTime,计算方式永远不变。
最后让我们来看看 ConsoleReporter 是如何来实现控制台的输出,这里我们只需要关注 report 和 printGauge 即可,里面的 output 输出流实际上就是 System.out。
@Override
public void report(SortedMap<String,Gauge> gauges,SortedMap<String,Counter> counters,SortedMap<String,Histogram> histograms,SortedMap<String,Meter> meters,SortedMap<String,Timer> timers) {
final String dateTime = dateFormat.format(new Date(clock.getTime()));
printWithBanner(dateTime, '=');
output.println();
if (!gauges.isEmpty()) {
printWithBanner("-- Gauges", '-');
for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
output.println(entry.getKey());
printGauge(entry);}
output.println();
}
...}
private voidprintGauge(Map.Entry<String, Gauge> entry) {
output.printf(locale, "value = %s%n", entry.getValue().getValue());
}
复制代码
3.3 Gauge 层级结构
Metrics 没有提供任何关于 Gauge 接口的实现,使用时我们需要自己实现,这里面需要注意的是变量可见性的问题,防止变量值的脏读。
4. 结束语
至此已经完成了 Gauge 的讲解,如果你正在思考如何保证服务的健壮,Metrics 是很好的选择。 下一章会介绍 Counters 的使用,文章中有任何纰漏的地方欢迎给予指出。