コンテンツへスキップ
戻る

バックエンド屋の性能最適化 実戦(2):Spring Boot Actuator

公開日:  at  08:00 午前

前置き

ナレーション:システムを買いに来た人がいた。

(バイクの甲高いブレーキ音)

あなたの上司:おい、このシステムって何 RPS まで耐えられるんだ?

あなた:一日 5000RPS です。

あなたの上司:What’s up… お前のシステムの API は金でできてるのか、それとも外部モジュールが金でできてるのか?

そう問われて、あなたは何も言い返せなくなる。「これは量産型のシステムなんだから大目に見てくれ」と言いたいところだが、あなたの上司もそう簡単に丸め込める相手ではない。もっと情報が必要だ。自分のシステムがどう動いているか把握していないことは、疑いようもなく致命的だ。システムアーキテクチャに加えて、性能最適化を進めるときには、システムの現在の稼働状況を測定する手段が必要になる。そして今回取り上げるのが、その手段としての Spring Actuator だ。

Spring Actuator とは?

Spring Boot ActuatorSpring Boot 公式が提供する**本番環境(Production)**の監視・管理モジュールです。その核心的な価値は、依存関係を一つ追加して少し設定するだけで、Spring Boot が自動的にアプリケーションの内部状態を一連の直接アクセス可能な HTTP endpoints(あるいは JMX 経由)として公開してくれる点にあります。

使うには、まず spring-boot-starter-actuator という依存関係を導入します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

依存関係を追加しただけで何も設定しない場合、Actuator はデフォルトで最も無害な /actuator/health/actuator/info の二つの endpoint しか公開しません。 しかし詳細に観測したい場合は、どの指標を公開するかを一つひとつ手動で設定する必要があります。

management:
  endpoints:
    web:
      exposure:
        include: health, metrics, env, beans, loggers, threaddump, prometheus
      base-path: /actuator # デフォルトは /actuator なので省略可

  endpoint:
    health:
      show-details: always # デフォルトは never。そうしないと /health は {"status":"UP"} しか返さず詳細が見えない
    shutdown:
      enabled: false # これは危険なので、安易に有効化しないこと

  server:
    port: 9090 # 業務 API と分離するため、専用ポートを推奨。ネットワーク隔離がしやすくなる

  metrics:
    tags:
      application: ${spring.application.name} # Prometheus が取得する metrics に識別性を持たせる

設定が終われば、システムの状態を照会できる一連の endpoint が揃います。以下は特に頻繁に使う、ほぼ毎日お世話になるものです。

Endpoint用途
/actuator/healthアプリケーションのヘルス状態(DBRedis、ディスク容量など)。KubernetesLiveness/Readiness Probe としてよく使われる
/actuator/metrics各種性能指標(JVM MemoryHTTP Request 数、GC 回数など)。前回の記事で扱った指標の実際の出どころでもある
/actuator/env現在の環境変数と設定値。トラブルシューティングにとても役立つ
/actuator/beansすべての Spring Bean の一覧。あるビーンが正しくロードされているかを確認できる
/actuator/loggerslog level を動的に変更できる。サービスを再起動せずに、あるパッケージの DEBUG を一時的に有効化できる
/actuator/threaddumpThread DumpDeadlock のトラブルシューティングや「どの Thread が詰まっているか」を調べるのに便利
/actuator/prometheusPrometheus が取得する形式でデータを出力する(Micrometer が必要)。この記事の後半で可視化に使う

一つ注意点を。Actuator は開けば開くほど良いというものではありません。これらの endpoint はほぼすべて機密性の高い情報であり、漏洩すれば攻撃者にシステムを丸裸にされてしまいます。 そのため本番環境では通常、独立した management.server.port を使って Actuator を業務 API から切り離し、さらにファイアウォールや Spring Security と組み合わせて、社内ネットワークや特定の身元からのみアクセスできるように制限します(この部分は後ほど実際にハンズオンで行います)。

閲覧用の指標だけでなく、Actuator には直接システムの挙動を変更できる endpoint もわずかながら存在します。最も典型的な例が /actuator/loggers です。サービスを再起動せずに、あるパッケージの log level を一時的に引き上げ、調査が終わったら元に戻すことができます。

# com.example.demo パッケージの現在の log level を照会する
curl http://localhost:9090/actuator/loggers/com.example.demo
# com.example.demo.DemoController クラスの現在の log level を照会する
curl http://localhost:9090/actuator/loggers/com.example.demo.DemoController

# DEBUG に動的変更。即座に反映され、再起動不要
curl -X POST http://localhost:9090/actuator/loggers/com.example.demo \
  -H "Content-Type: application/json" \
  -d '{"configuredLevel": "DEBUG"}'

この「現場救急」的な能力は、本番環境がエラーを吐いて再起動もできないような状況で特に役立ちます。まず DEBUG を有効にして問題をはっきり確認し、確認できたら INFO に戻す——再デプロイなしで一連の対応が完結します。

これらの指標は初めて触れる人からすると錯綜として複雑に感じられるでしょう。そこで、もっと手軽にこれらの指標を確認する方法が必要になります。それが Prometheus と Grafana です。

Prometheus とは?

Prometheus はオープンソースの**時系列データベース(Time Series Database)**であり、同時に監視システムの事実上の標準の一つでもあります。その動作モデルは **Pull(取得)**方式です——アプリケーション側がデータを能動的に送りつけるのではなく、Prometheus が一定間隔(例えば 15 秒ごと)で各サービスのある endpoint に自ら取りに行き、現時点の指標をタイムスタンプ付きのレコードとして保存します。

その「ある endpoint」というのが、先ほどの表に出てきた /actuator/prometheus です。ただし spring-boot-starter-actuator だけでは足りません。この endpoint はデフォルトでは存在せず、MicrometerSpring Boot 内蔵の指標ファサード)がデータを Prometheus が理解できる形式に変換できるよう、依存関係をもう一つ追加する必要があります。

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

追加後に /actuator/prometheus を叩くと、このようなプレーンテキスト形式のデータがずらりと出てきます。

http_server_requests_seconds_bucket{uri="/api/v1/ticket",le="0.1",} 823.0
http_server_requests_seconds_bucket{uri="/api/v1/ticket",le="0.5",} 910.0
http_server_requests_seconds_count{uri="/api/v1/ticket",} 915.0
http_server_requests_seconds_sum{uri="/api/v1/ticket",} 42.183

次に、このデータをどこから取得すればよいかを Prometheus に教えます。これは Prometheus 自身の設定ファイル prometheus.yml で行います。

scrape_configs:
  - job_name: "ticket-service"
    metrics_path: "/actuator/prometheus"
    scrape_interval: 15s
    static_configs:
      - targets: ["localhost:9090"] # あなたの management.server.port

設定を終えて Prometheus が動き出すと、15 秒ごとに /actuator/prometheus を叩いて現時点の数値を自身の時系列データベースに保存してくれます。これにより PromQLPrometheus 専用のクエリ言語)を使って、過去 5 分間の P95 レイテンシ、ある APIQPSJVM のメモリ使用量の推移といった各種分析を後から行えるようになります。

Grafana とは?

Prometheus が「データを収集し、保存し、照会できるようにする」役割を担うなら、Grafana が担うのは「照会したデータを人が理解できるグラフに描き起こす」役割です。Grafana 自体はデータを保存せず、PrometheusData Source として接続し、PromQL でクエリを書いて、その結果を折れ線グラフやダッシュボード、アラートルールに変換します。

具体例を挙げましょう。/api/v1/ticket という APIP95 レイテンシ(95% のリクエストがこの時間以内に収まる)を知りたいとします。Grafana ではこのような PromQL を書くことになります。

histogram_quantile(0.95, sum by (le) (rate(
  http_server_requests_seconds_bucket{uri="/api/v1/ticket"}[5m])))

分解して見てみましょう。

描画されるグラフでは、X 軸は時間(HH:MM:SS、デフォルトはブラウザのローカルタイムゾーン)です。Y 軸の単位には特に注意が必要です。メトリクス名が http_server_requests_seconds_bucket であり、途中の seconds が単位になっているため、Y 軸の 0.15 は **0.15 秒(150 ミリ秒)**を指しており、「150」ではありません。秒表示が直感的でないと感じる場合は、Grafana のパネル設定で Unitseconds (s) に変更すれば、自動的に読みやすい ms 表示に変換してくれます。

一般的に、API レイテンシの P95 はこのようなおおまかな基準で判断できます。

P95 レイテンシ評価
< 100ms良好
100〜300ms普通、最適化の余地あり
> 500ms遅め、要調査

数値そのものより重要なのは傾向です。短時間で P95 レイテンシが単発の急上昇ではなく持続的に上昇していく様子が見えた場合、それは通常どこかのリソースが押し潰されつつあることを意味します——コネクションプールが満杯になっている、キャッシュの失効でリクエストがすべて DB に向かっている、あるいは JVM が頻繁な GC に陥っている、などです。そんなときは、いくつかの折れ線を追加で引いて突き合わせてみると良いでしょう。

# 同じ時間帯の QPS。単純にトラフィックが増えただけかどうかを切り分ける
sum(rate(http_server_requests_seconds_count{uri="/api/v1/ticket"}[5m]))

# HikariCP のコネクションプール使用率。コネクションが枯渇していないか確認する
hikaricp_connections_active / hikaricp_connections_max

# JVM の GC 一時停止時間。GC がレイテンシ急上昇の原因になっていないか確認する
rate(jvm_gc_pause_seconds_sum[5m])

QPS に目立った変化がないのにレイテンシだけが上がり続けているなら、原因はほぼ間違いなくコネクションプールかキャッシュにあり、単純なトラフィック過多ではありません——これこそ Metrics の可視化が持つ本当の価値です。深夜に障害対応を叩き起こされる前に、その予兆に気づけるということです。

その他のよく使う指標

このあたりは数が多いので、以下によく使うものをいくつか挙げておきます。

# 同じ時間帯の QPS。単純にトラフィックが増えただけかどうかを切り分ける
sum(rate(http_server_requests_seconds_count{uri="/api/v1/ticket"}[5m]))

# HikariCP のコネクションプール使用率。コネクションが枯渇していないか確認する
hikaricp_connections_active / hikaricp_connections_max

# JVM の GC 一時停止時間。GC がレイテンシ急上昇の原因になっていないか確認する
rate(jvm_gc_pause_seconds_sum[5m])

# CPU 使用率(この JVM プロセス自身がどれだけ CPU を消費しているか)
process_cpu_usage * 100

# CPU 使用率(マシン全体レベル)。上のものと混同しないこと。「誰が」食っているのかを区別するのが重要
system_cpu_usage * 100

# JVM Heap Memory の使用率。満杯になりそうか、-Xmx を調整すべきかを確認する
sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"}) * 100

# GC の世代別(Eden、Old Gen など)に Memory 使用量を見る。メモリリークの調査に有用
jvm_memory_used_bytes{area="heap"}

# JVM の生存スレッド数。急増している場合、たいてい Thread Pool の設定ミスか Thread リークが原因
jvm_threads_live_threads

# ディスクの空き容量比率。低すぎると /actuator/health が直接 DOWN を返してしまう
disk_free_bytes / disk_total_bytes * 100

# HTTP 5xx エラー率。レイテンシと合わせて見ることで、システムが「遅い」のか「壊れている」のかを判断できる
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
  / sum(rate(http_server_requests_seconds_count[5m])) * 100

これらの指標は非常に数が多いので、私のおすすめは「カンペとして保存しておき、必要なときに引っ張り出す」というやり方です。全部暗記する必要はありません。

まとめると——Spring Actuator + Grafana + Prometheus をあわせて使うことで、システムのボトルネックがどこにあるのかを突き止められるようになります。次回は実際のシナリオを持ち込み、いくつかの実践的なケースを紹介していきます。今回はここまでです。


修正を提案する
この記事をシェアする:

前の記事
バックエンド屋の性能最適化 実戦(3):Grafana と Prometheus のクエリ実例
次の記事
バックエンド屋の性能最適化 実戦(1):問題を観測する