SpringBoot實現(xiàn)埋點監(jiān)控
JVM應(yīng)用度量框架Micrometer實戰(zhàn)
前提
spring-actuator做度量統(tǒng)計收集,使用Prometheus(普羅米修斯)進(jìn)行數(shù)據(jù)收集,Grafana(增強ui)進(jìn)行數(shù)據(jù)展示,用于監(jiān)控生成環(huán)境機器的性能指標(biāo)和業(yè)務(wù)數(shù)據(jù)指標(biāo)。一般,我們叫這樣的操作為”埋點”。SpringBoot中的依賴spring-actuator中集成的度量統(tǒng)計API使用的框架是Micrometer,官網(wǎng)是Micrometer.io。
在實踐中發(fā)現(xiàn)了業(yè)務(wù)開發(fā)者濫用了Micrometer的度量類型Counter,導(dǎo)致無論什么情況下都只使用計數(shù)統(tǒng)計的功能。這篇文章就是基于Micrometer分析其他的度量類型API的作用和適用場景。
Micrometer提供的度量類庫
Meter是指一組用于收集應(yīng)用中的度量數(shù)據(jù)的接口,Meter單詞可以翻譯為”米”或者”千分尺”,但是顯然聽起來都不是很合理,因此下文直接叫Meter,理解它為度量接口即可。Meter是由MeterRegistry創(chuàng)建和保存的,可以理解MeterRegistry是Meter的工廠和緩存中心,一般而言每個JVM應(yīng)用在使用Micrometer的時候必須創(chuàng)建一個MeterRegistry的具體實現(xiàn)。
Micrometer中,Meter的具體類型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。
下面分節(jié)詳細(xì)介紹這些類型的使用方法和實戰(zhàn)使用場景。而一個Meter具體類型需要通過名字和Tag(這里指的是Micrometer提供的Tag接口)作為它的唯一標(biāo)識,這樣做的好處是可以使用名字進(jìn)行標(biāo)記,通過不同的Tag去區(qū)分多種維度進(jìn)行數(shù)據(jù)統(tǒng)計。
MeterRegistry
MeterRegistry在Micrometer是一個抽象類,主要實現(xiàn)包括:
- SimpleMeterRegistry:每個Meter的最新數(shù)據(jù)可以收集到SimpleMeterRegistry實例中,但是這些數(shù)據(jù)不會發(fā)布到其他系統(tǒng),也就是數(shù)據(jù)是位于應(yīng)用的內(nèi)存中的。
- CompositeMeterRegistry:多個MeterRegistry聚合,內(nèi)部維護(hù)了一個MeterRegistry的列表。
- 全局的MeterRegistry:工廠類io.micrometer.core.instrument.Metrics中持有一個靜態(tài)final的CompositeMeterRegistry實例globalRegistry。
當(dāng)然,使用者也可以自行繼承MeterRegistry去實現(xiàn)自定義的MeterRegistry。SimpleMeterRegistry適合做調(diào)試的時候使用,它的簡單使用方式如下:
MeterRegistry registry = new SimpleMeterRegistry(); Counter counter = registry.counter("counter"); counter.increment();
CompositeMeterRegistry實例初始化的時候,內(nèi)部持有的MeterRegistry列表是空的,如果此時用它新增一個Meter實例,Meter實例的操作是無效的
CompositeMeterRegistry composite = new CompositeMeterRegistry(); Counter compositeCounter = composite.counter("counter"); compositeCounter.increment(); // <- 實際上這一步操作是無效的,但是不會報錯 SimpleMeterRegistry simple = new SimpleMeterRegistry(); composite.add(simple); // <- 向CompositeMeterRegistry實例中添加SimpleMeterRegistry實例 compositeCounter.increment(); // <-計數(shù)成功
全局的MeterRegistry的使用方式更加簡單便捷,因為一切只需要操作工廠類Metrics的靜態(tài)方法:
Metrics.addRegistry(new SimpleMeterRegistry()); Counter counter = Metrics.counter("counter", "tag-1", "tag-2"); counter.increment();
Tag與Meter的命名
Micrometer中,Meter的命名約定使用英文逗號(dot,也就是”.”)分隔單詞。但是對于不同的監(jiān)控系統(tǒng),對命名的規(guī)約可能并不相同,如果命名規(guī)約不一致,在做監(jiān)控系統(tǒng)遷移或者切換的時候,可能會對新的系統(tǒng)造成破壞。
Micrometer中使用英文逗號分隔單詞的命名規(guī)則,再通過底層的命名轉(zhuǎn)換接口NamingConvention進(jìn)行轉(zhuǎn)換,最終可以適配不同的監(jiān)控系統(tǒng),同時可以消除監(jiān)控系統(tǒng)不允許的特殊字符的名稱和標(biāo)記等。開發(fā)者也可以覆蓋NamingConvention實現(xiàn)自定義的命名轉(zhuǎn)換規(guī)則:
registry.config().namingConvention(myCustomNamingConvention);
。
在Micrometer中,對一些主流的監(jiān)控系統(tǒng)或者存儲系統(tǒng)的命名規(guī)則提供了默認(rèn)的轉(zhuǎn)換方式,例如當(dāng)我們使用下面的命名時候:
MeterRegistry registry = ... registry.timer("http.server.requests");
對于不同的監(jiān)控系統(tǒng)或者存儲系統(tǒng),命名會自動轉(zhuǎn)換如下:
- Prometheus - http_server_requests_duration_seconds。
- Atlas - httpServerRequests。
- Graphite - http.server.requests。
- InfluxDB - http_server_requests。
其實NamingConvention已經(jīng)提供了5種默認(rèn)的轉(zhuǎn)換規(guī)則:dot、snakeCase、camelCase、upperCamelCase和slashes。
另外,Tag(標(biāo)簽)是Micrometer的一個重要的功能,嚴(yán)格來說,一個度量框架只有實現(xiàn)了標(biāo)簽的功能,才能真正地多維度進(jìn)行度量數(shù)據(jù)收集。Tag的命名一般需要是有意義的,所謂有意義就是可以根據(jù)Tag的命名可以推斷出它指向的數(shù)據(jù)到底代表什么維度或者什么類型的度量指標(biāo)。
假設(shè)我們需要監(jiān)控數(shù)據(jù)庫的調(diào)用和Http請求調(diào)用統(tǒng)計,一般推薦的做法是:
MeterRegistry registry = ... registry.counter("database.calls", "db", "users") registry.counter("http.requests", "uri", "/api/users")
這樣,當(dāng)我們選擇命名為”database.calls”的計數(shù)器,我們可以進(jìn)一步選擇分組”db”或者”users”分別統(tǒng)計不同分組對總調(diào)用數(shù)的貢獻(xiàn)或者組成。一個反例如下:
MeterRegistry registry = ... registry.counter("calls", "class", "database", "db", "users"); registry.counter("calls", "class", "http", "uri", "/api/users");
通過命名”calls”得到的計數(shù)器,由于標(biāo)簽混亂,數(shù)據(jù)是基本無法分組統(tǒng)計分析,這個時候可以認(rèn)為得到的時間序列的統(tǒng)計數(shù)據(jù)是沒有意義的。可以定義全局的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")));
像上面這樣子使用,就能通過主機,實例,區(qū)域,堆棧等操作環(huán)境進(jìn)行多維度深入分析。更多springboot實戰(zhàn)文章:SpringBoot內(nèi)容聚合
還有兩點點需要注意:
1、Tag的值必須不為null。
2、Micrometer中,Tag必須成對出現(xiàn),也就是Tag必須設(shè)置為偶數(shù)個,實際上它們以Key=Value
的形式存在,具體可以看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()); } }
當(dāng)然,有些時候,我們需要過濾一些必要的標(biāo)簽或者名稱進(jìn)行統(tǒng)計,或者為Meter的名稱添加白名單,這個時候可以使用MeterFilter。MeterFilter本身提供一些列的靜態(tài)方法,多個MeterFilter可以疊加或者組成鏈實現(xiàn)用戶最終的過濾策略。例如:
MeterRegistry registry = ... registry.config() .meterFilter(MeterFilter.ignoreTags("http")) .meterFilter(MeterFilter.denyNameStartsWith("jvm"));
表示忽略”http”標(biāo)簽,拒絕名稱以”jvm”字符串開頭的Meter。更多用法可以參詳一下MeterFilter這個類。
Meter的命名和Meter的Tag相互結(jié)合,以命名為軸心,以Tag為多維度要素,可以使度量數(shù)據(jù)的維度更加豐富,便于統(tǒng)計和分析。
Meters
前面提到Meter主要包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。下面逐一分析它們的作用和個人理解的實際使用場景(應(yīng)該說是生產(chǎn)環(huán)境)。
Counter
Counter是一種比較簡單的Meter,它是一種單值的度量類型,或者說是一個單值計數(shù)器。Counter接口允許使用者使用一個固定值(必須為正數(shù))進(jìn)行計數(shù)。準(zhǔn)確來說:Counter就是一個增量為正數(shù)的單值計數(shù)器。這個舉個很簡單的使用例子:
使用場景:
Counter的作用是記錄XXX的總量或者計數(shù)值,適用于一些增長類型的統(tǒng)計,例如下單、支付次數(shù)、Http請求總量記錄等等,通過Tag可以區(qū)分不同的場景,對于下單,可以使用不同的Tag標(biāo)記不同的業(yè)務(wù)來源或者是按日期劃分,對于Http請求總量記錄,可以使用Tag區(qū)分不同的URL。用下單業(yè)務(wù)舉個例子:
//實體 @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}]
上面的例子是使用全局靜態(tài)方法工廠類Metrics去構(gòu)造Counter實例,實際上,io.micrometer.core.instrument.Counter
接口提供了一個內(nèi)部建造器類Counter.Builder去實例化Counter,Counter.Builder的使用方式如下:
public class CounterBuilderMain { public static void main(String[] args) throws Exception{ Counter counter = Counter.builder("name") //名稱 .baseUnit("unit") //基礎(chǔ)單位 .description("desc") //描述 .tag("tagKey", "tagValue") //標(biāo)簽 .register(new SimpleMeterRegistry());//綁定的MeterRegistry counter.increment(); } }
FunctionCounter
FunctionCounter是Counter的特化類型,它把計數(shù)器數(shù)值增加的動作抽象成接口類型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匿名實現(xiàn)其實可以使用Lambda表達(dá)式簡化為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); //下面模擬三次計數(shù) n.incrementAndGet(); n.incrementAndGet(); n.incrementAndGet(); } }
FunctionCounter使用的一個明顯的好處是,我們不需要感知FunctionCounter實例的存在,實際上我們只需要操作作為FunctionCounter實例構(gòu)建元素之一的AtomicInteger實例即可,這種接口的設(shè)計方式在很多框架里面都可以看到。
Timer
Timer(計時器)適用于記錄耗時比較短的事件的執(zhí)行時間,通過時間分布展示事件的序列和發(fā)生頻率。所有的Timer的實現(xiàn)至少記錄了發(fā)生的事件的數(shù)量和這些事件的總耗時,從而生成一個時間序列。
Timer的基本單位基于服務(wù)端的指標(biāo)而定,但是實際上我們不需要過于關(guān)注Timer的基本單位,因為Micrometer在存儲生成的時間序列的時候會自動選擇適當(dāng)?shù)幕締挝?。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); ... }
實際上,比較常用和方便的方法是幾個函數(shù)式接口入?yún)⒌姆椒ǎ?/p>
Timer timer = ... timer.record(() -> dontCareAboutReturnValue()); timer.recordCallable(() -> returnValue()); Runnable r = timer.wrap(() -> dontCareAboutReturnValue()); Callable c = timer.wrap(() -> returnValue());
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
- 記錄指定方法的執(zhí)行時間用于展示。
- 記錄一些任務(wù)的執(zhí)行時間,從而確定某些數(shù)據(jù)來源的速率,例如消息隊列消息的消費速率等。
這里舉個實際的例子,要對系統(tǒng)做一個功能,記錄指定方法的執(zhí)行時間,還是用下單方法做例子:
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 } } }
在實際生產(chǎn)環(huán)境中,可以通過spring-aop把記錄方法耗時的邏輯抽象到一個切面中,這樣就能減少不必要的冗余的模板代碼。上面的例子是通過Mertics構(gòu)造Timer實例,實際上也可以使用Builder構(gòu)造:
MeterRegistry registry = ... Timer timer = Timer .builder("my.timer") .description("a description of what this timer does") // 可選 .tags("region", "test") // 可選 .register(registry);
另外,Timer的使用還可以基于它的內(nèi)部類Timer.Sample,通過start和stop兩個方法記錄兩者之間的邏輯的執(zhí)行耗時。例如:
Timer.Sample sample = Timer.start(registry); // 這里做業(yè)務(wù)邏輯 Response response = ... sample.stop(registry.timer("my.timer", "response", response.status()));
FunctionTimer
FunctionTimer是Timer的特化類型,它主要提供兩個單調(diào)遞增的函數(shù)(其實并不是單調(diào)遞增,只是在使用中一般需要隨著時間最少保持不變或者說不減少):一個用于計數(shù)的函數(shù)和一個用于記錄總調(diào)用耗時的函數(shù),它的建造器的入?yún)⑷缦拢?/p>
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 = ...; // 假設(shè)使用了Hazelcast緩存 registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache, c -> c.getLocalMapStats().getGetOperationCount(), //實際上就是cache的一個方法,記錄緩存生命周期初始化的增量(個數(shù)) c -> c.getLocalMapStats().getTotalGetLatency(), // Get操作的延遲時間總量,可以理解為耗時 TimeUnit.NANOSECONDS );
按照個人理解,ToDoubleFunction用于統(tǒng)計事件個數(shù),ToDoubleFunction用于記錄執(zhí)行總時間,實際上兩個函數(shù)都只是Function函數(shù)的變體,還有一個比較重要的是總時間的單位totalTimeFunctionUnit。簡單的使用方式如下:
public class FunctionTimerMain { public static void main(String[] args) throws Exception { //這個是為了滿足參數(shù),暫時不需要理會 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
LongTaskTimer也是一種Timer的特化類型,主要用于記錄長時間執(zhí)行的任務(wù)的持續(xù)時間,在任務(wù)完成之前,被監(jiān)測的事件或者任務(wù)仍然處于運行狀態(tài),任務(wù)完成的時候,任務(wù)執(zhí)行的總耗時才會被記錄下來。
LongTaskTimer適合用于長時間持續(xù)運行的事件耗時的記錄,例如相對耗時的定時任務(wù)。在Spring應(yīng)用中,可以簡單地使用@Scheduled和@Timed注解,基于spring-aop完成定時調(diào)度任務(wù)的總耗時記錄:
@Timed(value = "aws.scrape", longTask = true) @Scheduled(fixedDelay = 360000) void scrapeResources() { //這里做相對耗時的業(yè)務(wù)邏輯 }
當(dāng)然,在非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
Gauge(儀表)是獲取當(dāng)前度量記錄值的句柄,也就是它表示一個可以任意上下浮動的單數(shù)值度量Meter。Gauge通常用于變動的測量值,測量值用ToDoubleFunction參數(shù)的返回值設(shè)置,如當(dāng)前的內(nèi)存使用情況,同時也可以測量上下移動的”計數(shù)”,比如隊列中的消息數(shù)量。
官網(wǎng)文檔中提到Gauge的典型使用場景是用于測量集合或映射的大小或運行狀態(tài)中的線程數(shù)。Gauge一般用于監(jiān)測有自然上界的事件或者任務(wù),而Counter一般使用于無自然上界的事件或者任務(wù)的監(jiān)測,所以像Http請求總量計數(shù)應(yīng)該使用Counter而非Gauge。
MeterRegistry中提供了一些便于構(gòu)建用于觀察數(shù)值、函數(shù)、集合和映射的Gauge相關(guān)的方法:
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構(gòu)建Gauge并且返回了集合或者映射實例,使用這些集合或者映射實例就能在其size變化過程中記錄這個變更值。更重要的優(yōu)點是,我們不需要感知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創(chuàng)建Gauge之外,還可以使用建造器流式創(chuàng)建:
//一般我們不需要操作Gauge實例 Gauge gauge = Gauge .builder("gauge", myObj, myObj::gaugeValue) .description("a description of what this gauge does") // 可選 .tags("region", "test") // 可選 .register(registry);
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
- 有自然(物理)上界的浮動值的監(jiān)測,例如物理內(nèi)存、集合、映射、數(shù)值等。
- 有邏輯上界的浮動值的監(jiān)測,例如積壓的消息、(線程池中)積壓的任務(wù)等,其實本質(zhì)也是集合或者映射的監(jiān)測。
舉個相對實際的例子,假設(shè)我們需要對登錄后的用戶發(fā)送一條短信或者推送,做法是消息先投放到一個阻塞隊列,再由一個線程消費消息進(jìn)行其他操作:
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(); } }
上面的例子代碼寫得比較糟糕,只為了演示相關(guān)使用方式,切勿用于生產(chǎn)環(huán)境。
TimeGauge
TimeGauge是Gauge的特化類型,相比Gauge,它的構(gòu)建器中多了一個TimeUnit類型的參數(shù),用于指定ToDoubleFunction入?yún)⒌幕A(chǔ)時間單位。這里簡單舉個使用例子:
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}]
DistributionSummary
Summary(摘要)主要用于跟蹤事件的分布,在Micrometer中,對應(yīng)的類是DistributionSummary(分發(fā)摘要)。它的使用方式和Timer十分相似,但是它的記錄值并不依賴于時間單位。
常見的使用場景:使用DistributionSummary測量命中服務(wù)器的請求的有效負(fù)載大小。使用MeterRegistry創(chuàng)建DistributionSummary實例如下:
DistributionSummary summary = registry.summary("response.size");
通過建造器流式創(chuàng)建如下:
DistributionSummary summary = DistributionSummary .builder("response.size") .description("a description of what this summary does") // 可選 .baseUnit("bytes") // 可選 .tags("region", "test") // 可選 .scale(100) // 可選 .register(registry);
DistributionSummary中有很多構(gòu)建參數(shù)跟縮放和直方圖的表示相關(guān),見下一節(jié)。
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
1、不依賴于時間單位的記錄值的測量,例如服務(wù)器有效負(fù)載值,緩存的命中率等。
舉個相對具體的例子:
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()); } }
直方圖和百分?jǐn)?shù)配置
直方圖和百分?jǐn)?shù)配置適用于Summary和Timer,這部分相對復(fù)雜,等研究透了再補充。
基于SpirngBoot、Prometheus、Grafana集成
集成了Micrometer框架的JVM應(yīng)用使用到Micrometer的API收集的度量數(shù)據(jù)位于內(nèi)存之中,因此,需要額外的存儲系統(tǒng)去存儲這些度量數(shù)據(jù),需要有監(jiān)控系統(tǒng)負(fù)責(zé)統(tǒng)一收集和處理這些數(shù)據(jù),還需要有一些UI工具去展示數(shù)據(jù),一般大佬只喜歡看炫酷的圖表或者動畫。
常見的存儲系統(tǒng)就是時序數(shù)據(jù)庫,主流的有Influx、Datadog等。比較主流的監(jiān)控系統(tǒng)(主要是用于數(shù)據(jù)收集和處理)就是Prometheus(一般叫普羅米修斯,下面就這樣叫吧)。而展示的UI目前相對用得比較多的就是Grafana。
另外,Prometheus已經(jīng)內(nèi)置了一個時序數(shù)據(jù)庫的實現(xiàn),因此,在做一套相對完善的度量數(shù)據(jù)監(jiān)控的系統(tǒng)只需要依賴目標(biāo)JVM應(yīng)用,Prometheus組件和Grafana組件即可。下面花一點時間從零開始搭建一個這樣的系統(tǒng),使用CentOS7。
SpirngBoot中使用Micrometer
SpringBoot中的spring-boot-starter-actuator依賴已經(jīng)集成了對Micrometer的支持,其中的metrics端點的很多功能就是通過Micrometer實現(xiàn)的,prometheus端點默認(rèn)也是開啟支持的,實際上actuator依賴的spring-boot-actuator-autoconfigure中集成了對很多框架的開箱即用的API。
其中prometheus包中集成了對Prometheus的支持,使得使用了actuator可以輕易地讓項目暴露出prometheus端點,作為Prometheus收集數(shù)據(jù)的客戶端,Prometheus(服務(wù)端軟件)可以通過此端點收集應(yīng)用中Micrometer的度量數(shù)據(jù)。
我們先引入spring-boot-starter-actuator和spring-boot-starter-web,實現(xiàn)一個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>
接著編寫一個下單接口和一個消息發(fā)送模塊,模擬用戶下單之后向用戶發(fā)送消息:
//實體 @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; } //控制器和服務(wù)類 @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 } //記錄下單總數(shù) Metrics.counter("order.count", "order.channel", order.getChannel()).increment(); //發(fā)送消息 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("模擬發(fā)送短信,orderId:{},userId:{},內(nèi)容:{},耗時:{}毫秒", 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官方文檔關(guān)于Actuator的詳細(xì)描述,在SpringBoot-2.x之后,配置Web端點暴露的權(quán)限控制和1.x有很大的不同。
總結(jié)一下就是:除了shutdown端點之外,其他端點默認(rèn)都是開啟支持的這里僅僅是開啟支持,并不是暴露為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}
訪問的。
暴露監(jiān)控端點為Web端點的配置是:
management.endpoints.web.exposure.include=info,health management.endpoints.web.exposure.exclude=prometheus
management.endpoints.web.exposure.exclude
用于指定不暴露為Web端點的監(jiān)控端點,指定多個的時候用英文逗號分隔management.endpoints.web.exposure.include
默認(rèn)指定的只有info和health兩個端點,我們可以直接指定暴露所有的端點:management.endpoints.web.exposure.include=*
,如果采用YAML配置,記得要加單引號’‘。暴露所有Web監(jiān)控端點是一件比較危險的事情,如果需要在生產(chǎn)環(huán)境這樣做,請務(wù)必先確認(rèn)http://{host}:{management.port}
不能通過公網(wǎng)訪問(也就是監(jiān)控端點訪問的端口只能通過內(nèi)網(wǎng)訪問,這樣可以方便后面說到的Prometheus服務(wù)端通過此端口收集數(shù)據(jù))。
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節(jié)點的屬性:
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路徑,這里選擇應(yīng)用程序的prometheus端點 metrics_path: /management/prometheus static_configs: # 這里配置host和port - targets: ['localhost:10091']
配置拉取度量數(shù)據(jù)的路徑為localhost:10091/management/metrics,此前記得把前一節(jié)提到的應(yīng)用在虛擬機中啟動。接著啟動Prometheus應(yīng)用:
# 參數(shù) --storage.tsdb.path=存儲數(shù)據(jù)的路徑,默認(rèn)路徑為./data ./prometheus --config.file=prometheus.yml
Prometheus引用的默認(rèn)啟動端口是9090,啟動成功后,日志如下:
此時,訪問ttp://${虛擬機host}:9090/targets
就能看到當(dāng)前Prometheus中執(zhí)行的Job
訪問ttp://${虛擬機host}:9090/graph
以查找到我們定義的度量Meter和spring-boot-starter-actuator中已經(jīng)定義好的一些關(guān)于JVM或者Tomcat的度量Meter。
我們先對應(yīng)用的/order接口進(jìn)行調(diào)用,然后查看一下監(jiān)控前面在應(yīng)用中定義的rder_count_total``ethod_cost_time_seconds_sum
可以看到,Meter的信息已經(jīng)被收集和展示,但是顯然不夠詳細(xì)和炫酷,這個時候就需要使用Grafana的UI做一下點綴。
Grafana的安裝和使用
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啟動即可,默認(rèn)的啟動端口為3000,通過ttp://${host}:3000
即可。初始的賬號密碼都為admin,權(quán)限是管理員權(quán)限。接著需要在Home面板添加一個數(shù)據(jù)源,目的是對接Prometheus服務(wù)端從而可以拉取它里面的度量數(shù)據(jù)。數(shù)據(jù)源添加面板如下:
其實就是指向Prometheus服務(wù)端的端口就可以了。接下來可以天馬行空地添加需要的面板,就下單數(shù)量統(tǒng)計的指標(biāo),可以添加一個Graph的面板
配置面板的時候,需要在基礎(chǔ)(General)中指定Title:
接著比較重要的是Metrics的配置,需要指定數(shù)據(jù)源和Prometheus的查詢語句:
最好參考一下Prometheus的官方文檔,稍微學(xué)習(xí)一下它的查詢語言PromQL的使用方式,一個面板可以支持多個PromQL查詢。
前面提到的兩項是基本配置,其他配置項一般是圖表展示的輔助或者預(yù)警等輔助功能,這里先不展開,可以取Grafana的官網(wǎng)挖掘一下使用方式。然后我們再調(diào)用一下下單接口,過一段時間,圖表的數(shù)據(jù)就會自動更新和展示:
接著添加一下項目中使用的Timer的Meter,便于監(jiān)控方法的執(zhí)行時間,完成之后大致如下:
到此這篇關(guān)于SpringBoot實現(xiàn)埋點監(jiān)控的文章就介紹到這了,更多相關(guān)SpringBoot 埋點監(jiān)控內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java配置變量的解釋,搬運他人優(yōu)質(zhì)評論(推薦)
這篇文章主要介紹了java配置變量,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04淺析Spring Boot中的spring-boot-load模塊
spring-boot-loader模塊允許我們使用java -jar archive.jar運行包含嵌套依賴jar的jar或者war文件,它提供了三種類啟動器。下面通過本文給大家介紹spring-boot-load模塊的相關(guān)知識,感興趣的朋友一起看看吧2018-01-01IDEA2020.1創(chuàng)建springboot項目(國內(nèi)腳手架)安裝lombok
這篇文章主要介紹了IDEA2020.1創(chuàng)建springboot項目(國內(nèi)腳手架)安裝lombok,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06eclipse 如何創(chuàng)建 user library 方法詳解
這篇文章主要介紹了eclipse 如何創(chuàng)建 user library 方法詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04Java 數(shù)據(jù)庫時間返回前端顯示錯誤(差8個小時)的解決方法
本文主要介紹了Java 數(shù)據(jù)庫時間返回前端顯示錯誤(差8個小時)的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08