自定義Metrics埋點方案

一、背景

1.1 什麼是埋點?

有別於前端的埋點,我們這裏主要討論的是後端的代碼埋點,埋的是一些預定義或者自定義的關於系統業務、性能方面的Metrics。Metrics,就是度量的意思,主要是爲了給某個系統某個服務做監控、做統計。

1.2 爲什麼需要埋點?

不同的IT系統有各自關注的業務場景和關鍵細節,需要通過埋點來獲知當前系統運行的狀況,埋點數據採集起來也可以用於事後統計分析。以服務註冊中心爲例,當前cache大小、watch數、client數、節點數、有多少獨立IP等等Zabbix對ZooKeeper做的事情,需要通過自定義埋點去統計實現。在壓測時候,想要知道新生代、老生代的內存使用情況,GC時長等也可以通過Spring Boot內置的MicroMeter埋點去觀察。

1.3 Metric的種類

對於Metrics埋點工具的選擇,大公司會有內部的自研框架,開源的也有Coda Hale的DropWizard,Spring Boot的MicroMeter,甚至還有淘寶前端團隊封裝的前端Metrics框架Pandora.js等。

而縱觀以上各種Metrics埋點框架,所提供的Metrics種類主要有以下幾種類型:

  • Counter:累加型度量,對指標的數據進行累加,反映的是數據隨着時間單調遞增的關係,應用接受到的 HTTP 請求的總次數;
  • Gauge:瞬態型度量,表示指標在當前時間點的瞬時情況,反映的是數據隨着時間上下波動的關係,如系統的load,內存使用率,堆信息等;
  • Timer:變化速率度量,表示指標在某個時間段內變化的速率,反映的是數據隨時間的增長快慢關係,如某個接口的耗時;
  • Histogram:數據分佈度量,表示某一些指標在某個時間段內的分佈情況,反映的是數據隨時間的統計學分佈關係,如某段時間內,某個接口的 RT 的最大,最小,平均值,方差,95% 分位數等;

其中最常用的是 Gauge 瞬態值,Counter 累加值,以及Timer計時器,基本上可以覆蓋90% 以上的場景。

1.4 Metric的命名

MetricName 大體分爲兩部分,key 和 tags。
metric naming
key 代表着一個具體的項,比如:register.counter,儘量做到 key 可描述,可擴展。

tags 代表着一個指標的不同分類,它和 key 加起來唯一指定了一個 Metric。tags 是一個對象 {},通過不同的 kv 對來描述詳情,比如區分不同請求的來源,以 HTTP 接口來舉例,{“source”:“shanghai”} 和 {“source”: “hangzhou”} 這樣就是不同的 tags,結合 key,就用來表示不同的 Metric 了。

這樣的好處就是 tags 可以無限擴展,不會影響到 key,同時,在後續的存儲中,同一個 key 可以進行查詢篩選,保證數據一致性和連貫性。

1.5 對於埋點框架的選擇

1.5.1 內部埋點框架

敝司內部有自研的埋點框架,除了提供客戶端埋點的API,還對Metrics的計算做了優化,即先在客戶端進行進程級別的輕量級統計計算,再把計算後的結果輸出到日誌,通過flume agent採集上報到kafka集羣,後端系統可按需使用這些計算結果,再做二次聚合計算然後保存到大數據存儲引擎,這樣便可大大降低後端計算的壓力和資源。

1.5.2 Spring Boot Actuator Metrics

Spring Boot使用的是MicroMeter Metrics,與Spring集成度高,內置了JVM、GC等預定義的埋點,也支持自定義埋點,提供的Metrics API也方便易用,而且可以與Prometheus和Grafana對接,可以很方便地打通數據的採集與展現。

1.5.3 爲什麼需要隔離並使用多套埋點框架?

公司內部的埋點框架足夠強大易用,而且有着Client端預計算的性能加成,爲何我們還需要再用一套外面的埋點框架呢?原因有三:

