服務監控-友好地整合Metrics到專案中
Metrics的基本介紹可以參考之前的文章:Metrics-服務指標度量。
本文簡單介紹下如何將Metrics監控整合到我們的專案中。
本文所使用的metrics-core為3.1.0版本。
<dependency> <groupId>io.dropwizard.metrics</groupId> <artifactId>metrics-core</artifactId> <version>3.1.0</version> </dependency> 複製程式碼
2、場景
我們的主要的監控需求有以下方面:
- 機器指標
記憶體、執行緒、硬碟、服務GC情況等基本資訊是我們關心的核心指標。我們可以考慮通過 Gauge 指標項把這些機器指標做統一收集。
- 服務介面請求頻率及耗時
請求頻率及耗時是我們服務介面效能的核心指標,我們可以考慮通過 Timer 指標項來採集相關資訊。
- 服務內部基本資料
在某些場景下我們將內部Service統計到的瞬時指標上報,如Web Filter裡面統計當前正在處理的請求數等。我們也可以使用 Gauge 指標項來收集。
3、方案
針對於以上場景,我們雖然可以通過寫程式碼的方式建立和註冊相應的服務指標,可是在使用上卻不太友好。如何更方便靈活地將Metrics指標統計整合到我們的專案中呢?
3.1、MetricSet自動註冊,收集機器指標
- (1) 預先定義好MetricSet;
指標集合MetricSet可參看metrics-jvm庫的MemoryUsageGaugeSet 來定義,MemoryUsageGaugeSet定義了記憶體使用情況的基本指標,如下所示。
/** * A set of gauges for JVM memory usage, including stats on heap vs. non-heap memory, plus * GC-specific memory pools. */ public class MemoryUsageGaugeSet implements MetricSet { private static final Pattern WHITESPACE = Pattern.compile("[\\s]+"); private final MemoryMXBean mxBean; private final List<MemoryPoolMXBean> memoryPools; public MemoryUsageGaugeSet() { this(ManagementFactory.getMemoryMXBean(), ManagementFactory.getMemoryPoolMXBeans()); } public MemoryUsageGaugeSet(MemoryMXBean mxBean, Collection<MemoryPoolMXBean> memoryPools) { this.mxBean = mxBean; this.memoryPools = new ArrayList<MemoryPoolMXBean>(memoryPools); } @Override public Map<String, Metric> getMetrics() { final Map<String, Metric> gauges = new HashMap<String, Metric>(); gauges.put("total.init", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getInit() + mxBean.getNonHeapMemoryUsage().getInit(); } }); gauges.put("total.used", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getUsed() + mxBean.getNonHeapMemoryUsage().getUsed(); } }); gauges.put("total.max", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getMax() + mxBean.getNonHeapMemoryUsage().getMax(); } }); gauges.put("total.committed", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getCommitted() + mxBean.getNonHeapMemoryUsage().getCommitted(); } }); gauges.put("heap.init", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getInit(); } }); gauges.put("heap.used", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getUsed(); } }); gauges.put("heap.max", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getMax(); } }); gauges.put("heap.committed", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getHeapMemoryUsage().getCommitted(); } }); gauges.put("heap.usage", new RatioGauge() { @Override protected Ratio getRatio() { final MemoryUsage usage = mxBean.getHeapMemoryUsage(); return Ratio.of(usage.getUsed(), usage.getMax()); } }); gauges.put("non-heap.init", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getNonHeapMemoryUsage().getInit(); } }); gauges.put("non-heap.used", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getNonHeapMemoryUsage().getUsed(); } }); gauges.put("non-heap.max", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getNonHeapMemoryUsage().getMax(); } }); gauges.put("non-heap.committed", new Gauge<Long>() { @Override public Long getValue() { return mxBean.getNonHeapMemoryUsage().getCommitted(); } }); gauges.put("non-heap.usage", new RatioGauge() { @Override protected Ratio getRatio() { final MemoryUsage usage = mxBean.getNonHeapMemoryUsage(); return Ratio.of(usage.getUsed(), usage.getMax()); } }); for (final MemoryPoolMXBean pool : memoryPools) { gauges.put(name("pools", WHITESPACE.matcher(pool.getName()).replaceAll("-"), "usage"), new RatioGauge() { @Override protected Ratio getRatio() { final long max = pool.getUsage().getMax() == -1 ? pool.getUsage().getCommitted() : pool.getUsage().getMax(); return Ratio.of(pool.getUsage().getUsed(), max); } }); } return Collections.unmodifiableMap(gauges); } } 複製程式碼
- (2) 通過BeanPostProcessor 處理器自動註冊MetricSet物件Bean;
public class UserDefinedMetricBeanPostProcessor implements BeanPostProcessor { private final Logger LOG = LoggerFactory.getLogger(getClass()); private final MetricRegistry metrics = MetricBeans.getRegistry(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof MetricSet) { MetricSet metricSet = (MetricSet) bean; if (!canRegister(beanName)) { return bean; } String metricName; if (isJvmCollector(beanName)) { metricName = Config.getProjectPrefix() + "." + beanName; } else { //根據規則生成Metric的名字 metricName = Util.forMetricBean(bean.getClass(), beanName); } try { metrics.register(metricName, metricSet); LOG.debug("Registered metric named {} in registry. class: {}.", metricName, metricSet); } catch (IllegalArgumentException ex) { LOG.warn("Error injecting metric for field. bean named {}.", metricName, ex); } } return bean; } private boolean isJvmCollector(String beanName) { return beanName.indexOf("jvm") != -1; } private boolean canRegister(String beanName) { return !isJvmCollector(beanName) || Config.canJvmCollectorStart(); } } 複製程式碼
- (3) 在spring xml檔案或通過spring註解定義bean物件;
<!--定義Jvm監控物件--> <bean id="jvm.memory" class="com.codahale.metrics.jvm.MemoryUsageGaugeSet"/> <!--自動新增使用者定義的監控物件Metric--> <bean class="com.test.metrics.collector.UserDefinedMetricBeanPostProcessor"/> 複製程式碼
可以根據需要定製MetricSet集合,實現服務指標的自動註冊及上報。
3.2、結合註解實現成員變數自動註冊
我們可以結合註解實現成員變數的自動註冊。在BeanPostProcessor可以獲取到成員變數的註解,若是我們的目標註解,可以通過反射的方式獲取到變數資訊進行自動註冊。
下面以Gauged註解為例說明,Gauged註解可以讓成員變數自動註冊並上報。
- (1)Gauged註解定義;
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) public @interface Gauged { String name() default ""; } 複製程式碼
- (2) 使用BeanPostProcessor 解析Gauge註解並註冊;
核心程式碼如下所示:
protected void withField(final Object bean, String beanName, Class<?> targetClass, final Field field) { ReflectionUtils.makeAccessible(field); final Gauged annotation = field.getAnnotation(Gauged.class); final String metricName = Util.forGauge(targetClass, field, annotation); metrics.register(metricName, new Gauge<Object>() { @Override public Object getValue() { return ReflectionUtils.getField(field, bean); } }); LOG.debug("Created gauge {} for field {}.{}", metricName, targetClass.getCanonicalName(), field.getName()); } 複製程式碼
- (3) 在spring.xml檔案定義對應BeanPostProcessor 即可使用。
基本使用如下:
@Component public class GaugeUsage { @Gauged(name = "gaugeField") private int gaugedField = 999; } 複製程式碼
3.3、結合註解實現方法切面的攔截統計
基於Spring AOP可以實現介面呼叫的耗時統計。
下面以Timed註解為例,Timed註解可以統計介面方法耗時情況。
- (1) Timed註解定義;
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) public @interface Timed { String name() default ""; } 複製程式碼
- (2) Timed註解切面定義;
@Component @Aspect public class MetricAspect { @Around("@annotation(timed)") public Object processTimerAnnotation(ProceedingJoinPoint joinPoint, Timed timed) throws Throwable { Class clazz = joinPoint.getTarget().getClass(); Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); String metricName = Util.forTimedMethod(clazz, method, timed); Timer timer = MetricBeans.timer(metricName); final Timer.Context context = timer.time(); try { return joinPoint.proceed(); } finally { context.stop(); } } } 複製程式碼
- (3) 在spring.xml檔案定義MetricAspect即可實現帶Timed註解介面的請求頻率和耗時統計。
基本示例如下:
@Component public class TimedUsage { //@Timed註解會讓監控元件建立Timer物件,統計該方法的執行次數和執行時間等指標 @Timed(name = "simple-timed-method") public void timedMethod() { for (int i = 0; i < 1000; i++) { } } } 複製程式碼
4、總結
當前我們主要通過 BenPostProcessor 和 Spring AOP 對類例項進行攔截,從而實現服務指標的自動註冊和收集。