LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Spring Boot アプリケーションにおけるメトリクスの取り方の基本

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 の頻度などが取得可能です。
特にリリース後に Full GC が増えるケースなどがありうるので、リリース直後やライブラリのバージョンアップ後にはチェックするようにしています。

logback_events_total

特定のログレベルのログの出力件数をモニタリング可能。
error ログが急激に増大したことを検出できるので便利です。メトリクス機能がなくてもログは出している、というライブラリも時々あるので、ここのモニタリングは「最後の砦」的な存在です。

# HELP logback_events_total Number of events that made it to the logs
# TYPE logback_events_total counter
logback_events_total{level="warn",} 0.0
logback_events_total{level="debug",} 3.0
logback_events_total{level="error",} 0.0
logback_events_total{level="trace",} 0.0
logback_events_total{level="info",} 5.0

例えば以下のようにアラートを設定しています。

sum by (project) ( rate(logback_events_total{service="bot-crm", level="error"}[1m]) ) > 2
sum by (project) ( rate(logback_events_total{service="bot-crm", level="warn"}[1m]) ) > 10
Tomcat

Tomcat のスレッドが詰まることがあるので、Tomcat のスレッドプールの空き状況は常にチェックしておく必要があります。
application.yml に以下を設定しておくと取れる情報が増えますので、設定しています。

server.tomcat.mbeanregistry.enabled: true

Tomcat のスレッドプールを使い切るとシステム障害になるので、以下のようなアラートを設定しています。

tomcat_threads_busy_threads{service="bot-crm"} / tomcat_threads_config_max_threads{service="bot-crm"} > 0.7
ExecutorService

ExecutorService の処理についてもモニタリング可能です。
ExecutorService のメトリクスとしては、完了したタスクの数やキューに溜まっている数を見ることができます。

キューにたまりすぎると問題なので、そのあたりをモニタリングしておくと良いかもしれません。

RestTemplate, WebClient

HTTP Client にも対応しています。
近年のシステム開発では microsrevices 化が進行しているので、社内の他のチームが運用しているサービスとの通信が発生するケースが増えています。
他のコンポーネントとの通信において、どのサービスへの通信がどのぐらいのレイテンシになっているかを把握しておくことは非常に重要です。

ライブラリ固有のメトリクス

Java のライブラリでは、micrometer 対応がされているものが多いです。一部ライブラリは micrometer 側で組み込みで対応されています。

ライブラリ
概要

Lettuce

Java 用の Redis クライアントライブラリである Lettuce では、コマンドごとのレイテンシなどのメトリクスが取得可能です。

Redis は多様なデータ構造を利用して様々なユースケースに対応した開発が可能な一方で、設計時の想定よりもデータ量が増えた場合など、パフォーマンスが悪化してしまうことも多いですので、メトリクスをしっかりととっておくことが重要です。

Decaton

LINE が提供する OSS である Decaton でももちろん micrometer に対応しています。
タスクの処理回数や、タスクを処理するのにかかる時間などを計測できます。

Caffeine

On-memory Cache ライブラリの caffeine は micrometer 側でサポートしています。

Cache のヒット率などを取得できるので便利です。On-Memory の Cacheは「いざ運用してみたら思ったよりヒット率が低かった」なんてこともよくあるので、メトリクスをしっかりととっておいて、たまに点検したほうが良いですね。

HikariCP

コネクションプールが枯渇するというのも、JVM 上で運用されているアプリケーションではありがちなケースです。
コネクション数を増やしすぎると RDBMS のサーバー側に負荷がかかるので、ちょうどいいぐらいに調整する必要があります。

HikariCP のコネクション取得がペンディング状態になっている数が増えてきた場合には障害の原因になりますから、ここもアラートを設定しておくのが良いでしょう。

sum by (instance, pool) (hikaricp_connections_pending{service="bot-crm"}) > 1

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 でのシステム運用の雰囲気を少しでも感じていただけたら幸いです。