服務性能監控之Micrometer詳解


Micrometer 為基於 JVM 的應用程序的性能監測數據收集提供了一個通用的 API,支持多種度量指標類型,這些指標可以用於觀察、警報以及對應用程序當前狀態做出響應。

通過添加如下依賴可以將 Micrometer 收集的服務指標數據發布到 Prometheus 中。

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
  <version>${micrometer.version}</version>
</dependency>

當然如果你還沒有確定好接入哪種監測系統,也可以先直接依賴micrometer-core,然后創建一個SimpleMeterRegistry

可接入監控系統

Micrometer 有一組包含各種監控系統實現的模塊,其中的每一種實現被稱為registry

在深入了解 Micrometer 之前,我們首先來看一下監控系統的三個重要特征:

  • 維度(Dimensionality):描述系統是否支持多維度數據模型。

    Dimensional Hierarchical
    AppOptics, Atlas, Azure Monitor, Cloudwatch, Datadog, Datadog StatsD, Dynatrace, Elastic, Humio, Influx, KairosDB, New Relic, Prometheus, SignalFx, Sysdig StatsD, Telegraf StatsD, Wavefront Graphite, Ganglia, JMX, Etsy StatsD
  • 速率聚合(Rate Aggregation):指的是在規定的時間間隔內的一組樣本聚合。一種是指標數據發送前在客戶端做速率聚合,另一種是直接發送聚合值。

    Client-side Server-side
    AppOptics, Atlas, Azure Monitor, Datadog, Elastic, Graphite, Ganglia, Humio, Influx, JMX, Kairos, New Relic, all StatsD flavors, SignalFx Prometheus, Wavefront
  • 發布(Publishing):描述的是指標數據的發布方式,一種是客戶端定時將數據推送給監控系統,還有一種是監控系統在空閑時間自己調客戶端接口拉數據。

    Client pushes Server polls
    AppOptics, Atlas, Azure Monitor, Datadog, Elastic, Graphite, Ganglia, Humio, Influx, JMX, Kairos, New Relic, SignalFx, Wavefront Prometheus, all StatsD flavors

Registry

Meter是一個用於收集應用程序各項指標數據的接口,Micrometer 中的所有的Meters都通過MeterRegistry創建並管理,Micrometer 支持的每一種監控系統都有對應的MeterRegistry實現。

最簡單的Register就是SimpleMeterRegistry(在 Spring-based 的應用程序中自動裝配),它會在內存中保存每個meter的最新值,但是不會將這個值發布到任何地方。

MeterRegistry registry = new SimpleMeterRegistry();

Composite Registries

Micrometer 提供了一個CompositeMeterRegistry,允許開發者通過添加多個 registry 的方式將指標數據同時發布到多個監控系統中。

CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
// 此處increment語句處於等待狀態,直到CompositeMeterRegistry注冊了一個registry。
// 此時counter計數器值為0
compositeCounter.increment(); (1)

SimpleMeterRegistry simple = new SimpleMeterRegistry();
// counter計數器注冊到simple registry
composite.add(simple); (2)

// simple registry counter 與CompositeMeterRegistry中的其他registries的counter一起遞增
compositeCounter.increment(); (3)

Global Registry

Micrometer 提供了一個全局注冊表Metrics.globalRegistry,它也是一個CompositeMeterRegistry,其內部提供了一系列用於構建 meters 的方法。

public class Metrics {
    public static final CompositeMeterRegistry globalRegistry = new CompositeMeterRegistry();
    private static final More more = new More();

    /**
     * 當使用Metrics.counter(…​)之類的方法構建meters之后,就可以向globalRegistry中添加registry了
     * 這些meters會被添加到每個registry中
     *
     * @param registry Registry to add.
     */
    public static void addRegistry(MeterRegistry registry) {
        globalRegistry.add(registry);
    }

