关于java:服务性能监控之Micrometer详解

14次阅读

共计 20586 个字符,预计需要花费 52 分钟才能阅读完成。

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);

还有一种非凡类型的GaugeMultiGauge,能够一次公布一组 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。
  1. 创立 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 形式计算耗时更靠近办法自身逻辑执行占用工夫。


欢送关注我的微信公众号:【高高木】。第一工夫浏览最新教训分享,一起交换成长。

正文完
 0