我们知道SpringBoot actuator是有一块内容是关于metrics的,可以很容易和prometheus等监控体系进行集成,但是目前还没有接入相关的监控体系,想着能不能定期输出到日志文件中。
方案设计
将监控指标输出到日志,是否有现成方案?
micrometer-core在1.10+是有引入LoggingMeterRegistry
来定期输出日志的,但是看了一下具体的输出内容,它是按一个指标一行来输出的,一次输出的行记录比较多,但我们更希望分门别类,每一类一行输出。所以,还是得自己定制一下输出。
基于actuator的metrics,还是从头构建?
从头构建,优点就是可以与框架无关,可以应用于非springboot的一些老工程。但考虑到:
-
老工程已经有相应的监控体系和性能信息,目前可以不考虑;
-
后续接入prometheus监控,包括新增自定义指标都可以直接切换过去;
所以,选择基于actuator的metrics来开发,但是不开放metrics访问端点。
数据和日志格式是怎样的?
打算输出的指标内容,分为三个部分:
-
应用通用的,包括线程、类加载、堆内存、非堆内存、永久区(元空间)内存、GC、CPU、文件句柄。
-
请求相关的,包括请求、会话、Tomcat线程池。
-
数据库相关的,如果有多个数据源,则分多行输出。
当前每间隔10秒输出一次,按每次1k计算,一天大约8.5M,一个月大约250M。所以,建议按250M大小,保留一个备份。
指标来源
上述的指标,经过对比基本都是内置的,但有几点需要注意:
-
对于数据源的话,默认的Hikari是有实现的,其他的数据源例如Druid就得实现
DataSourcePoolMetadataProvider
接口或者自己定义binder,否则为空。 -
SpringBoot 2.2.x+默认禁用JMX Tomcat Bean暴露,需要通过参数
server.tomcat.mbeanregistry.enabled=false
重置为默认打开。 -
不少指标是来源于micrometer-core的。例如tomcat的部分指标在micrometer-core较新版本才出现,当然加上也不会出现错误,只是没有数值。例如Spring Boot2.3.x对应的是1.5.14,而最新版本已经是1.9.x了,当前就发现
tomcat.connections.config.max
、tomcat.connections.current
这两个指标需要新版本才有。
源码流程简单分析
mertics入口在哪?
根据spring.factories
可以找到一堆metrics相关的配置:
...
org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.export.appoptics.AppOpticsMetricsExportAutoConfiguration,\
...
org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\
...
主要分类几类:
-
入口 MetricsAutoConfiguration、MetricsEndpointAutoConfiguration
-
暴露metrics org.springframework.boot.actuate.autoconfigure.metrics.export
-
web相关的metrics org.springframework.boot.actuate.autoconfigure.metrics.web
-
其他的,包括JVM/System/Log/Cache等等
MetricsAutoConfiguration
应该是核心入口,在这里会初始化一个MeterRegistryPostProcessor,这是一个BeanPostProcessor类型的对象,这里的逻辑很好理解,就是把MeterRegistry类型的Bean注册到MeterRegistryConfigurer去。
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof MeterRegistry) {
getConfigurer().configure((MeterRegistry) bean);
}
return bean;
}
MeterRegistry
默认是一个SimpleMeterRegistry,从SimpleMetricsExportAutoConfiguration出来的。从这里看结构分析,MeterRegistry是可以存在多个的,在export包里边定义。
private void addBinders(MeterRegistry registry) {
this.binders.orderedStream().forEach((binder) -> binder.bindTo(registry));
}
在上面的configure方法里边就会调用addBinders,它的实现就是把具体的mertics注册到registry去。MeterBinder负责输出指标给MeterRegistry(见bindTo方法),这样整个关系也很好理解了,以Jvm GC指标为例。
JvmMetricsAutoConfiguration会生成JvmGcMetrics,而JvmGcMetrics实现了MeterBinder接口,所以会在启动时候,通过调用bindTo方法,把相应的指标获取方式注册到registry去。
mertics数据怎么来的?
public abstract class MeterRegistry {
...
private final List<Consumer<Meter>> meterAddedListeners = new CopyOnWriteArrayList<>();
private final List<Consumer<Meter>> meterRemovedListeners = new CopyOnWriteArrayList<>();
private final More more = new More();
// Even though writes are guarded by meterMapLock, iterators across value space are supported
// Hence, we use CHM to support that iteration without ConcurrentModificationException risk
private final Map<Id, Meter> meterMap = new ConcurrentHashMap<>();
...
}
核心的结构就是meterMap
,里边存放着每一个指标定义。
那数据是怎么来的?以JvmMemoryMetrics为例。
Gauge.builder("jvm.buffer.count", bufferPoolBean, BufferPoolMXBean::getCount)
.tags(tagsWithId)
.description("An estimate of the number of buffers in the pool")
.baseUnit(BaseUnits.BUFFERS)
.register(registry);
很明显就是通过BufferPoolMXBean::getCount这个方法来获取的。大体就可以理解为:
MeterRegistry通过一个map存放所有的指标定义,每个指标定义包括id, 指标来源对象,指标来源方法引用,需要取值的话,就调用一下。内置的这些通用指标,主要来源于内置的MXBean,主要有ClassLoadMXBean、CompilationMXBean、GarbageCollectorMXBean、MemoryManagerMXBean、MemoryMXBean、MemoryPoolMXBean、OperatingSystemMXBean、RuntimeMXBean、ThreadMXBean。具体的话,可以通过查看JvmMetricsAutoConfiguration、Log4J2MetricsAutoConfiguration、CacheMetricsAutoConfiguration、DataSourcePoolMetricsAutoConfiguration来学习。
metrics数据怎么整理?
由于metrics可能是有多个tag的,我们需要的值可能是需要计算出来的。例如某个指标是总数,就需要忽略tag,其实写起来有点麻烦,但是我们可以直接参考MetricsEndpointAutoConfiguration
中用于暴露端点的MetricsEndpoint
的计算逻辑。
@ReadOperation
public MetricResponse metric(@Selector String requiredMetricName, @Nullable List<String> tag) {
// tag的格式是"key1:value1,key2:value2",要转成Tag类型
List<Tag> tags = parseTags(tag);
// 根据metric的名称和tag找到指标定义Meter
Collection<Meter> meters = findFirstMatchingMeters(this.registry, requiredMetricName, tags);
if (meters.isEmpty()) {
return null;
}
// 对指标数据进行统计,对应的key是指标统计,可能有多种,例如最大,最小,总数之类
Map<Statistic, Double> samples = getSamples(meters);
// 找指标里边包含了哪些可用的tag
Map<String, Set<String>> availableTags = getAvailableTags(meters);
tags.forEach((t) -> availableTags.remove(t.getKey()));
Meter.Id meterId = meters.iterator().next().getId();
return new MetricResponse(requiredMetricName, meterId.getDescription(), meterId.getBaseUnit(),
asList(samples, Sample::new), asList(availableTags, AvailableTag::new));
}
参考源码中的实现逻辑,就可以仿照这得到计算指标的逻辑。对于每个指标,主要是确认使用的指标名称是哪个?是否需要采用Tag来具体定位?指标统计是哪种类型?(在io.micrometer.core.instrument.Statistic
定义)
另外,最终的数据都是double类型的,对某些指标展示不太友好需要进行转换:
-
表示数量等整数类型的,需要转换成int或long;
-
表示字节数大小等整数类型的,考虑转换成KB或者MB等单位;