    /**
     * Remove a registry from the global composite registry. Removing a registry does not remove any meters
     * that were added to it by previous participation in the global composite.
     *
     * @param registry Registry to remove.
     */
    public static void removeRegistry(MeterRegistry registry) {
        globalRegistry.remove(registry);
    }

    /**
     * Tracks a monotonically increasing value.
     *
     * @param name The base metric name
     * @param tags Sequence of dimensions for breaking down the name.
     * @return A new or existing counter.
     */
    public static Counter counter(String name, Iterable<Tag> tags) {
        return globalRegistry.counter(name, tags);
    }

    ...
}

自定義 Registry

Micrometer 為我們提供了很多開箱即用的 Registry,基本上可以滿足大多數的業務場景。同時也支持用戶根據實際場景需求,自定義 registry。

通常我們可以通過繼承MeterRegistry, PushMeterRegistry, 或者 StepMeterRegistry來創建定制化的 Registry。

// 自定義registry config
public interface CustomRegistryConfig extends StepRegistryConfig {

  CustomRegistryConfig DEFAULT = k -> null;

  @Override
  default String prefix() {
    return "custom";
  }

}


// 自定義registry
public class CustomMeterRegistry extends StepMeterRegistry {

  public CustomMeterRegistry(CustomRegistryConfig config, Clock clock) {
    super(config, clock);

    start(new NamedThreadFactory("custom-metrics-publisher"));
  }

  @Override
  protected void publish() {
    getMeters().stream().forEach(meter -> System.out.println("Publishing " + meter.getId()));
  }

  @Override
  protected TimeUnit getBaseTimeUnit() {
    return TimeUnit.MILLISECONDS;
  }

}

/**
 *
 */
@Configuration
public class MetricsConfig {

  @Bean
  public CustomRegistryConfig customRegistryConfig() {
    return CustomRegistryConfig.DEFAULT;
  }

  @Bean
  public CustomMeterRegistry customMeterRegistry(CustomRegistryConfig customRegistryConfig, Clock clock) {
    return new CustomMeterRegistry(customRegistryConfig, clock);
  }

}


Meters

Micrometer 支持多種類型的度量器,包括Timer, Counter, Gauge, DistributionSummary, LongTaskTimer, FunctionCounter, FunctionTimer以及TimeGauge

在 Micrometer 中,通過名稱和維度(dimensions,也可以稱為"tags",即 API 中的Tag標簽)來唯一確定一種meter。引入維度的概念便於我們對某一指標數據進行更細粒度的拆分研究。

Naming Meters

每種監控系統都有自己的命名風格,不同系統間的命名規則可能是不兼容的。Micrometer 采用的命名約定是通過.來分隔小寫單詞。在 Micrometer 中,針對每種監控系統的不同實現都會將這種.分隔單詞的命名風格轉換為各個監控系統推薦的命名約定,同時也會去除命名中禁止出現的特殊字符。

// Micrometer naming convention
registry.timer("http.server.requests");

// Prometheus naming convention
registry.timer("http_server_requests_duration_seconds");

// Atlas naming convention
registry.timer("httpServerRequests");

// Graphite naming convention
registry.timer("http.server.requests");

// InfluxDB naming convention
registry.timer("http_server_requests");

當然,我們可以通過實現NamingConvention接口來覆蓋默認的命名約定規則:

registry.config().namingConvention(myCustomNamingConvention);

Tag Naming

對於 Tag 的命名,建議也采用跟 meter 一致的點號分隔小寫單詞的方式,這同樣有助於將命名風格轉換為各個監控系統推薦的命名模式。

推薦寫法

registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

這種命名方式為我們分析數據提供了足夠的上下文語義,設想如果我們只通過 name 分析數據,得到的數據也是有意義的。比如,選擇database.calls,那我們就可以得到針對所有數據庫的訪問情況。接下來如果想要深入分析,就可以通過Tag標簽db來對數據做進一步的篩選。

錯誤示例

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

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

