2018/10/31 の Spring Festで『Micrometer/Prometheusによる大規模システムモニタリング 〜ヤフーインターネット広告システムでの導入事例〜』というセッションを聴講し、興味を持ったので調べました。
Micrometerは、Spring Boot Actuatorが内部で利用しているライブラリです。
参考 Spring Boot 2.0 Release Notes Micrometer
対象バージョン
- Spring Boot 2.1.0.RELEASE
- Micrometer Core 1.1.0
Micrometerとは?
モニタリングシステムに依存せずに、メトリクスの取得を透過的に行う仕組み。 以下の機能を提供する。
- メトリクスの収集
- モニタリングシステムへの通知

メトリスクの収集(micrometer-core)とモニタリングシステムへの通知(micrometer-registry-xxx)が切り離されているので、モニタリングシステムを入れ替えることができる。
この点は、micrometer.ioにも以下のように記載されている。
Micrometer provides a simple facade over the instrumentation clients for the most popular monitoring systems, allowing you to instrument your JVM-based application code without vendor lock-in. Think SLF4J, but for metrics.
参考 micrometer.io
収集できるメトリクス
Micrometer + Spring Boot で収集できるメトリクスは以下の通り。
JVM metrics, report utilization of:
- Various memory and buffer pools
- Statistics related to garbage collection
- Threads utilization
- Number of classes loaded/unloaded
CPU metrics
File descriptor metrics
Kafka consumer metrics
Log4j2 metrics: record the number of events logged to Log4j2 at each level
Logback metrics: record the number of events logged to Logback at each level
Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
Tomcat metrics
Spring Integration metrics
参考 Spring Boot Reference Guide 57.3 Supported Metrics
Spring Boot + Micrometerの場合、spring-boot-actuatorでFilterを利用した拡張のメトリクスが設定されている。そのため、Micrometer単体で使うよりも、多くのメトリクス(APIごとのレスポンスタイムなど)が取得できる。詳細はリファレンスを参照。
対応しているモニタリングシステム
以下のモニタリングシステムに対応している。
- AppOptics
- Atlas
- Datadog
- Dynatrace
- Elastic
- Ganglia
- Graphite
- Humio
- Influx
- JMX
- KairosDB
- New Relic
- Prometheus
- SignalFx
- Simple (in-memory)
- StatsD
- Wavefront
参考 Spring Boot Reference Guide 57. Metrics
なぜCPU metrics等の情報が収集できるのか?
Javaライブラリなのでアプリケーションレベルの情報しか取得できない。 と思いきや、OSレベルの情報(CPU metricsなど)が取得できる。なぜか?
これは、MXBeanという仕組みを利用している。
参考 MXBean から取得できる情報あれこれ
例えば、以下のように物理メモリの情報を取得できる。
import com.sun.management.OperatingSystemMXBean;
...
public static void main(String[] args) {
  OperatingSystemMXBean mxBean =(OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
  System.out.println("物理メモリ(合計): " + mxBean.getTotalPhysicalMemorySize());
  System.out.println("物理メモリ(空き): " + mxBean.getFreePhysicalMemorySize());
}
物理メモリ(合計): 16848523264
物理メモリ(空き): 2480197632
また、アプリケーションレベルのメトリクスは、各種ミドルウェアのAPIやAspectJを利用して収集している様子。また、Spring Boot + Spring MVCと組み合わせて使う場合はFilterも利用する様子。
どのように動作しているのか?
モニタリングシステムへの通知方法
対象のモニタリングシステムの収集方法(Push型とPull型)によって異なる。
- Push型の場合、モニタリングシステムにリクエストが投げられる。
- Pull型の場合、エンドポイントが用意される。 (Prometheusだと、以下のように /actuator/prometheusが公開される)

メトリクスの収集方法
まず、中心となるクラスを押さえる。
Meter
メトリクスの集合を表すクラス。 収集方法ごとにクラスがある。(Timer、Counter、Gaugeなど) 各Meterの違いは Micrometer リファレンス を参照してください。
MeterRegistry
Meterを管理するクラス。 モニタリングシステムごとに実装がある。(PrometheusMeterRegistryなど)
MeterBinder
メトリクスを収集する方法を登録するクラス。 収集対象ごとにクラスがある。(UptimeMetrics、JvmGcMetricsなど)
例えば、UptimeMetricsでは以下のように実装されている。
public class UptimeMetrics implements MeterBinder {
  ...
  @Override
  public void bindTo(MeterRegistry registry) {
    TimeGauge.builder("process.uptime", runtimeMXBean, TimeUnit.MILLISECONDS, RuntimeMXBean::getUptime)
      .tags(tags)
      .description("The uptime of the Java virtual machine")
      .register(registry);
    ...
}
処理の流れ
Spring Boot + Prometheus では、以下のようになっていた。 (ざっくりとした図なので、正確ではありません)
- 初期化
- MeterRegistryConfigurerがMeterRegistryをSpringの管理Beanから取り出す
- MeterRegistryConfigurerがMeterBinderにMeterRegistryを渡す
- MeterBinderはMeterRegistryにMeterを登録する
- MeterRegistryはモニタリングシステムごとの初期化を行う
- PrometheusMeterRetigstyの場合は、MicrometerCollectorにMeter(MeterによってはMeterBinderから受け取ったハンドラを含む)を登録しておく
- PrometheusScrapeEndpointはエンドポイントを公開する
- エンドポイントアクセス時
- エンドポイントにアクセスしてきたら、CollectorRegistryを通してMicrometerCollectorにアクセスしてMeterを取得する
- Meterを取得するときにMeterBinderで指定したハンドラが実行される

メトリクスを絞る
設定でメトリクスを絞ることができる。
# jvmから始まるメトリクスを取得しない
management.metrics.enable.jvm = false
独自メトリクスを設定する
例として、C直下のlogsディレクトリにあるファイルとディレクトリの合計数をメトリクスとして収集する。
@Component
public class LocalFileMetrics {
  LocalFileMetrics(MeterRegistry meterRegistry) {
    meterRegistry.gauge("custom.metrics.local.file.number", this, LocalFileMetrics::invoke);
  }
  public Double invoke() {
    try {
      return Double.valueOf(Files.list(Paths.get("C:\\logs")).count());
    } catch (IOException e) {
      return Double.NaN;
    }
  }
}
動作確認する
Spring Boot + Prometheus を利用する。 Prometheusでデータ収集したものを、Grafanaを利用して可視化する。
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <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>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
    </dependencies>
アプリケーションの実装
Mavenの設定だけでアプリケーションとしては、何も実装することなし!
ただし、メトリクスのエンドポイントはデフォルトでは公開されていない。 そのため、公開設定をする。
参考 Spring Boot reference 53.2 Exposing Endpoints 参考 Spring Boot 2.0のActuator、とりあえず動かすために知っておきたい変更点3つ
application.properties
# 全開け(ガバガバセキュリティ)
management.endpoints.web.exposure.include=*
# Grafanaのダッシュボードで使うTag
management.metrics.tags.application=sample-app

Prometheus/Grafanaの構築
構築には Docker と Docker-Compose を利用する。
参考 Prometheus Docker Install reference
参考 Grafana Docker Install reference
docker-compose.yml
version: '3'
services:
  prometheus:
    image: prom/prometheus
    volumes:
      # 設定ファイルを設定する
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      # ロケールを合わせる
      - /etc/localtime:/etc/localtime:ro 
      # 収集したデータを永続化する
      - prometheus-data:/prometheus-data
    ports:
      - 9090:9090
  grafana:
    image: grafana/grafana
    volumes:
      # ロケールを合わせる
      - /etc/localtime:/etc/localtime:ro 
      # Grafanaの設定類を永続化する
      - grafana-data:/var/lib/grafana 
    ports:
      - 3000:3000
    env_file:
      # 環境変数を設定してGrafanaの設定を変更する
      - ./grafana.env 
volumes: 
  grafana-data:
  prometheus-data:
Prometheusの収集先を指定する。
prometheus.yml
scrape_configs:
  - job_name: 'spring'
    metrics_path: '/actuator/prometheus'
    static_configs:
    - targets: ['192.168.11.104:8080']
Grafanaは環境変数で設定を変えられるため、適切な設定を入れる。
参考 Grafana Configuration
grafana.env
# 初期パスワード設定するだけ
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=secret
Prometheus と Grafana を起動する。
]$ docker-compose up -d
9090 ポートにアクセスすると、メトリクスの収集が開始していることがわかる。

3000 ポートにアクセスすると、ログイン画面が表示される。 grafana.envで設定したアカウントでログインする。

まず、Add data source を押下する。

Data Source として、先ほどのPrometheusを登録する。(あくまでもデータを持つのはPrometheus側。クエリを発行してグラフに必要な情報を取得する。)

Grafanaは他の人が作ったDashBoardを共有することができる。 今回は、Micrometerのリファレンスで紹介されているJVM (Micrometer)を利用する。
参考 Grafana Dashboard、 Micrometer Prometheus
Dashboard > Manage > Import からインポートする。

すると、以下のようにダッシュボードが表示できる。

Grafanaはアラート設定ができるため、運用に応じて設定する。 Grafana Alerting Engine & Rules Guide
注意点 Micrometerのバージョンによっては、メトリクスの名前が変更されている。そのため、インポートしたDashboardがそのまま使えないことがある。 実際に、今回利用した JVM (Micrometer)のDashboard も、メトリクス名がちょこちょこ違っていた。ダッシュボードは使うまえに一通り確認したほうが良い。
最後に
Micrometerで楽にアプリケーションのモニタリングができる!
疑問点
- 必要なメトリクスを全部取れているんだろうか? - メインはアプリケーションレベルのメトリクス(Javaから取得できるレベルのメトリクス、特にレスポンスタイムとか)
- プロセス数とか、ネットワークトラフィックとか、そういうOSに近い情報はどこまで取得できるのか(JavaからRuntime#execを叩けば、なんだって取得できますね…冗談です)
 
- ZabbixやNagiosなどのOSのモニタリングツールも併用する? - PaaS 使ってれば基盤意識しなくていいいんだけどなぁ
- 運用の経験がないので、よくわからない(誰か教えてください)