首先是由於內部埋點框架的體系架構本身比較重的原因(基於日誌的採集上報再做實時和離線的統計運算),開發測試環境的機器資源不足,測試系統響應較慢,而且需要用戶在測試機上自行安裝日誌採集的Agent,再聯繫開發團隊打開測試環境的採集開關。在這樣的情況下,實在難以滿足在開發測試尤其是壓測遇到問題的時候,需要支持快速便利地添加、修改並查看埋點的要求。而Spring Boot Actuator + Prometheus + Grafana這樣開源的埋點展現解決方案目前已經比較成熟,網上資源豐富,可以利用項目組本身的測試資源快速地搭建實施。

還有就是考慮到目前在研發的系統日後假如開源的話,所使用的內部埋點框架必然需要剝離並替換爲開源的組件。

而從系統架構本身的靈活性去考慮,對於埋點框架這種非核心業務功能,應該保留儘可能多的可選項,以便日後在需要的時候能夠很方便地作出必要的變更。譬如說,出現了其他更好的埋點框架被Spring採用了,正如MicroMeter Metrics逐步蠶食Coda Hale DropWizard的地位一樣。

因此,基於以上幾點的考慮,我們採用接口隔離的方式同時使用了兩套埋點框架,在生產環境使用內部埋點框架,在開發測試環境使用MicroMeter Metrics。

二、如何同時使用兩套埋點框架

如何同時使用兩套埋點框架呢?最簡單的做法,當然是不做隔離,同時用兩套框架在代碼裏埋兩遍了。不過這樣做的話,除了代碼重複冗長之外,還把埋點實現強耦合到系統本身的業務層代碼,以後如果需要更新、更換埋點框架,需要修改代碼,都要修改代碼,非常笨拙。

參考接口隔離(ISP)和依賴倒置(DIP)原則,我們採取的做法是根據不同埋點實現框架API的共性抽象出一套埋點接口層(metrics-api),然後針對每個採用的具體框架寫一個Spring Boot Starter,使用Maven引入依賴,通過Spring IOC注入到使用埋點的地方,並利用Spring的@Profile註解根據不同的運行時環境激活對應的埋點實現框架。

2.1 通用埋點接口層

按照網上的經驗,結合我們目前項目中的所需,目前抽取出來的埋點接口層API暫時只包含了Gauge、Counter和Timer三種類型的埋點:

import java.util.SortedMap;
import java.util.concurrent.Callable;

/**
 * 通用埋點接口,爲了隔離不同的metrics埋點實現
 */
public interface MetricsClient {
	/**
	 * 註冊並返回一個MetricCounter對象,累加器。
	 * 針對同種類型的埋點只需調用本方法一次,保存返回的counter重複使用即可。
	 *
	 * @param metricsName 埋點名
	 * @param description 埋點描述
	 * @param tagMap 定義埋點的相關標籤,一般會添加到tsdb中用於區分記錄
	 * @return
	 */
	MetricCounter counter(String metricsName, String description, SortedMap<String, String> tagMap);

	/**
	 * 註冊並返回一個MetricTimer,計時器。用法建議可參考counter。
	 *
	 * @param metricsName 埋點名
	 * @param description 埋點描述
	 * @param tagMap 定義埋點的相關標籤,一般會添加到tsdb中用於區分記錄
	 * @return
	 */
	MetricTimer timer(String metricsName, String description, SortedMap<String, String> tagMap);

	/**
	 * 註冊一個gauge類型的埋點,瞬時值
	 *
	 * @param metricsName 埋點名
	 * @param description 埋點描述
	 * @param tagMap 定義埋點的相關標籤,一般會添加到tsdb中用於區分記錄
	 * @param callable 封裝如何計算值的邏輯閉包
	 */
	void gauge(String metricsName, String description, SortedMap<String, String> tagMap, Callable<Double> callable);
}

其中所返回的MetricCounter提供了類似Redis的incr和incrby的操作:

/**
 * 累加器埋點類型
 */
public interface MetricCounter {
    /**
     * 累加器加1
     */
    void increment();
    /**
     * 累加器加delta
     *
     * @param delta
     */
    void incrementBy(long delta);
}