再來看一下上面這種命名方式,此時如果僅僅通過 name 屬性calls來查看數據,得到的是包含了 db 訪問和 http 調用的所有的指標數據。顯然這種數據對於我們分析生產問題來說是毫無意義的,需要進一步選擇class標簽來細化數據維度。

Common Tags

common tags 屬於 registry 級別的 tag,它會被應用到報告給監控系統的所有 metric 中,這類 tag 通常是系統維度的一些屬性,比如 host、instance、region、堆棧信息等等。

registry.config().commonTags("stack", "prod", "region", "us-east-1");
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1"))); // equivalently

common tags 必須在添加任何 meter 之前就被加入到 registry 中。

Tag Values

首先,tag values 不能為空。

除此之外,我們還需要做的就是對 tag 值做規范化,對其可能取值做限制。比如針對 HTTP 請求中的 404 異常響應,可以將這類異常的響應值設置為統一返回NOT_FOUND,否則指標數據的度量維度將會隨着這類找不到資源異常數量的增加而增長,導致本該聚合的指標數據變得很離散。


Meter Filters

Meter Filter 用於控制meter注冊時機、可以發布哪些類型的統計數據,我們可以給每一個 registry 配置過濾器。

過濾器提供以下三個基本功能:

  • 拒絕/接受meter注冊。
  • 變更meter的 ID 信息(io.micrometer.core.instrument.Meter.Id
  • 針對某些類型的meter配置分布統計。
registry.config()
    // 多個filter配置按順序生效
    .meterFilter(MeterFilter.ignoreTags("too.much.information"))
    .meterFilter(MeterFilter.denyNameStartsWith("jvm"));

拒絕/接受Meters

用於配置只接受指定形式的meters,或者屏蔽某些meters

new MeterFilter() {
    @Override
    public MeterFilterReply accept(Meter.Id id) {
       if(id.getName().contains("test")) {
          return MeterFilterReply.DENY;
       }
       return MeterFilterReply.NEUTRAL;
    }
}


public enum MeterFilterReply {
    // 拒絕meter注冊請求,registry將會返回一個該meter的NOOP版本(如NoopCounter、NoopTimer)
    DENY,

    // 當沒有任何過濾器返回DENY時,meter的注冊流程繼續向前推進
    NEUTRAL,

    // 表示meter注冊成功,無需繼續向下流轉“詢問”其他filter的accept(...)方法
    ACCEPT
}


針對Meter的 deny/accept 策略, MeterFilter為我們提供了一些常用的方法:

  • accept():接受所有的meter注冊,該方法之后的任何 filter 都是無效的。
  • accept(Predicate<Meter.Id>):接收滿足給定條件的meter注冊。
  • acceptNameStartsWith(String):接收 name 以指定字符打頭的meter注冊。
  • deny():拒絕所有meter的注冊請求,該方法之后的任何 filter 都是無效的。
  • denyNameStartsWith(String):拒絕所有 name 以指定字符串打頭的meter的注冊請求。
  • deny(Predicate<Meter.Id>):拒絕滿足特定條件的meter的注冊請求。
  • maximumAllowableMetrics(int):當已注冊的meters數量達到允許的注冊上限時,拒絕之后的所有注冊請求。
  • maximumAllowableTags(String meterNamePrefix, String tagKey, int maximumTagValues, MeterFilter onMaxReached):設置一個tags上限,達到這個上限時拒絕之后的注冊請求。
  • denyUnless(Predicate<Meter.Id>):白名單機制,拒絕不滿足給定條件的所有meter的注冊請求。

變更Meter的 ID 信息

new MeterFilter() {
    @Override
    public Meter.Id map(Meter.Id id) {
       if(id.getName().startsWith("test")) {
          return id.withName("extra." + id.getName()).withTag("extra.tag", "value");
       }
       return id;
    }
}

常用方法:

  • commonTags(Iterable<Tag>):為所有指標添加一組公共 tags。通常建議開發者為應用程序名稱、host、region 等信息添加公共 tags。
  • ignoreTags(String…​):用於從所有meter中去除指定的 tag key。比如當我們發現某個 tag 具有過高的基數,並且已經對監控系統構成壓力,此時可以在無法立即改變所有檢測點的前提下優先采用這種方式來快速減輕系統壓力。
  • replaceTagValues(String tagKey, Function<String, String> replacement, String…​ exceptions):替換滿足指定條件的所有 tag 值。通過這種方式可以某個 tag 的基數大小。
  • renameTag(String meterNamePrefix, String fromTagKey, String toTagKey):重命名所有以給定前綴命名的metric的 tag key。

配置分布統計信息

new MeterFilter() {
    @Override
    public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
        if (id.getName().startsWith(prefix)) {
            return DistributionStatisticConfig.builder()
                    // ID名稱以指定前綴開頭的請求提供指標統計直方圖信息
                    .publishPercentiles(0.9, 0.95)
                    .build()
                    .merge(config);
        }
        return config;
    }
};

