🔍

Cloud Run で Cloud Profiler を利用する

に公開

はじめに

Google Cloud の 公式ドキュメント では Cloud Profiler がサポート対象として明記されているランタイムは GKE・GCE・App Engine などに限られ、2025‑05 時点で Cloud Run はリストに含まれていない。しかし実際には Java Agent をコンテナに同梱すれば Cloud Run でもプロファイルを取得できる。今回はその最小手順をまとめる。

手順の詳細はリポジトリ を参照してください。

Dockerfile — Profiler Agent を組み込む

FROM gradle:8.5-jdk17 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
RUN gradle build --no-daemon

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar

# Cloud Profiler agent
RUN apt-get update && apt-get install -y curl && \
    curl -sSL https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz | tar xz -C /app && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

RUN echo '#!/bin/bash' > /app/start.sh && \
    echo 'exec java -agentpath:/app/profiler_java_agent.so -jar /app/app.jar' >> /app/start.sh && \
    chmod +x /app/start.sh

ENV PORT=8080
EXPOSE 8080
CMD ["/app/start.sh"]

ポイント

  • Agent の .so と .jar を展開するだけで OK。環境変数は不要。
  • GOOGLE_CLOUD_PROJECT などは Cloud Run が 自動で環境変数に注入する。Profiler Java Agent はこれを内部で参照するため、Dockerfile や CMD で明示する必要はない。
  • JDK 17 以降は -XX:+PreserveFramePointer がデフォルト有効。

Cloud Profiler で Flame Graph を出す

test-profiler.sh(主要部)

CPU 負荷

for i in {1..10}; do
    curl -s $SERVICE_URL/cpu-intensive > /dev/null
done

メモリ負荷

for i in {1..10}; do
    curl -s $SERVICE_URL/memory-intensive > /dev/null
done

Application.kt(抜粋)

fun main() {
    println("Starting application with Cloud Profiler enabled via JVM agent")
    val port = System.getenv("PORT")?.toInt() ?: 8080

    embeddedServer(Netty, port = port) {
        routing {
            get("/") {
                call.respondText("Hello from Cloud Run with Profiler!")
            }
            get("/cpu-intensive") {
                val result = performCpuIntensiveTask()
                call.respondText("CPU intensive task completed: $result")
            }
            get("/memory-intensive") {
                val result = performMemoryIntensiveTask()
                call.respondText("Memory intensive task completed: ${result.size} items")
            }
        }
    }.start(wait = true)
}

fun performCpuIntensiveTask(): Long {
    var sum = 0L
    for (i in 1..10_000_000) {
        sum += i * i
    }
    return sum
}

fun performMemoryIntensiveTask(): List<String> {
    val list = mutableListOf<String>()
    for (i in 1..100_000) {
        list.add("Item $i with some data: ${generateRandomString(100)}")
    }
    return list
}

fun generateRandomString(length: Int): String {
    val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    return (1..length).map { chars.random() }.joinToString("")
}

こんな感じで各種指標が見えて分析できる

ハマりどころ

現象 原因と対処
Profiler initialization failed IAM 権限不足/API 無効化を確認
データが来ない Cloud Run が 0 インスタンスまでスケールダウンすると Profiler が動かない → min-instances を 1 に設定するか、test-profiler.sh などの負荷スクリプトを定期実行してインスタンスを維持しサンプルを溜める
Flame Graph が粗くなる concurrency がデフォルト (> 1) だと複数リクエストのサンプルが混ざる → concurrency を 1 にして 1 リクエスト = 1 コンテナにする
Profiler が DEADLINE_EXCEEDED Serverless VPC Access 経由で NAT タイムアウト (120 s) に引っかかる → timeoutSeconds を 300 などへ拡張するか、VPC コネクタを介さず外部通信する
自作クラスが出ない JIT 未適用 → 30 秒以上負荷/-XX:TieredStopAtLevel=1
コストが心配 サンプリング 1 / 1000 sec、CPU 2 % 未満、数円/日レベル

まとめ

Cloud Run で Cloud Profiler を動かす課題はコールドスタートにありそう。インスタンスが0台になるたびに Profiler Agent も停止し指標が消える。この使いにくにさから公式サポートの対象外となっていると推測する。

それでも min‑instances を1に設定し、短時間でも負荷を継続して与えれば Flame Graph は取得できる。なので開発・テスト用途やパフォーマンスの傾向把握には十分使える。

Discussion