而Timer計時器接口主要提供了記錄時間的接口:

import java.util.concurrent.TimeUnit;

/**
 * 計時器埋點類型
 */
public interface MetricTimer {
	/**
	 * 記錄消耗的時間(毫秒)
	 *
	 * @param millis
	 */
	void record(long millis);

	/**
	 * 記錄消耗的時間(指定時間單位)
	 *
	 * @param time
	 * @param unit
	 */
	void record(long time, TimeUnit unit);
}

另外,在接口包中還提供了一個默認的實現——DefaultMetricsClient,把gauge和timer數據打印到控制檯,使用AtomicLong實現counter:

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 默認埋點實現,打印數據到控制檯,僅作參考
 */
public class DefaultMetricsClient implements MetricsClient {
	private Timer timer;
	private int period;
	private boolean mute;

	public DefaultMetricsClient() {
		this(10000);
	}

	public DefaultMetricsClient(int period) {
		this(period, false);
	}

	public DefaultMetricsClient(int period, boolean mute) {
		System.out.println("default metrics client created"); // NOSONAR
		timer = new Timer("default_metrics_client_timer", true);
		this.period = period;
		this.mute = mute;
	}

	@Override
	public MetricCounter counter(String metricsName, String description, SortedMap<String, String> tagMap) {
		return new MetricCounter() {
			private AtomicLong c = new AtomicLong(0);
			private String mn = metricsName;
			private String d = description;
			private SortedMap<String, String> t = tagMap;

			@Override
			public void increment() {
				if (!mute) {
					System.out.println(// NOSONAR
							"metric name: " + mn + ", description: " + d + (t != null && !t.isEmpty() ?
									", tags: " + t.toString() :
									"") + ", counter: " + c.incrementAndGet());
				}
			}

			@Override
			public void incrementBy(long delta) {
				if (!mute) {
					System.out.println(// NOSONAR
							"metric name: " + mn + ", description: " + d + (t != null && !t.isEmpty() ?
									", tags: " + t.toString() :
									"") + ", counter: " + c.addAndGet(delta));
				}
			}
		};
	}

	@Override
	public MetricTimer timer(String metricsName, String description, SortedMap<String, String> tagMap) {
		return new MetricTimer() {
			private int limit = 1_000_000;
			private SortedMap<Long, Long> storage = buildStorage();

			private SortedMap<Long, Long> buildStorage() {
				return Collections.synchronizedSortedMap(new TreeMap<>());
			}

			@Override
			public void record(long millis) {
				if (storage.size() >= limit) {
					storage = buildStorage();
				}
				storage.put(System.currentTimeMillis(), millis);
				if (!mute) {
					System.out.println("time consumed: " + millis + " ms."); // NOSONAR
				}
			}

			@Override
			public void record(long time, TimeUnit unit) {
				this.record(TimeUnit.MILLISECONDS.convert(time, unit));
			}
		};
	}

	@Override
	public void gauge(String metricsName, String description, SortedMap<String, String> tagMap,
			Callable<Double> callable) {
		try {
			if (!mute) {
				timer.scheduleAtFixedRate(new TimerTask() {
					@Override
					public void run() {
						try {
							System.out.println(// NOSONAR
									"metric name: " + metricsName + ", description: " + description + (
											tagMap != null && !tagMap.isEmpty() ? ", tags: " + tagMap.toString() : "")
											+ ", value: " + callable.call());
						} catch (Exception e) {
							e.printStackTrace(); // NOSONAR
						}
					}
				}, 0, period);
			}
		} catch (Exception e) {
			e.printStackTrace(); // NOSONAR
		}
	}
}

2.2 Spring Boot Actuator Metrics封裝

正如前面所述,針對Spring的Metrics的封裝,我們提供了一個Spring Boot Starter:metrics-api-spring