速率聚合

速率聚合可以在指標數據發布之前在客戶端完成,也可以作為服務器查詢的一部分在服務端臨時聚合。Micrometer 可以根據每種監控系統的風格

並不是所有的指標都需要被視為一種速率來發布或查看。例如,gauge值或者長期定時任務中的活躍任務數都不是速率。

服務端聚合

執行服務端速率計算的監控系統期望能在每個發布間隔報告計數絕對值。例如,從應用程序啟動開始 counter 計數器在每個發布間隔產生的所有增量的絕對計數和。

當服務重啟時 counter 的計數值就會降為零。一旦新的服務實例啟動成功,速率聚合圖示曲線將會返回到 55 左右的數值。

下圖表示的是一個沒有速率聚合的 counter,這種計數器幾乎沒什么用,因為它反映的只是 counter 的增長速度隨時間的變化關系。

通過以上圖示對比可以發現,如果在實際生產環境中,我們實現了零停機部署(例如紅黑部署),那么就可以通過設定速率聚合曲線的最小報警閾值來實現服務異常監測(零停機部署環境下無需擔心因服務重啟導致 counter 計數值下降)。

客戶端聚合

在實際應用中,有以下兩類監控系統期望客戶端在發布指標數據之前完成速率聚合。

  • 期望得到聚合數據。生產環境中大多數情況下我們都需要基於服務指標的速率作出決策,這種情況下服務端需要做更少的計算來滿足查詢要求。
  • 查詢階段只有少量或者根本沒有數學計算允許我們做速率聚合。對於這些系統,發布一個預先聚合的數據是非常有意義的事情。

Micrometer 的Timer會分別記錄count值和totalTime值。比如我們配置的發布間隔是 10s,然后有 20 個請求,每個請求的耗時是 100ms。那么,對於第一個時間區間來說:

  1. count = 10 seconds * (20 requests / 10 seconds) = 20 requests;
  2. totalTime = 10 seconds _ (20 _ 100 ms / 10 seconds) = 2 seconds。

count統計表示的是服務的吞吐量信息,totalTime表示的是整個時間區間內所有請求的總耗時情況。

totalTime / count = 2 seconds / 20 requests = 0.1 seconds / request = 100 ms / request 表示的是所有請求的平均時延情況。


指標類型

Counters

Counters 用於報告一個單一的計數指標。Counter 接口允許按照一個固定正向值遞增。

當使用counter構建圖表和報警時,通常我們最感興趣的是事件在給定的時間間隔內發生的速率。例如給定一個隊列,我們可以使用counter度量數據項寫入隊列以及從隊列中移除的速度。

Normal rand = ...; // a random generator

MeterRegistry registry = ...
Counter counter = registry.counter("counter"); (1)

Flux.interval(Duration.ofMillis(10))
        .doOnEach(d -> {
            if (rand.nextDouble() + 0.1 > 0) { (2)
                counter.increment(); (3)
            }
        })
        .blockLast();

