這世上有三樣東西是別人搶不走的:一是吃進胃裏的食物,二是藏在心中的夢想,三是讀進大腦的書java
spring-actuator
作度量統計收集,使用Prometheus(普羅米修斯)進行數據收集,Grafana(加強ui)進行數據展現,用於監控生成環境機器的性能指標和業務數據指標。通常,咱們叫這樣的操做爲」埋點」。SpringBoot中的依賴spring-actuator
中集成的度量統計API使用的框架是Micrometer,官網是Micrometer.io。在實踐中發現了業務開發者濫用了Micrometer的度量類型Counter
,致使不管什麼狀況下都只使用計數統計的功能。這篇文章就是基於Micrometer分析其餘的度量類型API的做用和適用場景。linux
Meter
是指一組用於收集應用中的度量數據的接口,Meter單詞能夠翻譯爲」米」或者」千分尺」,可是顯然聽起來都不是很合理,所以下文直接叫Meter,理解它爲度量接口便可。Meter
是由MeterRegistry
建立和保存的,能夠理解MeterRegistry
是Meter
的工廠和緩存中心,通常而言每一個JVM應用在使用Micrometer的時候必須建立一個MeterRegistry
的具體實現。Micrometer中,Meter
的具體類型包括:Timer
,Counter
,Gauge
,DistributionSummary
,LongTaskTimer
,FunctionCounter
,FunctionTimer
和TimeGauge
。下面分節詳細介紹這些類型的使用方法和實戰使用場景。而一個Meter
具體類型須要經過名字和Tag
(這裏指的是Micrometer提供的Tag接口)做爲它的惟一標識,這樣作的好處是可使用名字進行標記,經過不一樣的Tag
去區分多種維度進行數據統計。git
MeterRegistry在Micrometer是一個抽象類,主要實現包括:github
io.micrometer.core.instrument.Metrics
中持有一個靜態final的CompositeMeterRegistry
實例globalRegistry。固然,使用者也能夠自行繼承MeterRegistry去實現自定義的MeterRegistry。SimpleMeterRegistry
適合作調試的時候使用,它的簡單使用方式以下:web
MeterRegistry registry = new SimpleMeterRegistry(); Counter counter = registry.counter("counter"); counter.increment();
CompositeMeterRegistry
實例初始化的時候,內部持有的MeterRegistry
列表是空的,若是此時用它新增一個Meter實例,Meter實例的操做是無效的spring
CompositeMeterRegistry composite = new CompositeMeterRegistry(); Counter compositeCounter = composite.counter("counter"); compositeCounter.increment(); // <- 實際上這一步操做是無效的,可是不會報錯 SimpleMeterRegistry simple = new SimpleMeterRegistry(); composite.add(simple); // <- 向CompositeMeterRegistry實例中添加SimpleMeterRegistry實例 compositeCounter.increment(); // <-計數成功
全局的MeterRegistry的使用方式更加簡單便捷,由於一切只須要操做工廠類Metrics的靜態方法:數據庫
Metrics.addRegistry(new SimpleMeterRegistry()); Counter counter = Metrics.counter("counter", "tag-1", "tag-2"); counter.increment();
Micrometer中,Meter的命名約定使用英文逗號(dot,也就是」.」)分隔單詞。可是對於不一樣的監控系統,對命名的規約可能並不相同,若是命名規約不一致,在作監控系統遷移或者切換的時候,可能會對新的系統形成破壞。Micrometer中使用英文逗號分隔單詞的命名規則,再經過底層的命名轉換接口NamingConvention
進行轉換,最終能夠適配不一樣的監控系統,同時能夠消除監控系統不容許的特殊字符的名稱和標記等。開發者也能夠覆蓋NamingConvention
實現自定義的命名轉換規則:registry.config().namingConvention(myCustomNamingConvention);
。在Micrometer中,對一些主流的監控系統或者存儲系統的命名規則提供了默認的轉換方式,例如當咱們使用下面的命名時候:api
MeterRegistry registry = ... registry.timer("http.server.requests");
對於不一樣的監控系統或者存儲系統,命名會自動轉換以下:緩存
其實NamingConvention已經提供了5種默認的轉換規則:dot、snakeCase、camelCase、upperCamelCase和slashes。服務器
另外,Tag(標籤)是Micrometer的一個重要的功能,嚴格來講,一個度量框架只有實現了標籤的功能,才能真正地多維度進行度量數據收集。Tag的命名通常須要是有意義的,所謂有意義就是能夠根據Tag的命名能夠推斷出它指向的數據到底表明什麼維度或者什麼類型的度量指標。假設咱們須要監控數據庫的調用和Http請求調用統計,通常推薦的作法是:
MeterRegistry registry = ... registry.counter("database.calls", "db", "users") registry.counter("http.requests", "uri", "/api/users")
這樣,當咱們選擇命名爲」database.calls」的計數器,咱們能夠進一步選擇分組」db」或者」users」分別統計不一樣分組對總調用數的貢獻或者組成。一個反例以下:
MeterRegistry registry = ... registry.counter("calls", "class", "database", "db", "users"); registry.counter("calls", "class", "http", "uri", "/api/users");
經過命名」calls」獲得的計數器,因爲標籤混亂,數據是基本沒法分組統計分析,這個時候能夠認爲獲得的時間序列的統計數據是沒有意義的。能夠定義全局的Tag,也就是全局的Tag定義以後,會附加到全部的使用到的Meter上(只要是使用同一MeterRegistry),全局的Tag能夠這樣定義:
MeterRegistry registry = ... registry.counter("calls", "class", "database", "db", "users"); registry.counter("calls", "class", "http", "uri", "/api/users"); MeterRegistry registry = ... registry.config().commonTags("stack", "prod", "region", "us-east-1"); // 和上面的意義是同樣的 registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));
像上面這樣子使用,就能經過主機,實例,區域,堆棧等操做環境進行多維度深刻分析。
還有兩點點須要注意:
io.micrometer.core.instrument.Tag
接口:public interface Tag extends Comparable<Tag> { String getKey(); String getValue(); static Tag of(String key, String value) { return new ImmutableTag(key, value); } default int compareTo(Tag o) { return this.getKey().compareTo(o.getKey()); } }
固然,有些時候,咱們須要過濾一些必要的標籤或者名稱進行統計,或者爲Meter的名稱添加白名單,這個時候可使用MeterFilter。MeterFilter自己提供一些列的靜態方法,多個MeterFilter能夠疊加或者組成鏈實現用戶最終的過濾策略。例如:
MeterRegistry registry = ... registry.config() .meterFilter(MeterFilter.ignoreTags("http")) .meterFilter(MeterFilter.denyNameStartsWith("jvm"));
表示忽略」http」標籤,拒絕名稱以」jvm」字符串開頭的Meter。更多用法能夠參詳一下MeterFilter
這個類。
Meter的命名和Meter的Tag相互結合,以命名爲軸心,以Tag爲多維度要素,可使度量數據的維度更加豐富,便於統計和分析。
前面提到Meter主要包括:Timer
,Counter
,Gauge
,DistributionSummary
,LongTaskTimer
,FunctionCounter
,FunctionTimer
和TimeGauge
。下面逐一分析它們的做用和我的理解的實際使用場景(應該說是生產環境)。
Counter
是一種比較簡單的Meter,它是一種單值的度量類型,或者說是一個單值計數器。Counter
接口容許使用者使用一個固定值(必須爲正數)進行計數。準確來講:Counter
就是一個增量爲正數的單值計數器。這個舉個很簡單的使用例子:
MeterRegistry meterRegistry = new SimpleMeterRegistry(); Counter counter = meterRegistry.counter("http.request", "createOrder", "/order/create"); counter.increment(); System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=1.0}]
使用場景:
Counter
的做用是記錄XXX的總量或者計數值,適用於一些增加類型的統計,例以下單、支付次數、Http請求總量記錄等等,經過Tag能夠區分不一樣的場景,對於下單,可使用不一樣的Tag標記不一樣的業務來源或者是按日期劃分,對於Http請求總量記錄,可使用Tag區分不一樣的URL。用下單業務舉個例子:
//實體 @Data public class Order { private String orderId; private Integer amount; private String channel; private LocalDateTime createTime; } public class CounterMain { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); static { Metrics.addRegistry(new SimpleMeterRegistry()); } public static void main(String[] args) throws Exception { Order order1 = new Order(); order1.setOrderId("ORDER_ID_1"); order1.setAmount(100); order1.setChannel("CHANNEL_A"); order1.setCreateTime(LocalDateTime.now()); createOrder(order1); Order order2 = new Order(); order2.setOrderId("ORDER_ID_2"); order2.setAmount(200); order2.setChannel("CHANNEL_B"); order2.setCreateTime(LocalDateTime.now()); createOrder(order2); Search.in(Metrics.globalRegistry).meters().forEach(each -> { StringBuilder builder = new StringBuilder(); builder.append("name:") .append(each.getId().getName()) .append(",tags:") .append(each.getId().getTags()) .append(",type:").append(each.getId().getType()) .append(",value:").append(each.measure()); System.out.println(builder.toString()); }); } private static void createOrder(Order order) { //忽略訂單入庫等操做 Metrics.counter("order.create", "channel", order.getChannel(), "createTime", FORMATTER.format(order.getCreateTime())).increment(); } }
控制檯輸出
name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}] name:order.create,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
上面的例子是使用全局靜態方法工廠類Metrics去構造Counter實例,實際上,io.micrometer.core.instrument.Counter接口提供了一個內部建造器類Counter.Builder去實例化Counter,Counter.Builder的使用方式以下:
public class CounterBuilderMain { public static void main(String[] args) throws Exception{ Counter counter = Counter.builder("name") //名稱 .baseUnit("unit") //基礎單位 .description("desc") //描述 .tag("tagKey", "tagValue") //標籤 .register(new SimpleMeterRegistry());//綁定的MeterRegistry counter.increment(); } }
FunctionCounter
是Counter
的特化類型,它把計數器數值增長的動做抽象成接口類型ToDoubleFunction
,這個接口JDK1.8中對於Function
的特化類型接口。FunctionCounter
的使用場景和Counter
是一致的,這裏介紹一下它的用法:
public class FunctionCounterMain { public static void main(String[] args) throws Exception { MeterRegistry registry = new SimpleMeterRegistry(); AtomicInteger n = new AtomicInteger(0); //這裏ToDoubleFunction匿名實現其實可使用Lambda表達式簡化爲AtomicInteger::get FunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() { @Override public double applyAsDouble(AtomicInteger value) { return value.get(); } }).baseUnit("function") .description("functionCounter") .tag("createOrder", "CHANNEL-A") .register(registry); //下面模擬三次計數 n.incrementAndGet(); n.incrementAndGet(); n.incrementAndGet(); } }
FunctionCounter
使用的一個明顯的好處是,咱們不須要感知FunctionCounter實例的存在,實際上咱們只須要操做做爲FunctionCounter實例構建元素之一的AtomicInteger實例便可,這種接口的設計方式在不少框架裏面均可以看到。
Timer(計時器)適用於記錄耗時比較短的事件的執行時間,經過時間分佈展現事件的序列和發生頻率。全部的Timer的實現至少記錄了發生的事件的數量和這些事件的總耗時,從而生成一個時間序列。Timer的基本單位基於服務端的指標而定,可是實際上咱們不須要過於關注Timer的基本單位,由於Micrometer在存儲生成的時間序列的時候會自動選擇適當的基本單位。Timer接口提供的經常使用方法以下:
public interface Timer extends Meter { ... void record(long var1, TimeUnit var3); default void record(Duration duration) { this.record(duration.toNanos(), TimeUnit.NANOSECONDS); } <T> T record(Supplier<T> var1); <T> T recordCallable(Callable<T> var1) throws Exception; void record(Runnable var1); default Runnable wrap(Runnable f) { return () -> { this.record(f); }; } default <T> Callable<T> wrap(Callable<T> f) { return () -> { return this.recordCallable(f); }; } long count(); double totalTime(TimeUnit var1); default double mean(TimeUnit unit) { return this.count() == 0L ? 0.0D : this.totalTime(unit) / (double)this.count(); } double max(TimeUnit var1); ... }
實際上,比較經常使用和方便的方法是幾個函數式接口入參的方法:
Timer timer = ... timer.record(() -> dontCareAboutReturnValue()); timer.recordCallable(() -> returnValue()); Runnable r = timer.wrap(() -> dontCareAboutReturnValue()); Callable c = timer.wrap(() -> returnValue());
使用場景:
根據我的經驗和實踐,總結以下:
這裏舉個實際的例子,要對系統作一個功能,記錄指定方法的執行時間,仍是用下單方法作例子:
public class TimerMain { private static final Random R = new Random(); static { Metrics.addRegistry(new SimpleMeterRegistry()); } public static void main(String[] args) throws Exception { Order order1 = new Order(); order1.setOrderId("ORDER_ID_1"); order1.setAmount(100); order1.setChannel("CHANNEL_A"); order1.setCreateTime(LocalDateTime.now()); Timer timer = Metrics.timer("timer", "createOrder", "cost"); timer.record(() -> createOrder(order1)); } private static void createOrder(Order order) { try { TimeUnit.SECONDS.sleep(R.nextInt(5)); //模擬方法耗時 } catch (InterruptedException e) { //no-op } } }
在實際生產環境中,能夠經過spring-aop把記錄方法耗時的邏輯抽象到一個切面中,這樣就能減小沒必要要的冗餘的模板代碼。上面的例子是經過Mertics構造Timer實例,實際上也可使用Builder構造:
MeterRegistry registry = ... Timer timer = Timer .builder("my.timer") .description("a description of what this timer does") // 可選 .tags("region", "test") // 可選 .register(registry);
另外,Timer的使用還能夠基於它的內部類Timer.Sample,經過start和stop兩個方法記錄二者之間的邏輯的執行耗時。例如:
Timer.Sample sample = Timer.start(registry); // 這裏作業務邏輯 Response response = ... sample.stop(registry.timer("my.timer", "response", response.status()));
FunctionTimer是Timer的特化類型,它主要提供兩個單調遞增的函數(其實並非單調遞增,只是在使用中通常須要隨着時間最少保持不變或者說不減小):一個用於計數的函數和一個用於記錄總調用耗時的函數,它的建造器的入參以下:
public interface FunctionTimer extends Meter { static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction, ToDoubleFunction<T> totalTimeFunction, TimeUnit totalTimeFunctionUnit) { return new Builder<>(name, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit); } ... }
官方文檔中的例子以下:
IMap<?, ?> cache = ...; // 假設使用了Hazelcast緩存 registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache, c -> c.getLocalMapStats().getGetOperationCount(), //實際上就是cache的一個方法,記錄緩存生命週期初始化的增量(個數) c -> c.getLocalMapStats().getTotalGetLatency(), // Get操做的延遲時間總量,能夠理解爲耗時 TimeUnit.NANOSECONDS );
按照我的理解,ToDoubleFunction用於統計事件個數,ToDoubleFunction用於記錄執行總時間,實際上兩個函數都只是Function函數的變體,還有一個比較重要的是總時間的單位totalTimeFunctionUnit。簡單的使用方式以下:
public class FunctionTimerMain { public static void main(String[] args) throws Exception { //這個是爲了知足參數,暫時不須要理會 Object holder = new Object(); AtomicLong totalTimeNanos = new AtomicLong(0); AtomicLong totalCount = new AtomicLong(0); FunctionTimer.builder("functionTimer", holder, p -> totalCount.get(), p -> totalTimeNanos.get(), TimeUnit.NANOSECONDS) .register(new SimpleMeterRegistry()); totalTimeNanos.addAndGet(10000000); totalCount.incrementAndGet(); } }
LongTaskTimer也是一種Timer的特化類型,主要用於記錄長時間執行的任務的持續時間,在任務完成以前,被監測的事件或者任務仍然處於運行狀態,任務完成的時候,任務執行的總耗時纔會被記錄下來。LongTaskTimer適合用於長時間持續運行的事件耗時的記錄,例如相對耗時的定時任務。在Spring應用中,能夠簡單地使用@Scheduled和@Timed註解,基於spring-aop完成定時調度任務的總耗時記錄:
@Timed(value = "aws.scrape", longTask = true) @Scheduled(fixedDelay = 360000) void scrapeResources() { //這裏作相對耗時的業務邏輯 }
固然,在非spring體系中也能方便地使用LongTaskTimer:
public class LongTaskTimerMain { public static void main(String[] args) throws Exception{ MeterRegistry meterRegistry = new SimpleMeterRegistry(); LongTaskTimer longTaskTimer = meterRegistry.more().longTaskTimer("longTaskTimer"); longTaskTimer.record(() -> { //這裏編寫Task的邏輯 }); //或者這樣 Metrics.more().longTaskTimer("longTaskTimer").record(()-> { //這裏編寫Task的邏輯 }); } }
Gauge(儀表)是獲取當前度量記錄值的句柄,也就是它表示一個能夠任意上下浮動的單數值度量Meter。Gauge一般用於變更的測量值,測量值用ToDoubleFunction
參數的返回值設置,如當前的內存使用狀況,同時也能夠測量上下移動的」計數」,好比隊列中的消息數量。官網文檔中提到Gauge的典型使用場景是用於測量集合或映射的大小或運行狀態中的線程數。Gauge通常用於監測有天然上界的事件或者任務,而Counter通常使用於無天然上界的事件或者任務的監測,因此像Http請求總量計數應該使用Counter而非Gauge。MeterRegistry
中提供了一些便於構建用於觀察數值、函數、集合和映射的Gauge相關的方法:
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>()); Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());
上面的三個方法經過MeterRegistry
構建Gauge而且返回了集合或者映射實例,使用這些集合或者映射實例就能在其size變化過程當中記錄這個變動值。更重要的優勢是,咱們不須要感知Gauge接口的存在,只須要像平時同樣使用集合或者映射實例就能夠了。此外,Gauge還支持java.lang.Number
的子類,java.util.concurrent.atomic
包中的AtomicInteger
和AtomicLong
,還有Guava提供的AtomicDouble
:
AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0)); n.set(1); n.set(2);
除了使用MeterRegistry建立Gauge以外,還可使用建造器流式建立:
//通常咱們不須要操做Gauge實例 Gauge gauge = Gauge .builder("gauge", myObj, myObj::gaugeValue) .description("a description of what this gauge does") // 可選 .tags("region", "test") // 可選 .register(registry);
使用場景:
根據我的經驗和實踐,總結以下:
舉個相對實際的例子,假設咱們須要對登陸後的用戶發送一條短信或者推送,作法是消息先投放到一個阻塞隊列,再由一個線程消費消息進行其餘操做:
public class GaugeMain { private static final MeterRegistry MR = new SimpleMeterRegistry(); private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500); private static BlockingQueue<Message> REAL_QUEUE; static { REAL_QUEUE = MR.gauge("messageGauge", QUEUE, Collection::size); } public static void main(String[] args) throws Exception { consume(); Message message = new Message(); message.setUserId(1L); message.setContent("content"); REAL_QUEUE.put(message); } private static void consume() throws Exception { new Thread(() -> { while (true) { try { Message message = REAL_QUEUE.take(); //handle message System.out.println(message); } catch (InterruptedException e) { //no-op } } }).start(); } }
上面的例子代碼寫得比較糟糕,只爲了演示相關使用方式,切勿用於生產環境。
TimeGauge是Gauge的特化類型,相比Gauge,它的構建器中多了一個TimeUnit類型的參數,用於指定ToDoubleFunction
入參的基礎時間單位。這裏簡單舉個使用例子:
public class TimeGaugeMain { private static final SimpleMeterRegistry R = new SimpleMeterRegistry(); public static void main(String[] args) throws Exception{ AtomicInteger count = new AtomicInteger(); TimeGauge.Builder<AtomicInteger> timeGauge = TimeGauge.builder("timeGauge", count, TimeUnit.SECONDS, AtomicInteger::get); timeGauge.register(R); count.addAndGet(10086); print(); count.set(1); print(); } private static void print()throws Exception{ Search.in(R).meters().forEach(each -> { StringBuilder builder = new StringBuilder(); builder.append("name:") .append(each.getId().getName()) .append(",tags:") .append(each.getId().getTags()) .append(",type:").append(each.getId().getType()) .append(",value:").append(each.measure()); System.out.println(builder.toString()); }); } } //輸出 name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}] name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=1.0}]
Summary(摘要)主要用於跟蹤事件的分佈,在Micrometer中,對應的類是DistributionSummary(分發摘要)。它的使用方式和Timer十分類似,可是它的記錄值並不依賴於時間單位。常見的使用場景:使用DistributionSummary測量命中服務器的請求的有效負載大小。使用MeterRegistry
建立DistributionSummary實例以下:
DistributionSummary summary = registry.summary("response.size");
經過建造器流式建立以下:
DistributionSummary summary = DistributionSummary .builder("response.size") .description("a description of what this summary does") // 可選 .baseUnit("bytes") // 可選 .tags("region", "test") // 可選 .scale(100) // 可選 .register(registry);
DistributionSummary中有不少構建參數跟縮放和直方圖的表示相關,見下一節。
使用場景:
根據我的經驗和實踐,總結以下:
舉個相對具體的例子:
public class DistributionSummaryMain { private static final DistributionSummary DS = DistributionSummary.builder("cacheHitPercent") .register(new SimpleMeterRegistry()); private static final LoadingCache<String, String> CACHE = CacheBuilder.newBuilder() .maximumSize(1000) .recordStats() .expireAfterWrite(60, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String s) throws Exception { return selectFromDatabase(); } }); public static void main(String[] args) throws Exception{ String key = "doge"; String value = CACHE.get(key); record(); } private static void record()throws Exception{ CacheStats stats = CACHE.stats(); BigDecimal hitCount = new BigDecimal(stats.hitCount()); BigDecimal requestCount = new BigDecimal(stats.requestCount()); DS.record(hitCount.divide(requestCount,2,BigDecimal.ROUND_HALF_DOWN).doubleValue()); } }
直方圖和百分數配置適用於Summary和Timer,這部分相對複雜,等研究透了再補充。
集成了Micrometer框架的JVM應用使用到Micrometer的API收集的度量數據位於內存之中,所以,須要額外的存儲系統去存儲這些度量數據,須要有監控系統負責統一收集和處理這些數據,還須要有一些UI工具去展現數據,通常大佬只喜歡看炫酷的圖表或者動畫。常見的存儲系統就是時序數據庫,主流的有Influx、Datadog等。比較主流的監控系統(主要是用於數據收集和處理)就是Prometheus(通常叫普羅米修斯,下面就這樣叫吧)。而展現的UI目前相對用得比較多的就是Grafana。另外,Prometheus已經內置了一個時序數據庫的實現,所以,在作一套相對完善的度量數據監控的系統只須要依賴目標JVM應用,Prometheus組件和Grafana組件便可。下面花一點時間從零開始搭建一個這樣的系統,以前寫的一篇文章基於Windows系統,操做可能跟生產環境不夠接近,此次使用CentOS7。
SpringBoot中的spring-boot-starter-actuator
依賴已經集成了對Micrometer的支持,其中的metrics端點的不少功能就是經過Micrometer實現的,prometheus端點默認也是開啓支持的,實際上actuator依賴的spring-boot-actuator-autoconfigure
中集成了對不少框架的開箱即用的API,其中prometheus
包中集成了對Prometheus的支持,使得使用了actuator能夠輕易地讓項目暴露出prometheus端點,做爲Prometheus收集數據的客戶端,Prometheus(服務端軟件)能夠經過此端點收集應用中Micrometer的度量數據。
咱們先引入spring-boot-starter-actuator和spring-boot-starter-web,實現一個Counter和Timer做爲示例。依賴:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.22</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.1.0</version> </dependency> </dependencies>
接着編寫一個下單接口和一個消息發送模塊,模擬用戶下單以後向用戶發送消息:
//實體 @Data public class Message { private String orderId; private Long userId; private String content; } @Data public class Order { private String orderId; private Long userId; private Integer amount; private LocalDateTime createTime; } //控制器和服務類 @RestController public class OrderController { @Autowired private OrderService orderService; @PostMapping(value = "/order") public ResponseEntity<Boolean> createOrder(@RequestBody Order order){ return ResponseEntity.ok(orderService.createOrder(order)); } } @Slf4j @Service public class OrderService { private static final Random R = new Random(); @Autowired private MessageService messageService; public Boolean createOrder(Order order) { //模擬下單 try { int ms = R.nextInt(50) + 50; TimeUnit.MILLISECONDS.sleep(ms); log.info("保存訂單模擬耗時{}毫秒...", ms); } catch (Exception e) { //no-op } //記錄下單總數 Metrics.counter("order.count", "order.channel", order.getChannel()).increment(); //發送消息 Message message = new Message(); message.setContent("模擬短信..."); message.setOrderId(order.getOrderId()); message.setUserId(order.getUserId()); messageService.sendMessage(message); return true; } } @Slf4j @Service public class MessageService implements InitializingBean { private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500); private static BlockingQueue<Message> REAL_QUEUE; private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); private static final Random R = new Random(); static { REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size); } public void sendMessage(Message message) { try { REAL_QUEUE.put(message); } catch (InterruptedException e) { //no-op } } @Override public void afterPropertiesSet() throws Exception { EXECUTOR.execute(() -> { while (true) { try { Message message = REAL_QUEUE.take(); log.info("模擬發送短信,orderId:{},userId:{},內容:{},耗時:{}毫秒", message.getOrderId(), message.getUserId(), message.getContent(), R.nextInt(50)); } catch (Exception e) { throw new IllegalStateException(e); } } }); } } //切面類 @Component @Aspect public class TimerAspect { @Around(value = "execution(* club.throwable.smp.service.*Service.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName()); ThrowableHolder holder = new ThrowableHolder(); Object result = timer.recordCallable(() -> { try { return joinPoint.proceed(); } catch (Throwable e) { holder.throwable = e; } return null; }); if (null != holder.throwable) { throw holder.throwable; } return result; } private class ThrowableHolder { Throwable throwable; } }
yaml的配置以下:
server: port: 9091 management: server: port: 10091 endpoints: web: exposure: include: '*' base-path: /management
注意多看spring官方文檔關於Actuator的詳細描述,在SpringBoot-2.x以後,配置Web端點暴露的權限控制和1.x有很大的不一樣。總結一下就是:除了shutdown
端點以外,其餘端點默認都是開啓支持的這裏僅僅是開啓支持,並非暴露爲Web端點,端點必須暴露爲Web端點才能被訪問,禁用或者開啓端點支持的配置方式以下:
management.endpoint.${端點ID}.enabled=true/false能夠查
能夠查看actuator-api文檔查看全部支持的端點的特性,這個是2.1.0.RELEASE版本的官方文檔,不知道往後連接會不會掛掉。端點只開啓支持,可是不暴露爲Web端點,是沒法經過http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}訪問的。暴露監控端點爲Web端點的配置是:
management.endpoints.web.exposure.include=info,health management.endpoints.web.exposure.exclude=prometheus
management.endpoints.web.exposure.exclude
用於指定不暴露爲Web端點的監控端點,指定多個的時候用英文逗號分隔management.endpoints.web.exposure.include
默認指定的只有info和health兩個端點,咱們能夠直接指定暴露全部的端點:management.endpoints.web.exposure.include=*
,若是採用YAML配置,記得*要加單引號’*‘。暴露全部Web監控端點是一件比較危險的事情,若是須要在生產環境這樣作,請務必先確認http://{host}:{management.port}
不能經過公網訪問(也就是監控端點訪問的端口只能經過內網訪問,這樣能夠方便後面說到的Prometheus服務端經過此端口收集數據)。
Prometheus目前的最新版本是2.5,鑑於筆者沒深刻玩過Docker,這裏仍是直接下載它的壓縮包解壓安裝。
wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz tar xvfz prometheus-*.tar.gz cd prometheus-*
先編輯解壓出來的目錄下的prometheus配置文件prometheus.yml,主要修改scrape_configs節點的屬性:
scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: 'prometheus' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. # 這裏配置須要拉取度量信息的URL路徑,這裏選擇應用程序的prometheus端點 metrics_path: /management/prometheus static_configs: # 這裏配置host和port - targets: ['localhost:10091']
配置拉取度量數據的路徑爲localhost:10091/management/metrics,此前記得把前一節提到的應用在虛擬機中啓動。接着啓動Prometheus應用:
# 參數 --storage.tsdb.path=存儲數據的路徑,默認路徑爲./data ./prometheus --config.file=prometheus.yml
Prometheus引用的默認啓動端口是9090,啓動成功後,日誌以下:
此時,訪問ttp://${虛擬機host}:9090/targets就能看到當前Prometheus中執行的Job
訪問ttp://${虛擬機host}:9090/graph
以查找到咱們定義的度量Meter和spring-boot-starter-actuator中已經定義好的一些關於JVM或者Tomcat的度量Meter。咱們先對應用的/order接口進行調用,而後查看一下監控前面在應用中定義的rder_count_total``ethod_cost_time_seconds_sum
能夠看到,Meter的信息已經被收集和展現,可是顯然不夠詳細和炫酷,這個時候就須要使用Grafana的UI作一下點綴。
Grafana的安裝過程以下:
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm sudo yum localinstall grafana-5.3.4-1.x86_64.rpm
安裝完成後,經過命令service grafana-server start啓動便可,默認的啓動端口爲3000,經過ttp://${host}:3000
便可。初始的帳號密碼都爲admin,權限是管理員權限。接着須要在Home面板添加一個數據源,目的是對接Prometheus服務端從而能夠拉取它裏面的度量數據。數據源添加面板以下:
其實就是指向Prometheus服務端的端口就能夠了。接下來能夠天馬行空地添加須要的面板,就下單數量統計的指標,能夠添加一個Graph的面板
配置面板的時候,須要在基礎(General)中指定Title:
接着比較重要的是Metrics的配置,須要指定數據源和Prometheus的查詢語句:
最好參考一下Prometheus的官方文檔,稍微學習一下它的查詢語言PromQL的使用方式,一個面板能夠支持多個PromQL查詢。前面提到的兩項是基本配置,其餘配置項通常是圖表展現的輔助或者預警等輔助功能,這裏先不展開,能夠取Grafana的官網挖掘一下使用方式。而後咱們再調用一下下單接口,過一段時間,圖表的數據就會自動更新和展現:
接着添加一下項目中使用的Timer的Meter,便於監控方法的執行時間,完成以後大體以下:
來之不易,給個關注吧 https://github.com/yunlongn