SpringMetricsAutoConfiguration是整個starter的入口,@Profile("!prod")指定非prod環境時候激活,@EnableConfigurationProperties(SpringMetricsProperties.class)聲明相關的properties,並創建了SpringMetricsClient和SpringMetricsRegistry兩個bean。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@ConditionalOnClass(MetricsClient.class)
@EnableConfigurationProperties(SpringMetricsProperties.class)
@Profile("!prod")
public class SpringMetricsAutoConfiguration {
    @Autowired
    private SpringMetricsProperties properties;
    @Bean("springMetricsClient")
    @ConditionalOnMissingBean
    public MetricsClient springMetricsClient() {
        return new SpringMetricsClient();
    }
    @Bean
    public SpringMetricsRegistry springMetricsRegister() {
        return new SpringMetricsRegistry();
    }
}

按照Spring Boot Starter標準要求,需要把這個類在META-INF/spring.factories中聲明一下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.abc.metrics.SpringMetricsAutoConfiguration

SpringMetricsRegistry實現了MicroMeter的MeterBinder,在重載的bindTo方法中記下了傳入的MeterRegistry,registerGauge和registerCounter兩個方法創建micrometer中對應的埋點類型並註冊到bindTo時候記住的MeterRegistry中。這樣就不用把每個埋點都聲明爲一個Spring Bean,而用戶也可以在Spring的@PostConstruct的時候去埋點了。另外,由於MicroMeter是通過BeanPostProcessor去Spring容器中檢查有哪些Bean實現了MeterBinder接口然後逐個處理並添加到註冊表,如果在一些生命週期早於BeanPostProcessor的地方埋點的話可能會有空指針問題,這種情況可以使用TimerTask延遲實際埋點時機的方式解決。而我們在SpringMetricsRegistry中如果發現MeterRegistry尚未設置,會拋出IllegalStateException提示用戶。

import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import java.util.List;
import java.util.SortedMap;
import java.util.stream.Collectors;

public class SpringMetricsRegistry implements MeterBinder {
	private static Logger log = LoggerFactory.getLogger(SpringMetricsRegistry.class);

	private MeterRegistry registry;

	@PreDestroy
	public void destroy() {
		if (registry != null && !registry.isClosed()) {
			registry.close();
		}
	}

	@Override
	public void bindTo(MeterRegistry registry) {
		if (this.registry == null) {
			this.registry = registry;
			log.info("MeterRegistry is set...");
		}
	}

	public void registerGauge(SpringMetricGuage g) {
		checkState();
		List<Tag> tags = getTags(g.getTagMap());
		Gauge.builder(g.getMetricsName(), g.getCallable(), SpringMetricGuage.metricFunc).tags(tags)
				.description(g.getDescription()).register(this.registry);
	}

	public Counter registerCounter(String metricsName, String description, SortedMap<String, String> tagMap) {
		checkState();
		List<Tag> tags = getTags(tagMap);
		return Counter.builder(metricsName).tags(tags).description(description).register(this.registry);
	}

	public Timer registerTimer(String metricsName, String description, SortedMap<String, String> tagMap) {
		checkState();
		List<Tag> tags = getTags(tagMap);
		return Timer.builder(metricsName).tags(tags).description(description).register(this.registry);
	}

	private List<Tag> getTags(SortedMap<String, String> tagMap) {
		return tagMap.entrySet().stream().map(entry -> Tag.of(entry.getKey(), entry.getValue()))
				.collect(Collectors.toList());
	}

	private void checkState() {
		if (this.registry == null) {
			throw new IllegalStateException("Metrics registry is not initialized yet!");
		}
	}
}

SpringMetricsClient實現了MetricsClient中的gauge和counter方法,委託給SpringMetricsRegistry執行相應的埋點動作。使用set和map記住已經埋過的點避免重複埋點。SpringMetricsProperties.isEnabled()是一個默認打開的開關,用戶可以選擇在application.yml中關閉,這樣的話就會退化爲使用DefaultMetricsClient埋點(打印到控制檯)。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SpringMetricsClient implements MetricsClient {
	private static Logger log = LoggerFactory.getLogger(SpringMetricsClient.class);
	@Autowired
	private SpringMetricsRegistry register;
	@Autowired
	private SpringMetricsProperties springMetricsProperties;
	private Set<SpringMetricGuage> gauges = ConcurrentHashMap.newKeySet();
	private ConcurrentMap<String, MetricCounter> counters = new ConcurrentHashMap<>();
	private ConcurrentMap<String, MetricTimer> timers = new ConcurrentHashMap<>();
	private MetricsClient defaultClient;

	@PostConstruct
	public void init() {
		if (!springMetricsProperties.isEnabled()) {
			defaultClient = new DefaultMetricsClient(springMetricsProperties.getDefaultClientPeriod(),
					springMetricsProperties.isMuteDefaultClient());
		}
	}

	@Override
	public void gauge(String metricName, String description, SortedMap<String, String> tagMap,
			Callable<Double> callable) {
		SpringMetricGuage g = new SpringMetricGuage(metricName, description, tagMap, callable);
		if (gauges.add(g)) {
			if (springMetricsProperties.isEnabled()) {
				register.registerGauge(g);
				log.info("gauge metric added for: {}", g);
			} else {
				defaultClient.gauge(metricName, description, tagMap, callable);
			}
		} else {
			log.warn("duplicated gauge: {}", g);
		}
	}

	@Override
	public MetricCounter counter(String metricName, String description, SortedMap<String, String> tagMap) {
		MetricCounter c;
		String key = getKey(metricName, tagMap);
		if ((c = counters.get(key)) == null) {
			c = counters.computeIfAbsent(key, k -> getSpringMetricCounter(metricName, description, tagMap));
		}
		return c;
	}

	@Override
	public MetricTimer timer(String metricName, String description, SortedMap<String, String> tagMap) {
		MetricTimer t;
		String key = getKey(metricName, tagMap);
		if ((t = timers.get(key)) == null) {
			t = timers.computeIfAbsent(key, k -> getSpringMetricTimer(metricName, description, tagMap));
		}
		return t;
	}

	private MetricTimer getSpringMetricTimer(String metricName, String description, SortedMap<String, String> tagMap) {
		if (springMetricsProperties.isEnabled()) {
			return new SpringMetricTimer(metricName, tagMap, register.registerTimer(metricName, description, tagMap));
		} else {
			return defaultClient.timer(metricName, description, tagMap);
		}
	}

	private MetricCounter getSpringMetricCounter(String metricName, String description,
			SortedMap<String, String> tagMap) {
		if (springMetricsProperties.isEnabled()) {
			return new SpringMetricCounter(metricName, tagMap,
					register.registerCounter(metricName, description, tagMap));
		} else {
			return defaultClient.counter(metricName, description, tagMap);
		}
	}

	private String getKey(String metricName, SortedMap<String, String> tagMap) {
		return metricName + (tagMap != null ? tagMap.toString() : "");
	}

	public Set<SpringMetricGuage> getGauges() {
		return gauges;
	}

	public Map<String, MetricCounter> getCounters() {
		return counters;
	}
}

SpringMetricGuage、SpringMetricCounter和SpringMetricTimer主要是委託給Micrometer的Gauge實現:

import java.util.Objects;
import java.util.SortedMap;
import java.util.concurrent.Callable;
import java.util.function.ToDoubleFunction;

class SpringMetricGuage {
	public static final ToDoubleFunction<Callable<Double>> metricFunc = doubleCallable -> {
		try {
			return doubleCallable.call();
		} catch (Exception e) {
			e.printStackTrace(); // NOSONAR
			return 0L;
		}
	};
	private String metricsName;
	private String description;
	private SortedMap<String, String> tagMap;
	private Callable<Double> callable;

	SpringMetricGuage(String metricsName, String description, SortedMap<String, String> tagMap,
			Callable<Double> callable) {
		this.metricsName = metricsName;
		this.description = description;
		this.tagMap = tagMap;
		this.callable = callable;
	}