// counter流式調用
Counter counter = Counter
    .builder("counter")
    .baseUnit("beans") // optional
    .description("a description of what this counter does") // optional
    .tags("region", "test") // optional
    .register(registry);


Gauges

gauge用於獲取當前值。常見的應用場景比如實時統計當前運行的線程數。

gauge對於監測那些具有自然上限的屬性來說比較有用。它不適合用於統計應用程序的請求數,因為請求數會隨着服務生命周期的增加而無限延長。

永遠不要用gauge度量那些本可以使用counter計數的數據。

List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); (1)
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>()); (2)
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

// maintain a reference to myGauge
AtomicInteger myGauge = registry.gauge("numberGauge", new AtomicInteger(0));

// ... elsewhere you can update the value it holds using the object reference
myGauge.set(27);
myGauge.set(11);

還有一種特殊類型的Gauge-MultiGauge,可以一次發布一組 metric。

// SELECT count(*) from job group by status WHERE job = 'dirty'
MultiGauge statuses = MultiGauge.builder("statuses")
    .tag("job", "dirty")
    .description("The number of widgets in various statuses")
    .baseUnit("widgets")
    .register(registry);

...

// run this periodically whenever you re-run your query
statuses.register(
    resultSet.stream()
        .map(result -> Row.of(Tags.of("status", result.getAsString("status")), result.getAsInt("count"))));

Timers

Timer用於度量短時間內的事件時延和響應頻率。所有的Timer實現都記錄了事件響應總耗時和事件總數。Timer不支持負數,此外如果使用它來記錄大批量、長時延事件的話,容易導致指標值數據越界(超過Long.MAX_VALUE)。

public interface Timer extends Meter {
    ...
    void record(long amount, TimeUnit unit);
    void record(Duration duration);
    double totalTime(TimeUnit unit);
}

對於Timer的基本實現(如CumulativeTimerStepTimer)中定義的最大統計值,指的都是一個時間窗口中的最大值(TimeWindowMax)。如果時間窗口范圍內沒有新值記錄,隨着一個新的時間窗口開始,最大值會被重置為零。

時間窗口大小默認是MeterRegistry定義的步長大小,也可以通過DistributionStatisticConfigexpiry(...)方法顯式設置。

/**
 * @return The step size to use in computing windowed statistics like max. The default is 1 minute.
 * To get the most out of these statistics, align the step interval to be close to your scrape interval.
 */
default Duration step() {
    // PrometheusMeterRegistry默認步長一分鍾
    return getDuration(this, "step").orElse(Duration.ofMinutes(1));
}


// 也可以通過DistributionStatisticConfig自定義步長
public class DistributionStatisticConfig implements Mergeable<DistributionStatisticConfig> {
    public static final DistributionStatisticConfig DEFAULT = builder()
            .percentilesHistogram(false)
            .percentilePrecision(1)
            .minimumExpectedValue(1.0)
            .maximumExpectedValue(Double.POSITIVE_INFINITY)
            .expiry(Duration.ofMinutes(2))
            .bufferLength(3)
            .build();

     ...
}


public Builder expiry(@Nullable Duration expiry) {
    config.expiry = expiry;
    return this;
}

Timer.Sample

可以用它來統計方法執行耗時。在方法開始執行之前,通過sample記錄啟動時刻的時間戳,之后當方法執行完畢時通過調用stop操作完成計時任務。

Timer.Sample sample = Timer.start(registry);

// do stuff
Response response = ...

sample.stop(registry.timer("my.timer", "response", response.status()));

@Timed

@Timed可以被添加到包括 Web 方法在內的任何一個方法中,加入該注解后可以支持方法計時功能。

Micrometer 的 Spring Boot 配置中無法識別@Timed

micrometer-core中提供了一個 AspectJ 切面,使得我們可以通過 Spring AOP 的方式使得@Timed注解在任意方法上可用。

@Configuration
public class TimedConfiguration {
   @Bean
   public TimedAspect timedAspect(MeterRegistry registry) {
      return new TimedAspect(registry);
   }
}


