<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          老王,快給你的 Spring Boot 做個埋點監(jiān)控吧!

          共 19993字,需瀏覽 40分鐘

           ·

          2022-04-28 21:20

          關(guān)注我們,設為星標,每天7:40不見不散,架構(gòu)路上與您共享

          回復架構(gòu)師獲取資源


          大家好,我是你們的朋友架構(gòu)君,一個會寫代碼吟詩的架構(gòu)師。

          'javajgs.com';

          JVM應用度量框架Micrometer實戰(zhàn)

          前提

          spring-actuator做度量統(tǒng)計收集,使用Prometheus(普羅米修斯)進行數(shù)據(jù)收集,Grafana(增強ui)進行數(shù)據(jù)展示,用于監(jiān)控生成環(huán)境機器的性能指標和業(yè)務數(shù)據(jù)指標。一般,我們叫這樣的操作為”埋點”。SpringBoot中的依賴spring-actuator中集成的度量統(tǒng)計API使用的框架是Micrometer,官網(wǎng)是Micrometer.io。

          在實踐中發(fā)現(xiàn)了業(yè)務開發(fā)者濫用了Micrometer的度量類型Counter,導致無論什么情況下都只使用計數(shù)統(tǒng)計的功能。這篇文章就是基于Micrometer分析其他的度量類型API的作用和適用場景。

          Micrometer提供的度量類庫

          Meter是指一組用于收集應用中的度量數(shù)據(jù)的接口,Meter單詞可以翻譯為”米”或者”千分尺”,但是顯然聽起來都不是很合理,因此下文直接叫Meter,理解它為度量接口即可。Meter是由MeterRegistry創(chuàng)建和保存的,可以理解MeterRegistry是Meter的工廠和緩存中心,一般而言每個JVM應用在使用Micrometer的時候必須創(chuàng)建一個MeterRegistry的具體實現(xiàn)。

          Micrometer中,Meter的具體類型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。

          下面分節(jié)詳細介紹這些類型的使用方法和實戰(zhàn)使用場景。而一個Meter具體類型需要通過名字和Tag(這里指的是Micrometer提供的Tag接口)作為它的唯一標識,這樣做的好處是可以使用名字進行標記,通過不同的Tag去區(qū)分多種維度進行數(shù)據(jù)統(tǒng)計。

          MeterRegistry

          MeterRegistry在Micrometer是一個抽象類,主要實現(xiàn)包括:

          • SimpleMeterRegistry:每個Meter的最新數(shù)據(jù)可以收集到SimpleMeterRegistry實例中,但是這些數(shù)據(jù)不會發(fā)布到其他系統(tǒng),也就是數(shù)據(jù)是位于應用的內(nèi)存中的。
          • CompositeMeterRegistry:多個MeterRegistry聚合,內(nèi)部維護了一個MeterRegistry的列表。
          • 全局的MeterRegistry:工廠類io.micrometer.core.instrument.Metrics中持有一個靜態(tài)final的CompositeMeterRegistry實例globalRegistry。

          當然,使用者也可以自行繼承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進行轉(zhuǎn)換,最終可以適配不同的監(jiān)控系統(tǒng),同時可以消除監(jiān)控系統(tǒng)不允許的特殊字符的名稱和標記等。開發(fā)者也可以覆蓋NamingConvention實現(xiàn)自定義的命名轉(zhuǎn)換規(guī)則:registry.config().namingConvention(myCustomNamingConvention);

          在Micrometer中,對一些主流的監(jiān)控系統(tǒng)或者存儲系統(tǒng)的命名規(guī)則提供了默認的轉(zhuǎn)換方式,例如當我們使用下面的命名時候:

          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種默認的轉(zhuǎn)換規(guī)則:dot、snakeCase、camelCase、upperCamelCase和slashes。

          另外,Tag(標簽)是Micrometer的一個重要的功能,嚴格來說,一個度量框架只有實現(xiàn)了標簽的功能,才能真正地多維度進行度量數(shù)據(jù)收集。Tag的命名一般需要是有意義的,所謂有意義就是可以根據(jù)Tag的命名可以推斷出它指向的數(shù)據(jù)到底代表什么維度或者什么類型的度量指標。

          假設我們需要監(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")

          這樣,當我們選擇命名為”database.calls”的計數(shù)器,我們可以進一步選擇分組”db”或者”users”分別統(tǒng)計不同分組對總調(diào)用數(shù)的貢獻或者組成。一個反例如下:

          MeterRegistry?registry?=?...
          registry.counter("calls",
          ????"class",?"database",
          ????"db",?"users");

          registry.counter("calls",
          ????"class",?"http",
          ????"uri",?"/api/users");

          通過命名”calls”得到的計數(shù)器,由于標簽混亂,數(shù)據(jù)是基本無法分組統(tǒng)計分析,這個時候可以認為得到的時間序列的統(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)境進行多維度深入分析。

          還有兩點點需要注意:

          1、Tag的值必須不為null。

          2、Micrometer中,Tag必須成對出現(xiàn),也就是Tag必須設置為偶數(shù)個,實際上它們以Key=Value的形式存在,具體可以看io.micrometer.core.instrument.Tag接口:

          public?interface?Tag?extends?Comparable?{
          ????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());
          ????}
          }

          當然,有些時候,我們需要過濾一些必要的標簽或者名稱進行統(tǒng)計,或者為Meter的名稱添加白名單,這個時候可以使用MeterFilter。MeterFilter本身提供一些列的靜態(tài)方法,多個MeterFilter可以疊加或者組成鏈實現(xiàn)用戶最終的過濾策略。例如:

          MeterRegistry?registry?=?...
          registry.config()
          ????.meterFilter(MeterFilter.ignoreTags("http"))
          ????.meterFilter(MeterFilter.denyNameStartsWith("jvm"));

          表示忽略”http”標簽,拒絕名稱以”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。下面逐一分析它們的作用和個人理解的實際使用場景(應該說是生產(chǎn)環(huán)境)。

          Counter

          Counter是一種比較簡單的Meter,它是一種單值的度量類型,或者說是一個單值計數(shù)器。Counter接口允許使用者使用一個固定值(必須為正數(shù))進行計數(shù)。準確來說:Counter就是一個增量為正數(shù)的單值計數(shù)器。這個舉個很簡單的使用例子:

          使用場景:

          Counter的作用是記錄XXX的總量或者計數(shù)值,適用于一些增長類型的統(tǒng)計,例如下單、支付次數(shù)、Http請求總量記錄等等,通過Tag可以區(qū)分不同的場景,對于下單,可以使用不同的Tag標記不同的業(yè)務來源或者是按日期劃分,對于Http請求總量記錄,可以使用Tag區(qū)分不同的URL。用下單業(yè)務舉個例子:

          //實體
          @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")??//標簽
          ????????????????????.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表達式簡化為AtomicInteger::get
          ????????????FunctionCounter.builder("functionCounter",?n,?new?ToDoubleFunction()?{
          ????????????????@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實例即可,這種接口的設計方式在很多框架里面都可以看到。

          Timer

          Timer(計時器)適用于記錄耗時比較短的事件的執(zhí)行時間,通過時間分布展示事件的序列和發(fā)生頻率。所有的Timer的實現(xiàn)至少記錄了發(fā)生的事件的數(shù)量和這些事件的總耗時,從而生成一個時間序列。

          Timer的基本單位基于服務端的指標而定,但是實際上我們不需要過于關(guān)注Timer的基本單位,因為Micrometer在存儲生成的時間序列的時候會自動選擇適當?shù)幕締挝弧imer接口提供的常用方法如下:

          public?interface?Timer?extends?Meter?{
          ????...
          ????void?record(long?var1,?TimeUnit?var3);

          ????default?void?record(Duration?duration)?{
          ????????this.record(duration.toNanos(),?TimeUnit.NANOSECONDS);
          ????}

          ?????T?record(Supplier?var1);

          ?????T?recordCallable(Callable?var1)?throws?Exception;

          ????void?record(Runnable?var1);

          ????default?Runnable?wrap(Runnable?f)?{
          ????????return?()?->?{
          ????????????this.record(f);
          ????????};
          ????}

          ????default??Callable?wrap(Callable?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í)行時間用于展示。
          • 記錄一些任務的執(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è)務邏輯
          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??Builder?builder(String?name,?T?obj,?ToLongFunction?countFunction,
          ??????????????????????????????????ToDoubleFunction?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的一個方法,記錄緩存生命周期初始化的增量(個數(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í)行的任務的持續(xù)時間,在任務完成之前,被監(jiān)測的事件或者任務仍然處于運行狀態(tài),任務完成的時候,任務執(zhí)行的總耗時才會被記錄下來。

          LongTaskTimer適合用于長時間持續(xù)運行的事件耗時的記錄,例如相對耗時的定時任務。在Spring應用中,可以簡單地使用@Scheduled和@Timed注解,基于spring-aop完成定時調(diào)度任務的總耗時記錄:

          @Timed(value?=?"aws.scrape",?longTask?=?true)
          @Scheduled(fixedDelay?=?360000)
          void?scrapeResources()?{
          ????//這里做相對耗時的業(yè)務邏輯
          }

          當然,在非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(儀表)是獲取當前度量記錄值的句柄,也就是它表示一個可以任意上下浮動的單數(shù)值度量Meter。Gauge通常用于變動的測量值,測量值用ToDoubleFunction參數(shù)的返回值設置,如當前的內(nèi)存使用情況,同時也可以測量上下移動的”計數(shù)”,比如隊列中的消息數(shù)量。

          官網(wǎng)文檔中提到Gauge的典型使用場景是用于測量集合或映射的大小或運行狀態(tài)中的線程數(shù)。Gauge一般用于監(jiān)測有自然上界的事件或者任務,而Counter一般使用于無自然上界的事件或者任務的監(jiān)測,所以像Http請求總量計數(shù)應該使用Counter而非Gauge。

          MeterRegistry中提供了一些便于構(gòu)建用于觀察數(shù)值、函數(shù)、集合和映射的Gauge相關(guān)的方法:

          List?list?=?registry.gauge("listGauge",?Collections.emptyList(),?new?ArrayList<>(),?List::size);?
          List?list2?=?registry.gaugeCollectionSize("listSize2",?Tags.empty(),?new?ArrayList<>());?
          Map?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)測,例如積壓的消息、(線程池中)積壓的任務等,其實本質(zhì)也是集合或者映射的監(jiān)測。

          舉個相對實際的例子,假設我們需要對登錄后的用戶發(fā)送一條短信或者推送,做法是消息先投放到一個阻塞隊列,再由一個線程消費消息進行其他操作:

          public?class?GaugeMain?{

          ????private?static?final?MeterRegistry?MR?=?new?SimpleMeterRegistry();
          ????private?static?final?BlockingQueue?QUEUE?=?new?ArrayBlockingQueue<>(500);
          ????private?static?BlockingQueue?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?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中,對應的類是DistributionSummary(分發(fā)摘要)。它的使用方式和Timer十分相似,但是它的記錄值并不依賴于時間單位。

          常見的使用場景:使用DistributionSummary測量命中服務器的請求的有效負載大小。使用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、不依賴于時間單位的記錄值的測量,例如服務器有效負載值,緩存的命中率等。

          舉個相對具體的例子:

          public?class?DistributionSummaryMain?{

          ????????private?static?final?DistributionSummary?DS??=?DistributionSummary.builder("cacheHitPercent")
          ????????????????.register(new?SimpleMeterRegistry());

          ????????private?static?final?LoadingCache?CACHE?=?CacheBuilder.newBuilder()
          ????????????????.maximumSize(1000)
          ????????????????.recordStats()
          ????????????????.expireAfterWrite(60,?TimeUnit.SECONDS)
          ????????????????.build(new?CacheLoader()?{
          ????????????????????@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());
          ????????}
          }

          直方圖和百分數(shù)配置

          直方圖和百分數(shù)配置適用于Summary和Timer,這部分相對復雜,等研究透了再補充。

          基于SpirngBoot、Prometheus、Grafana集成

          集成了Micrometer框架的JVM應用使用到Micrometer的API收集的度量數(shù)據(jù)位于內(nèi)存之中,因此,需要額外的存儲系統(tǒng)去存儲這些度量數(shù)據(jù),需要有監(jiān)控系統(tǒng)負責統(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)只需要依賴目標JVM應用,Prometheus組件和Grafana組件即可。下面花一點時間從零開始搭建一個這樣的系統(tǒng),使用CentOS7。

          SpirngBoot中使用Micrometer

          SpringBoot中的spring-boot-starter-actuator依賴已經(jīng)集成了對Micrometer的支持,其中的metrics端點的很多功能就是通過Micrometer實現(xiàn)的,prometheus端點默認也是開啟支持的,實際上actuator依賴的spring-boot-actuator-autoconfigure中集成了對很多框架的開箱即用的API。

          其中prometheus包中集成了對Prometheus的支持,使得使用了actuator可以輕易地讓項目暴露出prometheus端點,作為Prometheus收集數(shù)據(jù)的客戶端,Prometheus(服務端軟件)可以通過此端點收集應用中Micrometer的度量數(shù)據(jù)。

          我們先引入spring-boot-starter-actuator和spring-boot-starter-web,實現(xiàn)一個Counter和Timer作為示例。依賴:


          ??????
          ??????????
          ??????????????org.springframework.boot
          ??????????????spring-boot-dependencies
          ??????????????2.1.0.RELEASE
          ??????????????<type>pomtype>
          ??????????????import
          ??????????

          ??????

          ??

          ??
          ??????
          ??????????org.springframework.boot
          ??????????spring-boot-starter-web
          ??????

          ??????
          ??????????org.springframework.boot
          ??????????spring-boot-starter-actuator
          ??????

          ??????
          ??????????org.springframework.boot
          ??????????spring-boot-starter-aop
          ??????

          ??????
          ??????????org.projectlombok
          ??????????lombok
          ??????????1.16.22
          ??????


          ??????????io.micrometer
          ??????????micrometer-registry-prometheus
          ??????????1.1.0
          ??????

          ??

          接著編寫一個下單接口和一個消息發(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;
          ????}

          ????//控制器和服務類
          ????@RestController
          ????public?class?OrderController?{

          ????????@Autowired
          ????????private?OrderService?orderService;

          ????????@PostMapping(value?=?"/order")
          ????????public?ResponseEntity?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?QUEUE?=?new?ArrayBlockingQueue<>(500);
          ????????private?static?BlockingQueue?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的詳細描述,在SpringBoot-2.x之后,配置Web端點暴露的權(quán)限控制和1.x有很大的不同。

          總結(jié)一下就是:除了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}訪問的。

          暴露監(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默認指定的只有info和health兩個端點,我們可以直接指定暴露所有的端點:management.endpoints.web.exposure.include=*,如果采用YAML配置,記得要加單引號’‘。暴露所有Web監(jiān)控端點是一件比較危險的事情,如果需要在生產(chǎn)環(huán)境這樣做,請務必先確認http://{host}:{management.port}不能通過公網(wǎng)訪問(也就是監(jiān)控端點訪問的端口只能通過內(nèi)網(wǎng)訪問,這樣可以方便后面說到的Prometheus服務端通過此端口收集數(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=`?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']

          配置拉取度量數(shù)據(jù)的路徑為localhost:10091/management/metrics,此前記得把前一節(jié)提到的應用在虛擬機中啟動。接著啟動Prometheus應用:

          #?參數(shù)?--storage.tsdb.path=存儲數(shù)據(jù)的路徑,默認路徑為./data
          ./prometheus?--config.file=prometheus.yml

          Prometheus引用的默認啟動端口是9090,啟動成功后,日志如下:

          img

          此時,訪問ttp://${虛擬機host}:9090/targets就能看到當前Prometheus中執(zhí)行的Job

          img

          訪問ttp://${虛擬機host}:9090/graph以查找到我們定義的度量Meter和spring-boot-starter-actuator中已經(jīng)定義好的一些關(guān)于JVM或者Tomcat的度量Meter。

          我們先對應用的/order接口進行調(diào)用,然后查看一下監(jiān)控前面在應用中定義的rder_count_total``ethod_cost_time_seconds_sum

          img
          img

          可以看到,Meter的信息已經(jīng)被收集和展示,但是顯然不夠詳細和炫酷,這個時候就需要使用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啟動即可,默認的啟動端口為3000,通過ttp://${host}:3000即可。初始的賬號密碼都為admin,權(quán)限是管理員權(quán)限。接著需要在Home面板添加一個數(shù)據(jù)源,目的是對接Prometheus服務端從而可以拉取它里面的度量數(shù)據(jù)。數(shù)據(jù)源添加面板如下:

          img

          其實就是指向Prometheus服務端的端口就可以了。接下來可以天馬行空地添加需要的面板,就下單數(shù)量統(tǒng)計的指標,可以添加一個Graph的面板

          img

          配置面板的時候,需要在基礎(chǔ)(General)中指定Title:

          img

          接著比較重要的是Metrics的配置,需要指定數(shù)據(jù)源和Prometheus的查詢語句:

          img

          最好參考一下Prometheus的官方文檔,稍微學習一下它的查詢語言PromQL的使用方式,一個面板可以支持多個PromQL查詢。

          前面提到的兩項是基本配置,其他配置項一般是圖表展示的輔助或者預警等輔助功能,這里先不展開,可以取Grafana的官網(wǎng)挖掘一下使用方式。然后我們再調(diào)用一下下單接口,過一段時間,圖表的數(shù)據(jù)就會自動更新和展示:

          img

          接著添加一下項目中使用的Timer的Meter,便于監(jiān)控方法的執(zhí)行時間,完成之后大致如下:

          文章來源:https://cnblogs.com/rolandlee/p/11343848.html


          到此文章就結(jié)束了。Java架構(gòu)師必看一個集公眾號、小程序、網(wǎng)站(3合1的文章平臺,給您架構(gòu)路上一臂之力,javajgs.com)。如果今天的文章對你在進階架構(gòu)師的路上有新的啟發(fā)和進步,歡迎轉(zhuǎn)發(fā)給更多人。歡迎加入架構(gòu)師社區(qū)技術(shù)交流群,眾多大咖帶你進階架構(gòu)師,在后臺回復“加群”即可入群。



          這些年小編給你分享過的干貨


          1.idea永久激活碼(親測可用)

          2.優(yōu)質(zhì)ERP系統(tǒng)帶進銷存財務生產(chǎn)功能(附源碼)

          3.優(yōu)質(zhì)SpringBoot帶工作流管理項目(附源碼)

          4.最好用的OA系統(tǒng),拿來即用(附源碼)

          5.SBoot+Vue外賣系統(tǒng)前后端都有(附源碼

          6.SBoot+Vue可視化大屏拖拽項目(附源碼)



          轉(zhuǎn)發(fā)在看就是最大的支持??

          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费无码一区二区三区 | 天天干 夜夜操 | 操逼操逼操逼操逼操逼操逼操逼操逼 | 无码一区二区三区四区五区六区七区八区 | 成人网址在线看 |