	public String getMetricsName() {
		return metricsName;
	}

	public void setMetricsName(String metricsName) {
		this.metricsName = metricsName;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public SortedMap<String, String> getTagMap() {
		return tagMap;
	}

	public void setTagMap(SortedMap<String, String> tagMap) {
		this.tagMap = tagMap;
	}

	public Callable<Double> getCallable() {
		return callable;
	}

	public void setCallable(Callable<Double> callable) {
		this.callable = callable;
	}

	@Override
	public String toString() {
		return "SpringMetricGuage{" + "metricsName='" + metricsName + '\'' + ", description='" + description + '\''
				+ ", tagMap=" + (tagMap != null && !tagMap.isEmpty() ? tagMap.toString() : "{}") + '}';
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		SpringMetricGuage myGuage = (SpringMetricGuage) o;
		return Objects.equals(metricsName, myGuage.metricsName) && Objects.equals(description, myGuage.description)
				&& Objects.equals(tagMap, myGuage.tagMap);
	}

	@Override
	public int hashCode() {
		return Objects.hash(metricsName, description, tagMap);
	}
}
import io.micrometer.core.instrument.Counter;

import java.util.Objects;
import java.util.SortedMap;

public class SpringMetricCounter implements MetricCounter {
	private String metricName;
	private SortedMap<String, String> tagMap;
	private Counter counter;

	SpringMetricCounter(String metricName, SortedMap<String, String> tagMap, Counter counter) {
		this.metricName = metricName;
		this.tagMap = tagMap;
		this.counter = counter;
	}

	@Override
	public void increment() {
		this.counter.increment();
	}

	@Override
	public void incrementBy(long delta) {
		this.counter.increment(delta);
	}

	public String getMetricName() {
		return metricName;
	}

	public void setMetricName(String metricName) {
		this.metricName = metricName;
	}

	public SortedMap<String, String> getTagMap() {
		return tagMap;
	}

	public void setTagMap(SortedMap<String, String> tagMap) {
		this.tagMap = tagMap;
	}

	public Counter getCounter() {
		return counter;
	}

	public void setCounter(Counter counter) {
		this.counter = counter;
	}

	@Override
	public int hashCode() {
		return Objects.hash(metricName, tagMap);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		SpringMetricCounter that = (SpringMetricCounter) o;
		return Objects.equals(metricName, that.metricName) && Objects.equals(tagMap, that.tagMap);
	}

	@Override
	public String toString() {
		return "SpringMetricCounter{" + "metricName='" + metricName + '\'' + ", tagMap=" + (
				tagMap != null && !tagMap.isEmpty() ? tagMap.toString() : "{}") + ", counter=" + counter.count() + '}';
	}
}
import io.micrometer.core.instrument.Timer;

import java.util.Objects;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

public class SpringMetricTimer implements MetricTimer {
	private String metricName;
	private SortedMap<String, String> tagMap;
	private Timer timer;

	public SpringMetricTimer(String metricName, SortedMap<String, String> tagMap, Timer timer) {
		this.metricName = metricName;
		this.tagMap = tagMap;
		this.timer = timer;
	}

	public String getMetricName() {
		return metricName;
	}

	public void setMetricName(String metricName) {
		this.metricName = metricName;
	}

	public SortedMap<String, String> getTagMap() {
		return tagMap;
	}

	public void setTagMap(SortedMap<String, String> tagMap) {
		this.tagMap = tagMap;
	}

	public Timer getTimer() {
		return timer;
	}

	public void setTimer(Timer timer) {
		this.timer = timer;
	}

	@Override
	public void record(long millis) {
		this.record(millis, TimeUnit.MILLISECONDS);
	}

	@Override
	public void record(long time, TimeUnit unit) {
		timer.record(time, unit);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		SpringMetricTimer that = (SpringMetricTimer) o;
		return Objects.equals(metricName, that.metricName) && Objects.equals(tagMap, that.tagMap);
	}

	@Override
	public int hashCode() {
		return Objects.hash(metricName, tagMap);
	}