@Service
public class ExampleService {

  @Timed
  public void sync() {
    // @Timed will record the execution time of this method,
    // from the start and until it exits normally or exceptionally.
    ...
  }

  @Async
  @Timed
  public CompletableFuture<?> async() {
    // @Timed will record the execution time of this method,
    // from the start and until the returned CompletableFuture
    // completes normally or exceptionally.
    return CompletableFuture.supplyAsync(...);
  }

}

Distribution Summaries

分布式摘要記錄的是事件的分布情況,結構上與Timer類似,但是記錄的並不是一個時間單位中的值。比如,我們可以通過分布式摘要記錄命中服務器的請求負載大小。

通過以下方式可以創建分布式摘要:

DistributionSummary summary = registry.summary("response.size");

DistributionSummary summary = DistributionSummary
    .builder("response.size")
    .description("a description of what this summary does") // optional
    .baseUnit("bytes") // optional (1)
    .tags("region", "test") // optional
    .scale(100) // optional (2)
    .register(registry);

Long Task Timers

長任務計時器是一種特殊的計時器,它允許你在被檢測任務仍在運行時度量時間。普通定時器只在任務完成時記錄其持續時間。

長任務計時器會統計以下數據:

  • 活躍任務數;
  • 所有活躍任務的總持續時間;
  • 活躍任務中的最大持續時間。

Timer不同的是,長任務計時器不會發布關於已完成任務的統計信息。

設想一下這樣的場景:一個后台進程定時將數據庫中的數據刷新的 metadata 中,正常情況下整個刷新任務幾分鍾內就可以完成。一旦服務出現異常,刷新任務可能需要占用較長時間,此時長任務計時器可以用來記錄刷新數據的總活躍時間。

@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {
    // find instances, volumes, auto-scaling groups, etc...
}

如果你所用框架不支持@Timed,可以通過如下方式創建長任務計時器。

LongTaskTimer scrapeTimer = registry.more().longTaskTimer("scrape");
void scrapeResources() {
    scrapeTimer.record(() => {
        // find instances, volumes, auto-scaling groups, etc...
    });
}

還有一點需要注意的是,如果我們想在進程超過指定閾值時觸發報警,當使用長任務定時器時,在任務超過指定閾值后的首次報告間隔內我們就可以收到報警。如果使用的是常規的Timer,只能一直等到任務結束后的首次報告間隔時才能收到報警,此時可能已經過去很長時間了。

Histograms

Timerdistribution summaries 支持收集數據來觀察數據分布占比。通常有以下兩種方式查看占比:

  • Percentile histograms:Micrometer 首先將所有值累積到一個底層直方圖中,之后將一組預定的 buckets 發送到監控系統。監控系統的查詢語言負責計算這個直方圖的百分位。

    目前,只有 Prometheus, Atlas, and Wavefront 支持基於直方圖的百分比近似計算(通過histogram_quantile, :percentile, 和hs())。如果你選擇的監控系統是以上幾種,推薦使用這種方式,因為基於這種方式可以實現跨維度聚合直方圖,並從直方圖中得出可聚合的百分比。

  • Client-side percentiles:由 Micrometer 負責計算每個 meter ID 下的百分比近似值,然后將其發送到監控系統。這種方式顯示沒有Percentile histograms靈活,因為它不支持跨維度聚合百分比近似值。

    不過,這種方式給那些不支持服務器端基於直方圖做百分比計算的監控系統提供了一定程度上的百分比分布情況的洞察能力。

Timer.builder("my.timer")
   .publishPercentiles(0.5, 0.95) // 用於設置應用程序中計算的百分比值,不可跨維度聚合
   .publishPercentileHistogram() // (2)
   .serviceLevelObjectives(Duration.ofMillis(100)) // (3)
   .minimumExpectedValue(Duration.ofMillis(1)) // (4)
   .maximumExpectedValue(Duration.ofSeconds(10))

