LINE の Business Platform 開発担当フェローの Matsuno です。
今回は Spring Boot でアプリケーションを開発した場合のメトリクスの勘所についてご紹介しようと思います。
我々のチームでは Kotlin + Spring Boot での開発がデファクトスタンダードとなっているのですが、正直まだまだこのテクニカルスタックで開発しているエンジニアは日本では少ないのです。そこで、実際の運用の雰囲気を感じていただければと思いまして今回の記事を書くことにしました。
メトリクス取得の基本
我々のチームではメトリクスの格納先として Prometheus を利用しています。
Prometheus で格納したデータを元にアラートを発出したり、grafana でレンダリングしたりできるので便利です。後からクエリでデータを集計できるので、柔軟な運用が可能となっています。
Prometheus にデータを収集してもらうには Prometheus の形式でテキストデータを出力する HTTP のエンドポイントがあれば良いです。
具体的には以下のようなフォーマットで出力します。Java はスレッドベースのプログラミング言語なので、アプリケーション固有のメトリクスを JVM 内部で集計することは容易です。 我々はどんどんアプリケーションのメトリクスを Prometheus に入れています。
# HELP tomcat_sessions_alive_max_seconds
# TYPE tomcat_sessions_alive_max_seconds gauge
tomcat_sessions_alive_max_seconds 0.0
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of minor GC",cause="G1 Evacuation Pause",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="G1 Evacuation Pause",} 0.004
基本的なメトリクスの収集
OS の基本的なメトリクス、つまりロードアベレージや CPU 使用率などは Prometheus 公式の node_exporter で取得しています。
その他に、メジャーなミドルウェアでは nginx 用の nginx_exporter のように、prometheus のためのデータ取得エージェントが OSS で提供されていますので、これも活用しています。
自分たちで開発している場合は jmx_exporter は避ける
Prometheus 公式が出している jmx_exporter というミドルウェアがあります。JMX を通じて対象の Java プロセスの外部からとれて非常に便利です。
我々が Prometheus を導入しはじめた初期には jmx_exporter を利用してメトリクスの取得をしていた時期もあったのですが、JMX から Prometheus にマッピングする YAML を書くのが面倒であること、Java プロセスがもう一個立ち上がることによるオーバーヘッドがあることから最近では積極的に利用していません。
後述のSpring Boot で自分たちでメトリクス取得する方法のほうが便利です。
Spring Boot におけるメトリクス
最近の Spring Boot ではメトリクスの収集は micrometer に集約されています。micrometer は Java の世界ではデファクトスタンダードのメトリクスライブラリです。
Spring Boot を actuator を有効にして起動するとそれだけでかなりのメトリクスが取れるので、とても便利です。
Spring Boot では auto configuration という仕組みがあって、一部のライブラリについては設定を書くだけで利用可能になる仕組みがありますが、そのようなライブラリについては、自動的にメトリクスを取れるようになっています。
まず、以下を依存に入れてください。
implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
application.yml に以下のように追記するだけで prometheus エンドポイントは有効になります。
management:
endpoints:
web:
exposure:
include:
- health
- info
- prometheus
何も設定しないと通常のウェブサービスを提供しているポートで prometheus エンドポイントを晒してしまうことになるので、我々のチームでは以下のように書くようにして、別ポートにマッピングするようにしています。
management.server.port: 9090
Spring Boot でデフォルトで利用できるメトリクスには例えば以下のようなものがあり、いずれも有用です。
取得可能項目
|
概要
|
---|---|
jvm_* |
JVM 自体のメトリクス。GC の頻度などが取得可能です。 |
logback_events_total |
特定のログレベルのログの出力件数をモニタリング可能。
例えば以下のようにアラートを設定しています。
|
Tomcat |
Tomcat のスレッドが詰まることがあるので、Tomcat のスレッドプールの空き状況は常にチェックしておく必要があります。
Tomcat のスレッドプールを使い切るとシステム障害になるので、以下のようなアラートを設定しています。
|
ExecutorService |
ExecutorService の処理についてもモニタリング可能です。 キューにたまりすぎると問題なので、そのあたりをモニタリングしておくと良いかもしれません。 |
RestTemplate, WebClient |
HTTP Client にも対応しています。 |
ライブラリ固有のメトリクス
Java のライブラリでは、micrometer 対応がされているものが多いです。一部ライブラリは micrometer 側で組み込みで対応されています。
ライブラリ
|
概要
|
---|---|
Java 用の Redis クライアントライブラリである Lettuce では、コマンドごとのレイテンシなどのメトリクスが取得可能です。 Redis は多様なデータ構造を利用して様々なユースケースに対応した開発が可能な一方で、設計時の想定よりもデータ量が増えた場合など、パフォーマンスが悪化してしまうことも多いですので、メトリクスをしっかりととっておくことが重要です。 |
|
LINE が提供する OSS である Decaton でももちろん micrometer に対応しています。 |
|
Caffeine |
On-memory Cache ライブラリの caffeine は micrometer 側でサポートしています。 Cache のヒット率などを取得できるので便利です。On-Memory の Cacheは「いざ運用してみたら思ったよりヒット率が低かった」なんてこともよくあるので、メトリクスをしっかりととっておいて、たまに点検したほうが良いですね。 |
HikariCP |
コネクションプールが枯渇するというのも、JVM 上で運用されているアプリケーションではありがちなケースです。 HikariCP のコネクション取得がペンディング状態になっている数が増えてきた場合には障害の原因になりますから、ここもアラートを設定しておくのが良いでしょう。
|
mybatis など micrometer 対応していないライブラリの場合のメトリクスの取り方
micrometer 対応がされていないライブラリの場合にはどのようにメトリクスを取得したら良いのでしょうか。
例として mybatis を上げてみましょう。我々のチームでは、mybatis をメインのデータベースアクセスライブラリとして利用しているのですが、mybatis には micrometer サポートがありません。そこで、以下のようにインターセプターを書いてメトリクスを収集するようにしています。
だいたいの外部アクセスを伴うライブラリには、このようなインターセプター的な機構があるので、応用が可能なテクニックです。
(簡単に書けるので Spring Boot 3 以後で対応している micrometer-observation を使ってサンプルコードを書いていますが、実プロダクトでは 2022年12月 現在 Spring Boot 2 で運用しているので、実際には Observation は利用していません)
import io.micrometer.observation.Observation
import io.micrometer.observation.ObservationRegistry
import org.apache.ibatis.cache.CacheKey
import org.apache.ibatis.executor.Executor
import org.apache.ibatis.mapping.BoundSql
import org.apache.ibatis.mapping.MappedStatement
import org.apache.ibatis.plugin.Interceptor
import org.apache.ibatis.plugin.Intercepts
import org.apache.ibatis.plugin.Invocation
import org.apache.ibatis.plugin.Signature
import org.apache.ibatis.session.ResultHandler
import org.apache.ibatis.session.RowBounds
import org.springframework.stereotype.Component
@Intercepts(
Signature(
type = Executor::class,
method = "update",
args = [MappedStatement::class, Any::class]
),
Signature(
type = Executor::class,
method = "query",
args = [
MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class, CacheKey::class,
BoundSql::class
]
),
Signature(
type = Executor::class,
method = "query",
args = [MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class]
)
)
@Component
class MicrometerInterceptor(private val observationRegistry: ObservationRegistry) : Interceptor {
override fun intercept(invocation: Invocation): Any {
val mappedStatement = invocation.args[0] as MappedStatement
return Observation.createNotStarted("mybatis.query", observationRegistry)
.lowCardinalityKeyValue("id", mappedStatement.id)
.observe(invocation::proceed)
}
}
このような手法で取得しているクライアントライブラリとしては、他に Elasticsearch があります。
まとめ
以上、LINE の Business Platform でのメトリクスのとり方を紹介してみました。
システムの安定運用のためには様々なメトリクスをとっておくことで、トラブルシューティングが早くなる、システムのスケールアウト・スケールアップをするかどうかの判断ができるようになるなどのメリットがあります。
本稿を通じて、JVM でのシステム運用の雰囲気を少しでも感じていただけたら幸いです。