	@Override
	public String toString() {
		return "SpringMetricTimer{" + "metricName='" + metricName + '\'' + ", tagMap=" + (
				tagMap != null && !tagMap.isEmpty() ? tagMap.toString() : "{}") + '}';
	}
}

2.3 內部埋點框架的封裝

公司內部的埋點框架封裝與Spring的類似,其中的MetricsAutoConfiguration聲明瞭@Profile(“prod”),即生產環境啓用。其他細節由於對外部用戶參考意義不大,這裏就不表了。

2.4 使用說明

上面介紹了這麼多,我們看下在系統中具體是如何使用的。

首先在pom.xml中引入依賴:

<dependency>
    <groupId>com.abc</groupId>
    <artifactId>metrics-api</artifactId>
    <version>${project.parent.version}</version>
</dependency>
<dependency>
    <groupId>com.abc</groupId>
    <artifactId>metrics-api-spring</artifactId>
    <version>${project.parent.version}</version>
</dependency>

Gauge類型的埋點:

	@PostConstruct
	public void init() {
		metricsClient
				.gauge(CONFIG_CACHE_SIZE, "cached history configs of config server", tags, new Callable<Double>() {
					@Override
					public Double call() throws Exception {
						return (double) configHistoryService.getCacheSize();
					}
				});
		logger.info("metric added: {}", CONFIG_CACHE_SIZE);
		// ...
	}

Counter類型和Timer類型的埋點可以用AOP的方式去進行埋點:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class MetricsAspect {
	private static final Logger log = LoggerFactory.getLogger(MetricsAspect.class);
	@Autowired
	private MetricsClient metricsClient;
	private volatile MetricTimer publishTimer;
	private volatile MetricCounter publishCounter;

	@Around("execution(* com.abc.PublishService.doXXXPublish(..))")
	public void logAround(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.currentTimeMillis();
		joinPoint.proceed();
		getPublishCounter().increment();
		getPublishTimer().record(System.currentTimeMillis() - start);
	}
	private MetricCounter getPublishCounter() {
		if (this.publishCounter == null) {
			synchronized (this) {
				if (this.publishCounter == null) {
					this.publishCounter = metricsClient
							.counter(MetricsService.CONFIG_PUBLISH_COUNTER, "full publish counter", new TreeMap<>());
				}
			}
		}
		return this.publishCounter;
	}	
	private MetricTimer getPublishTimer() {
		if (this.publishTimer == null) {
			synchronized (this) {
				if (this.publishTimer == null) {
					this.publishTimer = metricsClient
							.timer(MetricsService.CONFIG_PUBLISH_TIMER, "full publish timer", new TreeMap<>());
				}
			}
		}
		return this.publishTimer;
	}
}

三、總結與反思

在本文中,我們介紹瞭如何同時隔離並使用不同的埋點框架。主要是通過抽取一個抽象的埋點接入層,封裝了目前系統中常用的Gauge、Counter和Timer埋點類型,隔離了具體的埋點框架實現;再通過不同的Spring Boot Starter引入依賴,提供配置,根據運行時Profile激活對應的埋點框架。至於Spring Metrics的埋點如何採集展現屬於另外一個話題了,感興趣的用戶可以留意小弟後續的博客文章。

使用Spring Metrics埋點的時候,由於MicroMeter是通過BeanPostProcessor掃描context中實現了MeterBinder的Bean然後放入其自身的regitry,所以如果在生命週期早於其執行的地方埋點的話會遇到空指針的問題,這種時候可以考慮使用TimerTask延遲註冊時機解決。在JUnit中也可能會遇到類似問題,可以考慮設置enabled屬性爲false或者mock掉埋點client。我們也會考慮是否有必要在後續的開發迭代中在client進行容錯處理,如果registry尚未被注入就被用戶用作埋點的話就把用戶埋點的Metric放入延遲隊列中,初始化完成後再從隊列中取出並執行埋點,這樣就不需要在用戶代碼中去糾結這個埋點時機的問題。

四、參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章