接入 Prometheus

Prometheus 基於服務發現的模式,定時從應用程序實例上拉取指標數據,它支持自定義查詢的語言以及數學操作。

  1. 接入 Prometheus 時首先需要引入如下的 maven 依賴:

    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-registry-prometheus</artifactId>
      <version>${micrometer.version}</version>
    </dependency>
    
  2. 創建 Prometheus Registry,同時需要給 Prometheus 的 scraper 暴露一個 HTTP 端點用於數據拉取。

    PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    
    try {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/prometheus", httpExchange -> {
            String response = prometheusRegistry.scrape(); (1)
            httpExchange.sendResponseHeaders(200, response.getBytes().length);
            try (OutputStream os = httpExchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });
    
        new Thread(server::start).start();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    
  3. 設置拉取的數據格式。默認情況下PrometheusMeterRegistryscrape()方法返回的是 Prometheus 默認的文本格式。從 Micrometer 1.7.0 開始,也可以通過如下方式指定數據格式為OpenMetrics定義的數據格式:

    String openMetricsScrape = registry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
    
  4. 圖形化展示。將 Prometheus 抓取的指標數據展示到 Grafana 面板中,下圖使用的是官方公開的一種 Grafana dashboard 模板(JVM-dashboard


SpringBoot 中如何使用

  1. Spring Boot Actuator 為 Micrometer 提供依賴項管理以及自動配置。需要先引入以下配置:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
    </dependency>
    
    

    接下來通過MeterRegistryCustomizer配置registry,比如在meter注冊到registry之前配置registry級別的公共標簽屬性。

    @Configuration
    public class MicroMeterConfig {
    
        @Bean
        public MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer() {
            return meterRegistry -> meterRegistry.config().commonTags(Collections.singletonList(Tag.of("application", "mf-micrometer-example")));
        }
    
    
        // Spring Boot中無法直接使用@Timed,需要引入TimedAspect切面支持。
        @Bean
        public TimedAspect timedAspect(MeterRegistry registry) {
            return new TimedAspect(registry);
        }
    }
    
    
    @RequestMapping("health")
    @RestController
    public class MetricController {
    
        @Timed(percentiles = {0.5, 0.80, 0.90, 0.99, 0.999})
        @GetMapping("v1")
        public ApiResp health(String message) {
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return ApiResp.ok(new JSONObject().fluentPut("message", message));
        }
    
    
        @GetMapping("v2")
        @Timed(percentiles = {0.5, 0.80, 0.90, 0.99, 0.999})
        public ApiResp ping() {
            return ApiResp.ok(new JSONObject().fluentPut("message", "OK"));
        }
    }
    
  2. Spring Boot 默認提供了一個/actuator/promethues端點用於服務指標數據拉取,端點暴露的數據中可能包含應用敏感數據,通過以下配置可以限制端點數據暴露(exclude 優先級高於 include 優先級)。

    Property Default
    management.endpoints.jmx.exposure.exclude
    management.endpoints.jmx.exposure.include *
    management.endpoints.web.exposure.exclude
    management.endpoints.web.exposure.include health
  3. 啟動服務,訪問http://localhost:8800/actuator/prometheus可以看到以下服務指標數據:

  4. 接下來就可以配置 Prometheus 了,在 prometheus.yml 中加入以下內容:

    # my global config
    global:
      scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
      evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
      # scrape_timeout is set to the global default (10s).
    
    # Alertmanager configuration
    alerting:
      alertmanagers:
      - static_configs:
        - targets:
          # - alertmanager:9093
    
    # Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
    rule_files:
      # - "first_rules.yml"
      # - "second_rules.yml"
    
    # A scrape configuration containing exactly one endpoint to scrape:
    # Here it's Prometheus itself.
    scrape_configs:
      # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
      - job_name: 'mf-micrometer-example'
        scrape_interval: 5s
        metrics_path: '/actuator/prometheus'
        static_configs:
          - targets: ['127.0.0.1:8800']
            labels:
               instance: 'mf-example'
    

    訪問Prometheus控制台(http://localhost:9090),Targets頁面中可以看到當前連接到這台Prometheus的所有客戶端及其狀態。

    同時,在Graph界面可以通過查詢語句查詢指定條件的指標數據:

  5. 到這一步,我們已經完成了服務指標數據度量及抓取的工作。最后,我們需要將 Prometheus 抓取的數據做圖形化展示,這里我們使用 Grafana。

    1. 首先創建數據源,Grafana 支持多種數據源接入,此處我們選擇 Prometheus。

    2. 創建 Dashboard,可以自定義,也可以使用官方發布的一些模板,比如4701模板。導入模板后選擇我們剛才創建好的數據源即可。

      可以看到這是指標數據圖形化展示的結果,可以非常直觀地看到服務調用量。

其他問題

批處理作業指標抓取

除此之外,針對臨時性或者批處理作業,他們執行的時間可能不夠長,使得 Prometheus 沒法抓取指標數據。對於這類作業,可以使用 Prometheus Pushgateway 主動推送數據到 Prometheus(PrometheusPushGatewayManager用於管理將數據推送到 Prometheus)。

<dependency>
    <groupId>io.prometheus</groupId>
    <artifactId>simpleclient_pushgateway</artifactId>
</dependency>

使用 pushgateway 需要同時設置management.metrics.export.prometheus.pushgateway.enabled=true

關於@Timed

前文中我們提到官方文檔中說 Spring Boot 中無法直接使用@Timed,需要引入TimedAspect切面支持。但是經過實際測試發現,對於 SpringMVC 請求,不引入TimedAspect也可以記錄接口調用耗時。

通過分析源碼可以發現,Spring Boot Actuator 中有一個WebMvcMetricsFilter類,這個類會對請求做攔截,其內部會判斷接口所在方法、類上是否加了@Timed

public class WebMvcMetricsFilter extends OncePerRequestFilter {

    private void record(WebMvcMetricsFilter.TimingContext timingContext, HttpServletRequest request, HttpServletResponse response, Throwable exception) {
      Object handler = this.getHandler(request);
      // 查找類、方法上的@Timed注解
      Set<Timed> annotations = this.getTimedAnnotations(handler);
      Sample timerSample = timingContext.getTimerSample();
      if(annotations.isEmpty()) {
        if(this.autoTimer.isEnabled()) {
          // 未加@Timed,使用默認配置構造Timer。此處metricName="http.server.requests"
          Builder builder = this.autoTimer.builder(this.metricName);
          timerSample.stop(this.getTimer(builder, handler, request, response, exception));
        }
      } else {
        Iterator var11 = annotations.iterator();

        while(var11.hasNext()) {
          Timed annotation = (Timed)var11.next();
          // 方法上有@Timed,構造timer builder,此處metricName="http.server.requests"
          Builder builder = Timer.builder(annotation, this.metricName);
          timerSample.stop(this.getTimer(builder, handler, request, response, exception));
        }
      }

    }


   /**
     * 先查找方法中是否存在@Timed注解,如果方法上沒有,則繼續在類上查找
     */
    private Set<Timed> getTimedAnnotations(HandlerMethod handler) {
        Set<Timed> methodAnnotations = this.findTimedAnnotations(handler.getMethod());
        return !methodAnnotations.isEmpty()?methodAnnotations:this.findTimedAnnotations(handler.getBeanType());
    }

}

所以,基於以上分析,我們去掉代碼中的TimedAspect,然后再次查看指標數據統計情況:

加入TimedAspect后的指標數據統計情況,可以看到同時記錄了method_timed_secondshttp_server_requests_seconds兩種名稱的指標數據。並且這兩種統計方式顯示的接口耗時有一定誤差,從執行流程上來看,使用TimedAspect方式計算耗時更接近方法本身邏輯執行占用時間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM