KtorのアプリからMicrometer+PrometheusでJVMのメトリクスを取得する

はじめに

Ktorのドキュメント呼んでいたらMicrometerに対応してそうだというのを見かけてちょっと動かしてみようかと思います。
基本的にはドキュメントに書かれた流れを沿う感じでやろうかと思います。
あと、PrometheusはDockerを用いて起動します。

やってみる

環境

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:        20.04
Codename:       focal

$ uname -srvmpio
Linux 5.4.0-72-generic #80-Ubuntu SMP Mon Apr 12 17:35:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux


$ docker version
Client: Docker Engine - Community
 Version:           20.10.6
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        370c289
 Built:             Fri Apr  9 22:47:17 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.6
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8728dd2
  Built:            Fri Apr  9 22:45:28 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ java --version
openjdk 16 2021-03-16
OpenJDK Runtime Environment (build 16+36-2231)
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)

プロジェクトの作成

プロジェクトはIntelliJKtorプラグインを使って作成します。
設定は以下のようにします。

f:id:yuya_hirooka:20210421214635p:plain

f:id:yuya_hirooka:20210421214439p:plain

依存の追加のところでMicrometerを追加することもできますが、今回ははRoutingだけを選択します。

出来上がったプロジェクトのPomの抜粋を以下に示します。

    <properties>
        <ktor_version>1.5.3</ktor_version>
        <kotlin.code.style>official</kotlin.code.style>
        <kotlin_version>1.4.32</kotlin_version>
        <logback_version>1.2.3</logback_version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
        <main.class>hirooka.dev.ApplicationKt</main.class>
    </properties>
      <dependencies>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-core</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-netty</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback_version}</version>
        </dependency>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-tests</artifactId>
            <version>${ktor_version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Ktorのバージョンは1.4.32が選択されているのと、依存にはテスト関連の者Lockbackそして、ktor-server-coreが追加されていました。

Prometheus形式のメトリクスを取得できるようにする

まずは以下の依存をPomに追加する必要があります。

        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-metrics-micrometer</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.6.6</version>
        </dependency>

1つ目はKtorのMicrometerのサポートを提供してくれるプロジェクトで、2つ目はPrometheusのレジストリーです。

次にApplication.ktを以下のように修正します。

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import hirooka.dev.plugins.*
import io.ktor.application.*
import io.ktor.metrics.micrometer.*

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry
        }
        configureRouting()
    }.start(wait = true)
}

追加したのはinstall(MicrometerMetrics)のところです。
KtorでMicromerを使う場合installでMicrometerMwetricsのクラスをインストールする必要があります。
次にJVMのメトリクスを公開するために更に以下のような修正をくわえます。

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry

            meterBinders = listOf(
                JvmMemoryMetrics(),
                JvmGcMetrics(),
                ProcessorMetrics(),
            )
        }
        configureRouting()
    }.start(wait = true)
}

最後のProcessorMetrics()はシステムのメトリクスなので今回はいらないかもしれませんが一応追加しておきます。
最後に取得したメトリクスを公開するRoutingを記述します。

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry
        }

        routing {
            get("/metrics") {
                call.respond(prometheusMeterRegistry.scrape())
            }
        }
        configureRouting()
    }.start(wait = true)
}

Mainの頭のところでイニシャライズしたprometheusMeterRegistryを使う必要があったため、今回はApplication.ktにRoutingを記述しました。
具体的には/metricsというPathに対してprometheusMeterRegistry.scrape()で取得できるメトリクスを返すようにしています。

これでアプリケーション側の設定は完了です。
アプリケーションを起動して、cURLでメトリクスを取得してみます。

$ curl localhost:8080/metrics 
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
jvm_buffer_memory_used_bytes{id="direct",} 3.3562632E7
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 0.06208306434258662
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.22908672E8
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.321499136E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 5836800.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.22912768E8
# 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="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.006
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Metadata GC Threshold",} 0.006
# HELP jvm_gc_max_data_size_bytes Max size of long-lived heap memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 8.321499136E9
# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time
# TYPE system_load_average_1m gauge
system_load_average_1m 2.18
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 2686976.0
jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 6291456.0
jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 4.5088768E8
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 2.5608192E7
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0
jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 6.291456E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 2883584.0
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2555904.0
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
jvm_buffer_total_capacity_bytes{id="direct",} 3.3562631E7
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 8.0
# HELP ktor_http_server_requests_seconds_max  
# TYPE ktor_http_server_requests_seconds_max gauge
ktor_http_server_requests_seconds_max{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 0.021426668
# HELP ktor_http_server_requests_seconds  
# TYPE ktor_http_server_requests_seconds histogram
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001048576",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001398101",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001747626",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002097151",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002446676",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002796201",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003145726",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003495251",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003844776",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.004194304",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.005592405",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.006990506",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.008388607",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.009786708",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.011184809",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.01258291",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.013981011",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.015379112",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.016777216",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.022369621",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.027962026",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.033554431",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.039146836",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.044739241",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.050331646",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.055924051",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.061516456",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.067108864",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.089478485",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.1",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.111848106",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.134217727",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.156587348",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.178956969",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.20132659",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.223696211",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.246065832",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.268435456",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.357913941",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.447392426",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.5",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.536870911",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.626349396",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.715827881",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.805306366",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.894784851",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.984263336",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.073741824",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.431655765",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.789569706",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.147483647",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.505397588",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.863311529",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.22122547",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.579139411",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.937053352",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="4.294967296",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="5.726623061",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="7.158278826",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="8.589934591",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="10.021590356",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="11.453246121",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="12.884901886",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="14.316557651",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="15.748213416",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="17.179869184",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="20.0",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="+Inf",} 1.0
ktor_http_server_requests_seconds_count{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 1.0
ktor_http_server_requests_seconds_sum{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 0.021426668
# HELP ktor_http_server_requests_active  
# TYPE ktor_http_server_requests_active gauge
ktor_http_server_requests_active 1.0
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="mapped",} 0.0
jvm_buffer_count_buffers{id="direct",} 6.0
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 2629376.0
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 6291456.0
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 0.0
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 2.4776112E7
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 1170432.0
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 1.2582912E7
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 2673248.0
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 507008.0
# HELP jvm_gc_live_data_size_bytes Size of long-lived heap memory pool after reclamation
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 0.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 9.6815834767642E-4
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 2.3068672E7
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 0.0

Promehteusから収集する

最後に公開されているメトリクスをPrometheus側から取得したいと思います。
まずは

global:
  scrape_interval:     15s
  evaluation_interval: 15s

rule_files:

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['host.docker.internal:8080/metric']

余談ですがDocker 20.10系からLinux--add-host=host.docker.internal:host-gatewayみたいな感じで起動時にオプションを渡してやるとhost.docker.internalでホストマシンにアクセスできるようになったみたいですね。

PrometheusをDockerで起動します。この際に上記の設定ファイルをマウントするようにします。

docker run \
    --add-host=host.docker.internal:host-gateway \
    -p 9090:9090 \
    -v /path/to/four/config/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus

これでPrometheusが起動し、localhost:9090でUIに接続できます。

f:id:yuya_hirooka:20210421225443p:plain

ターゲットの一覧を確認するときちんとアプリが認識されているのがわかります。

f:id:yuya_hirooka:20210421231219p:plain

以下のようにjvm_buffer_count_buffersも可視化できました。

f:id:yuya_hirooka:20210421231354p:plain

Docker 20.10.0についてまとめる

はじめに

だいぶ前ですがDokcer 20.10.0がリリースされています。かねてからやろうと思ってやってなかったのですが、リリースノートを眺めて、気になったところをかいつまんでまとめてみようと思います。
ものによっては動かしてみようかと。

やっていく

環境

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


$ docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

今回、使用するDockerは20.10.5ですが、20.10.0の変更点のみをまとめていこうと思います。

API

GET /eventspruneのイベントを取得できる

例えば、以下のようにcurl --unix-socket /var/run/docker.sock http:/v1.41/eventsにアクセスした状態で、docker system pruneコマンドを実行すると、以下のようにイベントを取得することができます。

$ curl --unix-socket /var/run/docker.sock http:/v1.41/events
{"status":"prune","Type":"container","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008756813704}
{"Type":"network","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008758330171}
{"status":"prune","Type":"image","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008798691967}
{"Type":"builder","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008866211129}

このエンドポイントはcontainernetworkvolumeimageそれぞれに対してreclaimed(回収したバイト数)を取得することができます。

GET /infoに対する変更点

以下のような変更点があります。

  • OSのVersionを取得できるようになった
  • DefaultAddressPoolsを取得できるようになった

Builder

#syntaxなしで、RUN --mountオプションを利用できるようになった

もともとBuildKitのRUN --mount拡張機能を利用したい場合は``のようなコメント行をDockerfileの先頭に追加する必要がありましたが不要になりました。

ARGENVのように複数の値を受け付けるように変更

ARGコマンドは変数を定義してビルド時に--build-arg <varname>=<value>のフラグを利用することによって引数を渡すことができます。このARGコマンドが今回の変更でENVと同じように複数定義を一行でできるように修正さています。

ADDコマンドに--chownパラメーター拡張を渡せるようになった

もともとADD/COPYコマンドでは--chownフラグを指定できファイルオーナーを設定できますしかし、ADDコマンドではARGENVで定義された変数からの指定ができないようになってました。しかし今回の修正でそれが行えるようになったようです。

Clientクライアントでは以下のような修正が行われています

  • -a/--all-tagsフラグですべてのタグをpushできるようになった
  • Kubernetesusername/password認証をサポート
  • runコマンドとcreateコマンドに--pull=missing|always|neverフラグを追加
  • docker exec--env-fileフラグがついかされ環境変数を渡せるようになった
  • log-driverが追加されPrettyオプションが利用可能になった
  • --cgroupnsフラグでネームスペースを指定できようになった
  • docker manifest rmローカルストレージのマニフェストリストドラフト(設定やレイヤーの情報を保持)を削除
  • docker infodocker versionにコンテキストの情報を出力するように変更

Runtime

ランタイムでは以下のような修正が行われています。

  • cgroup2のサポート
  • cgroup2がデフォルトでsystemdのcgroupを利用するようになった
  • 新しいストレージドライバfuse-overlayfs
  • containerdのバイナリを1.4.3にアップデート
  • docker pushのデフォルトがすべてのタグのPushからlatestに変更
  • イメージをPullする際のコネクションロスでリコネクトする回数を指定できるように変更
  • コンテナスタートアップのために、最低のメモリリミットを6Mへ変更。

Networking

  • host.docker.internalLinuxでのサポート

最後の方力尽きましたが、すべての変更に関してはこちらをご確認ください

QuarkusのDevServicesを試す

はじめに

先日、Quarkusの1.13がリリースされので、リリースブログを眺めていたのですがDevServicesという便利そうな機能が追加されていたので試してみようかと思います。

DevServicesとは

DevモードでQuarkusを起動した場合、追加の設定無しでDBを起動してくれてDevモードのコンフィグとバインドしてくれるようです。
例えば、PostgresSQL JDBCの拡張がPomの依存に追加されている場合Testcontainersを使って(もしくはJavaのプロセス内で)自動的にDBを立ち上げてくれます。(今回は試しませんがReactiveクライアントにも対応しているようです)
現在DevSercicesは以下のDBに対応しているようです。

また、DB2MSSQLに関してはライセンスへの同意が必要です。src/main/resources/container-license-acceptance.txtを作成して以下のようなテキストを記述する必要があります。

ibmcom/db2:11.5.0.0a
mcr.microsoft.com/mssql/server:2017-CU12

使ってみる

環境

今回は以下の環境でアプリを動かします。

$ java --version
openjdk 16 2021-03-16
OpenJDK Runtime Environment (build 16+36-2231)
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)

$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 16, vendor: Oracle Corporation, runtime: /home/username/.sdkman/candidates/java/16-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-70-generic", arch: "amd64", family: "unix"

$ docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

プロジェクトを作成する

今回はPosgresSQLを利用して、DBに保存されたデータを返す簡単なAPIを記述しようと思います。
以下の構成でプロジェクトを作成します。

f:id:yuya_hirooka:20210405140316p:plain

Devモードでアプリケーションを起動する

DevServcesはデフォルトでオンになっており、DB URLやパスワード、ユーザ名が設定されていなければTestcontainersを使ってDBを立ち上げてくれます。
先程、作成したプロジェクトをunzipしてなにも変更せずにDevモードで起動すると以下のようなログを出力してDBも立ち上げてくれます。

$ ./mvnw compile quarkus:dev
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< dev.hirooka:devservices >-----------------------
[INFO] Building devservices 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- quarkus-maven-plugin:1.13.0.Final:generate-code (default) @ devservices ---
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ devservices ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ devservices ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- quarkus-maven-plugin:1.13.0.Final:dev (default-cli) @ devservices ---
Listening for transport dt_socket at address: 5005
2021-04-05 12:13:24,948 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-28) Loaded org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy from ~/.testcontainers.properties, will try it first
2021-04-05 12:13:25,299 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-28) Found Docker environment with Environment variables, system properties and defaults. Resolved dockerHost=unix:///var/run/docker.sock
2021-04-05 12:13:25,300 INFO  [org.tes.DockerClientFactory] (build-28) Docker host IP address is localhost
2021-04-05 12:13:25,324 INFO  [org.tes.DockerClientFactory] (build-28) Connected to docker: 
  Server Version: 20.10.5
  API Version: 1.41
  Operating System: Ubuntu 20.04.2 LTS
  Total Memory: 31741 MB
2021-04-05 12:13:25,326 INFO  [org.tes.uti.ImageNameSubstitutor] (build-28) Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2021-04-05 12:13:25,352 INFO  [org.tes.uti.RegistryAuthLocator] (build-28) Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: testcontainers/ryuk:0.3.1, configFile: /home/yuya-hirooka/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /home/yuya-hirooka/.docker/config.json (そのようなファイルやディレクトリはありません)
2021-04-05 12:13:26,102 INFO  [org.tes.DockerClientFactory] (build-28) Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2021-04-05 12:13:26,103 INFO  [org.tes.DockerClientFactory] (build-28) Checking the system...
2021-04-05 12:13:26,103 INFO  [org.tes.DockerClientFactory] (build-28) ✔︎ Docker server version should be at least 1.6.0
2021-04-05 12:13:26,185 INFO  [org.tes.DockerClientFactory] (build-28) ✔︎ Docker environment should have more than 2GB free disk space
2021-04-05 12:13:26,295 INFO  [🐳 .6.12]] (build-28) Creating container for image: postgres:9.6.12
2021-04-05 12:13:26,337 INFO  [🐳 .6.12]] (build-28) Starting container with ID: 33b42f67a20ed4d4ff1b2f0135ec06b13c3444e5782567e4d30395e0c5741fd3
2021-04-05 12:13:26,710 INFO  [🐳 .6.12]] (build-28) Container postgres:9.6.12 is starting: 33b42f67a20ed4d4ff1b2f0135ec06b13c3444e5782567e4d30395e0c5741fd3
2021-04-05 12:13:30,167 INFO  [🐳 .6.12]] (build-28) Container postgres:9.6.12 started in PT3.98152593S
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-04-05 12:13:30,588 INFO  [io.quarkus] (Quarkus Main Thread) devservices 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.0.Final) started in 6.064s. Listening on: http://localhost:8080
2021-04-05 12:13:30,589 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-04-05 12:13:30,589 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, mutiny, narayana-jta, resteasy, smallrye-context-propagation]

docker psコマンドで確認してみるとPostgresとTestcontainersのコンテナがきどうしているのが確認できます。

$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED         STATUS         PORTS                     NAMES
74541a57e8cd   postgres:9.6.12             "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes   0.0.0.0:49164->5432/tcp   great_blackburn
72f55524eecf   testcontainers/ryuk:0.3.1   "/app"                   3 minutes ago   Up 3 minutes   0.0.0.0:49163->8080/tcp   testcontainers-ryuk-42c2bcf5-1ddc-415d-b694-3f7f311d885e

データを取得してみる

それでは、実際にこの起動したDB利用してみようと思います。
まずは初期データを投入します。
まずはapplication.propertiesを以下のように修正します。

%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.hibernate-orm.database.generation=drop-and-create

quarkus.hibernate-orm.database.generation=drop-and-createを指定することで、Quarkusのアプリは起動時にsqlファイルを読み取ってテーブルを作成してくれるようになります。
また、デフォルトではresourcesimport.sqlという名前のファイルを読み取るようになっているのでそちらも用意しておきます。

drop table if exists message;

create table message(
    id      int,
    content varchar(100)
);

insert into message
values (1, 'hello, dev service1'),
       (2, 'hello, dev service2'),
       (3, 'hello, dev service3');

quarkus.hibernate-orm.log.sqlSQLの実行がわかりやすいように設定しています。また%devはDevモードで起動時に有効になる設定を示します。

この状態でアプリケーションを再起動します。
すると、ここには記述しませんが実行されたSQLのログが出力されるはずです。

ここまでで、起動したDBを利用できていることががある程度確認できたかもしれませんが。
EntityとControllerを作成してmessageを取得してみようと思います。

MessageEntity.java

@Entity
public class MessageEntity {

    @Id
    private int id;

    private String content;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

GreetingResource .java

@Path("/messages")
public class GreetingResource {
    private EntityManager em;

    public GreetingResource(EntityManager em) {
        this.em = em;
    }


    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<MessageEntity> hello() {
        return em.createNativeQuery("select id, content from message", MessageEntity.class).getResultList();
    }
}

コントローラーはデフォルトで作成されたGreetingResource.javaを少し改造して作っています。また、少し筋は悪いかもしれませんがEntityをそのままレスポンスとして返すようにしています。

アプリケーションがリロードされたらcURLを叩いてみます。

$ curl localhost:8080/messages
[{"id":1,"content":"hello, dev service1"},{"id":2,"content":"hello, dev service2"},{"id":3,"content":"hello, dev service3"}]

import.sqlでセットしていた値が帰ってきていることが確認できました。

本番用の設定を記述する

前述したとおりDevServicesはDB URLなどがセットされている場合においてDBを起動しないようになっています。
例えば、以下のように設定を記述してアプリケーションを起動すると、DBが起動されません。

quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/hibernate_orm_test
quarkus.datasource.username=username
quarkus.datasource.password=password

起動すると、import.sqlの実行に失敗したエラーログが出力されたと思います。
application.properitesに本番用の設定値を書きたいが、ローカルでの開発ではDevServicesを利用したい場合は設定のプリフィクスとして%prodを以下のように付与します。

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/hibernate_orm_test
%prod.quarkus.datasource.username=username
%prod.quarkus.datasource.password=password

これで上記の設定はDevモードでは用いられることはなくDevServicesはDBを立ち上げてくれます。

その他の設定値

最後にDevServicesの設定をいくつかまとめておこうと思います。完全なリストはこちら をご覧ください。

設定値 説明 デフォルト値(型)
quarkus.datasource.devservices DevServicesを明示的に有効化するかのフラグ true(boolean)
quarkus.datasource.devservices.image-name 利用するコンテナイメージ名を指定する。もしH2などコンテナを起動しないサービスの場合は影響を及ぼさない (string)

また、ここにはまとめてませんが、名前付きデータソースごとにそれぞれの設定を行なうことも可能なようです。

k8s上のSpring Bootアプリからメトリクスを取得する(Actuator + Prometheus Operator)

はじめに

Kubernetes上でのSpring Bootのアプリのデバックを行なう際にPodのコンテナ内に応じて必要なコマンドとってきて、スレッドダンプやヒープダンプを取得してホストにコピーみたいなことをやっていたのですが、流石に面倒に感じはじめました。
そこでふとActuator入れりゃええやんと思いたち、どうせだったら、Prometeheusでメトリクスの収集までできればいろいろええんじゃないかと思いいろいろ動かしてみようと思います。

Spring Boot Actuatorとは

(まず、前提としてこのブログはBoot 2.4系のドキュメントを確認してます。貼られているリンクもそうなってると思います。)
Spring Boot Actuatorはアプリケーションをモニター、マネージするための追加機能を提供してくれています。HTTPやJMXのエンドポイントを通して操作を選択することができます(JMXを利用する場合はspring.jmx.enabledtrueで設定する必要があります)。HTTPを利用する場合はデフォルトでは/actuator/{id}の形式で情報を取得することが可能で、たとえばhealthの情報を取得したい場合は/actuator/healthのエンドポイントで利用できます。カスタムエンドポイントを公開することもできますが、基本としては以下のようなエンドポイントが用意されています。

ID 説明
health アプリケーションの正常性情報を取得でる。このエンドポイントを通して、実行中のアプリケーション(本体と関連するアプリケーション)ステータスを確認することができる。関連するアプリケーションに関してはHealthContributorRegistryに定義されているすべてのHealthContributorから収集される。(詳細はこちら
httptrace HTTPのトレース情報(リクエスト/レスポンス)を取得できる。デフォルトで最新100個をインメモリに保存。HTTP トレースを有効にするには、アプリケーションの構成で HttpTraceRepository型をBeanとして提供する必要がある
beans アプリケーション内のSpring Beanのリストを取得できる
mappings @RequestMappingのパスのリストを表示します
shutdown アップリケーションをシャットダウンする
threaddump スレッドダンプを取得できる
heapdump hprofヒープダンプを取得できる。アプリケーションがSpring MVCやWebFluxである場合に有効
logfile ログの内容を返す。logging.file.namelogging.file.pathプロパティが設定されている必要がある。アプリケーションがアプリケーションがSpring MVCやWebFluxである場合に有効

詳細なリストに関してはこちらをご覧ください。
また、Prometheusフォーマットで出力してくれるエンドポイントも提供されており今回はこちらを利用して、メトリクスを収集しようと思います。

エンドポイントの公開に関するセキュリティ的な考慮点に付いて

Actuatorでアプリケーションのインフラレイヤーやアプリケーションレイヤーで保護しておく必要があります。アプリケーションレイヤーでは「必要なエンドポイントのみ公開する」のが基本的な戦略となると思います。ActuatorではデフォルトでHTTPのエンドポイントはhealthinfo以外は非公開になっているようです。 これらのエンドポイントを公開非公開を制御する場合は以下のようなincludeもしくはexcluedeプロパティを使用します。例えば、info, health, threaddumpのエンドポイントを公開したい場合は、アプリケーションプロパティで以下のように設定します。

management.endpoints.web.exposure.include=info, health, threaddump

その他のプロパティに関してはこちらをご覧ください。

単純な公開非公開とは別に、Spring Securityを利用している場合、そのコンテンツネゴシエーションの仕組みを利用してエンドポイントを保護することができます。例えば、特定のロールを持つユーザのみアクセス可能と言ったような制御を行なうことができます。

やっていく

環境

今回KubernetesクラスタはMinikubeを用いてローカルに構成します。そのVMはデフォルトのDockerを利用します。
ミドルウェア、OSのバージョンは以下のとおりです。

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


(Minikubeが動いているDocker)
$ docker version

(クライアントは省略)

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ minikube version
minikube version: v1.16.0
commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1

$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-03-18T01:10:43Z"
  compiler: gc
  gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f
  gitTreeState: clean
  gitVersion: v1.20.5
  goVersion: go1.15.8
  major: "1"
  minor: "20"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-12-08T17:51:19Z"
  compiler: gc
  gitCommit: af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38
  gitTreeState: clean
  gitVersion: v1.20.0
  goVersion: go1.15.5
  major: "1"
  minor: "20"
  platform: linux/amd64

環境構築

プロジェクトの作成 & Ptometeusのメトリクスを取得できるように設定

まずは、プロジェクトを作成します。
プロジェクトはSpring Initializrを利用して以下のような構成で作成します。

f:id:yuya_hirooka:20210328124237p:plain

Webの他にActuatorとPrometheusの選択肢を選択肢てます。
出来上がったプロジェクトのPomの依存を確認すると以下の2つが存在することが確認できます。

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

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

Actuatorの出力をPrometheusの形式で出力する場合micrometer-registry-prometheusの依存を追加する必要があります。今回は初期Initializrで追加しましたが、既存のBootプロジェクトにActuatorとPromtheusを使ってメトリクスの収集をする場合は上記の2つの依存を追加してやれば大丈夫だと思います。

コントローラーを作っておく

GETでリクエストを受けると文字列を返すコントローラーを作っておきます。
コンポーネントスキャン対象のパッケージ配下にいかのようなGreetingControllerを作成します。

@RestController
public class GreetingController {
    @GetMapping("/hello")
    public String hello(){
        return "hello, hello";
    }
}

起動してcURLで作ったコントローラーとActuatorのエンドポイントにアクセスしてみます。

$ curl localhost:8080/hello
hello, hello

$ curl localhost:8080/actuator/health
{"status":"UP"}

Prometheusのエンドポイントの公開設定を行なう

前述の通りActuatorのエンドポイントはhealthinfo以外は非公開になっています。/actuatorにアクセスすると、公開されているエンドポイントの情報を取得することができます。

$ curl localhost:8080/actuator/ | jq
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "info": {
      "href": "http://localhost:8080/actuator/info",
      "templated": false
    }
  }

Prometheus用のエンドポイントを公開するには、設定を追加する必要があります。
また、もともとの困りごとだったダンプの取得を行なうエンドポイントも公開しておきます。 アプリケーションプロパティに以下のように設定を行います。

# ついでにPort番号も変えておきます。
server.port=8888

management.endpoints.web.exposure.include=info, health, threaddump, prometheus, heapdump

再度/actuatorにアクセスしてみます。

$ curl localhost:8888/actuator | jq
{
  "_links": {
    "self": {
      "href": "http://localhost:8888/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8888/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8888/actuator/health/{*path}",
      "templated": true
    },
    "info": {
      "href": "http://localhost:8888/actuator/info",
      "templated": false
    },
    "heapdump": {
      "href": "http://localhost:8888/actuator/heapdump",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8888/actuator/threaddump",
      "templated": false
    },
    "prometheus": {
      "href": "http://localhost:8888/actuator/prometheus",
      "templated": false
    }
  }
}
yuya-hirooka@yuya-hirooka:~/source/java/actu

先程と比べて、設定したしたエンドポイントの情報が出力されているのがわかります。
Prometheus形式のメトリクスの取得をしてみます。

$ curl 'http://localhost:8888/actuator/prometheus'
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 0.0
jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 5.4525952E7
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 3.0539776E7
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0
jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 4.6137344E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 4325376.0
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 6815744.0
# HELP tomcat_sessions_alive_max_seconds  
# TYPE tomcat_sessions_alive_max_seconds gauge
tomcat_sessions_alive_max_seconds 0.0
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{state="runnable",} 11.0
jvm_threads_states_threads{state="blocked",} 0.0
jvm_threads_states_threads{state="waiting",} 11.0
jvm_threads_states_threads{state="timed-waiting",} 4.0
jvm_threads_states_threads{state="new",} 0.0
jvm_threads_states_threads{state="terminated",} 0.0
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 1.0
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 9284952.0
# HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset
# TYPE jvm_threads_peak_threads gauge
jvm_threads_peak_threads 30.0
# HELP jvm_threads_daemon_threads The current number of live daemon threads
# TYPE jvm_threads_daemon_threads gauge
jvm_threads_daemon_threads 22.0
# HELP tomcat_sessions_active_current_sessions  
# TYPE tomcat_sessions_active_current_sessions gauge
tomcat_sessions_active_current_sessions 0.0
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
jvm_buffer_total_capacity_bytes{id="direct",} 49152.0
# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time
# TYPE system_load_average_1m gauge
system_load_average_1m 2.04
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 0.0
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 1.8475864E7
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 3.016488E7
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 1257088.0
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 0.0
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 4117488.0
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 6814208.0
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 8.0
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
jvm_buffer_memory_used_bytes{id="direct",} 49152.0
# HELP process_uptime_seconds The uptime of the Java virtual machine
# TYPE process_uptime_seconds gauge
process_uptime_seconds 231.285
# HELP tomcat_sessions_rejected_sessions_total  
# TYPE tomcat_sessions_rejected_sessions_total counter
tomcat_sessions_rejected_sessions_total 0.0
# HELP tomcat_sessions_created_sessions_total  
# TYPE tomcat_sessions_created_sessions_total counter
tomcat_sessions_created_sessions_total 0.0
# HELP logback_events_total Number of error level events that made it to the logs
# TYPE logback_events_total counter
logback_events_total{level="warn",} 0.0
logback_events_total{level="debug",} 0.0
logback_events_total{level="error",} 0.0
logback_events_total{level="trace",} 0.0
logback_events_total{level="info",} 7.0
# HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution
# TYPE jvm_classes_unloaded_classes_total counter
jvm_classes_unloaded_classes_total 0.0
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_count_buffers{id="mapped",} 0.0
jvm_buffer_count_buffers{id="direct",} 6.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 0.0
# HELP jvm_gc_live_data_size_bytes Size of long-lived heap memory pool after reclamation
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 1.8475864E7
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 6.291456E7
# HELP tomcat_sessions_expired_sessions_total  
# TYPE tomcat_sessions_expired_sessions_total counter
tomcat_sessions_expired_sessions_total 0.0
# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 26.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 major GC",cause="Heap Dump Initiated GC",} 2.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Heap Dump Initiated GC",} 0.077
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.005
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of major GC",cause="Heap Dump Initiated GC",} 0.0
jvm_gc_pause_seconds_max{action="end of minor GC",cause="G1 Evacuation Pause",} 0.0
# HELP http_server_requests_seconds  
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 2.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 0.425517121
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.024393608
# HELP http_server_requests_seconds_max  
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 0.0
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.0
# HELP process_start_time_seconds Start time of the process since unix epoch.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.616906295082E9
# HELP tomcat_sessions_active_max_sessions  
# TYPE tomcat_sessions_active_max_sessions gauge
tomcat_sessions_active_max_sessions 0.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.321499136E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7553024.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2.44105216E8
# HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine
# TYPE jvm_classes_loaded_classes gauge
jvm_classes_loaded_classes 7304.0
# HELP process_files_open_files The open file descriptor count
# TYPE process_files_open_files gauge
process_files_open_files 60.0
# HELP jvm_gc_max_data_size_bytes Max size of long-lived heap memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 8.321499136E9
# HELP process_files_max_files The maximum file descriptor count
# TYPE process_files_max_files gauge
process_files_max_files 1048576.0

JVMの情報とか諸々が出力されてるが見て取れます。

threddump、heapdumpを取得するときは以下のようにします。

$ curl localhost:8888/actuator/threaddump -H 'Accept: text/plain' > threddump.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 30125  100 30125    0     0   668k      0 --:--:-- --:--:-- --:--:--  684k

$ curl 'http://localhost:8888/actuator/heapdump' -O
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 27.6M  100 27.6M    0     0   124M      0 --:--:-- --:--:-- --:--:--  124M

Docker Imageの作成

アプリケーションの準備として最後に作成したアプリをDockerイメージ化しておきます。
ここで、今回はDockerのリポジトリを個別に用意したりDocker HubにイメージをPushのは面倒なので、Minikubeのコンテキストでイメージをビルドします。
具体的には次のコマンドを実行します。

$ eval $(minikube docker-env)
f

$ docker images
REPOSITORY                                TAG        IMAGE ID       CREATED         SIZE
kubernetesui/dashboard                    v2.1.0     9a07b5b4bfac   3 months ago    226MB
k8s.gcr.io/kube-proxy                     v1.20.0    10cc881966cf   3 months ago    118MB
k8s.gcr.io/kube-apiserver                 v1.20.0    ca9843d3b545   3 months ago    122MB
k8s.gcr.io/kube-scheduler                 v1.20.0    3138b6e3d471   3 months ago    46.4MB
k8s.gcr.io/kube-controller-manager        v1.20.0    b9fa1895dcaa   3 months ago    116MB
gcr.io/k8s-minikube/storage-provisioner   v4         85069258b98a   3 months ago    29.7MB
k8s.gcr.io/etcd                           3.4.13-0   0369cf4303ff   7 months ago    253MB
k8s.gcr.io/coredns                        1.7.0      bfe3a36ebd25   9 months ago    45.2MB
kubernetesui/metrics-scraper              v1.0.4     86262685d9ab   12 months ago   36.9MB
k8s.gcr.io/pause                          3.2        80d28bedfe5d   13 months ago   683kB

これで一時的にMinikubeのコンテキストでDocker ビルドできるようになりました。
minikube docker-envコマンドはMinikube組み込みのDockerデーモンをホストマシンから操作できるようにするためのコマンドです。

この状態でイメージをビルドします。

Spring Bootは2.3からBuildpackを利用したイメージのビルドをサポートしています。今回はその機能を利用してイメージを作成したいと思います。
具体的にはプロジェクトルートで以下のコマンドを実行します。

$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring/actuator-prometheus

$ docker images | grep spring

$ docker images | grep spring/actuator-prometheus
spring/actuator-prometheus                                         latest                                          2b5438489b00   41 years ago    278MB

なんかCreatedがバグってる気がしますが、一旦はこれでOKです。

クラスターの設定 & 諸々のリソース作成

今回クラスターは以前のブログで作成したものを再利用しようと思います。構成としてはMinikubeで作ったクラスタkube-promtheusを使ってPrometheusとGrafanaをデプロイ&設定を行っています。

PodMonitorを作成

通常、Prohetheusがk8sクラスタ内のサービスを見つける際にはkubernetes_sd_configをう場合が多いみたいですが、今回はkube-prometheusを利用しており、Kubernetes Operatorが利用できます。Kubernetes OperatorではPodMonitorPodMetricsEndpointsというCRDが用意されており、そっちを利用してやってみようと思います。
以下のようなマニフェストファイルを用意します。

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: spring-app
  labels:
    backend: spring-app
spec:
  selector:
    matchLabels:
      app: spring-app
  podMetricsEndpoints:
  - port: app-port
  - path: /actuator/prometheus

ポイントはpodMetricsEndpointsのところで、ここにはPodで指定するポートの名前と、メトリクスを収集するPathを指定します。Actuatorはデフォルトでは/actuator/prometheusでメトリクスを公開しているので、そのパスを指定します。

作ったマニフェストをApplyしておきます。

$ kubectl apply -f pod-monitor.yaml 
podmonitor.monitoring.coreos.com/spring-app created

$ kubectl get podmonitor
NAME         AGE
spring-app   19s

Doploymentを作成

それでは、Deploymentを作成して、作ったSpringのアプリをKubernetesにデプロイします。
まずは、--dry-runオプションを使ってベースとなるDeploymentを作成します。

$ kubectl create deployment spring-app --image=spring/actuator-prometheus --dry-run=client -o yaml

できたマニフェストを以下のように修正します。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: spring-app
  name: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-app
    spec:
      containers:
      - image: spring/actuator-prometheus
        name: actuator-prometheus
        resources: {}
status: {}

このマニフェストを以下のように修正します。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: spring-app
  name: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-app
    spec:
      containers:
      - image: spring/actuator-prometheus
        name: actuator-prometheus
        imagePullPolicy: IfNotPresent
        ports:
          - name: spring-app
            containerPort: 8888
        resources: {}
status: {}

具体的には

        imagePullPolicy: IfNotPresent
        ports:
          - name: spring-app
            containerPort: 8888

のところを追記しました。 imagePullPolicy: IfNotPresentはローカルにイメージがある場合はPullしないようの設定と、PodMonitorが利用するようにportに名前を付けています。

それではこのDeploymentをApplyします。

$ kubectl apply -f deployment.yaml 
deployment.apps/spring-app created

Prometheusからメトリクスを確認する

実はここまでで大体設定が終わっていて、すでにPrometheusでのメトリクスの収集が始まってます。
Prometheusのダッシュボードでメトリクスを確認してみます。
まずは、デプロイされているプロメテウスにlocalhostでアクセスするためにポートフォワードを行います。

$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090[f:id:yuya_hirooka:20210403140913p:plain]
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090

http://localhost:9090にアクセスしてUIから確認すると以下のようにJVMのメトリクスを取得できるようになっています。

f:id:yuya_hirooka:20210403140002p:plain

試しにjvm_memory_used_bytesをグラフ化してみると以下のようにヒープの領域ごとにいい感じにグラフ化できることが確認できました。

f:id:yuya_hirooka:20210403140913p:plain

Minikubeにkube-prometheusを使ってPrometheusとGrafanaをデプロイする

はじめに

別のことがしたくて、Kubernetesクラスタの監視をPrometheus + Grafanaで行っている環境が欲しくて、いろいろ調べてたらkube-prometheusというのを見つけて、スターも多そうだったのでちょっと触ってみようと思います。
なおこのブログで特に指定がない場合はコンテキストはminikubeが指定されているものとします。

kube-prometheusとは

kube-prometheusはKubernetesクラスターをPrometheusで監視を行ない簡単に運用していくくための諸々が用意されたアセットです。具体的には、以下のようなものが含まれます。

インストールしてみる

環境

動作環境は以下の通り

今回KubernetesクラスタはMinikubeを用いてローカルに構成します。そのVMはデフォルトのDockerを利用します。
ミドルウェア、OSのバージョンは以下のとおりです。

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


(Minikubeが動いているDocker)
$ docker version

(クライアントは省略)

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ minikube version
minikube version: v1.18.1
commit: 09ee84d530de4a92f00f1c5dbc34cead092b95bc


$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-03-18T01:10:43Z"
  compiler: gc
  gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f
  gitTreeState: clean
  gitVersion: v1.20.5
  goVersion: go1.15.8
  major: "1"
  minor: "20"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-12-08T17:51:19Z"
  compiler: gc
  gitCommit: af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38
  gitTreeState: clean
  gitVersion: v1.20.0
  goVersion: go1.15.5
  major: "1"
  minor: "20"
  platform: linux/amd64

クラスターを構築する

公式のやり方通りにMinikubeを用いてクラスタを作ってみたいと思います。
以下のコマンドを実行します。

 minikube start --kubernetes-version=v1.20.0 --memory=6g --bootstrapper=kubeadm --extra-config=kubelet.authentication-token-webhook=true --extra-config=kubelet.authorization-mode=Webhook --extra-config=scheduler.address=0.0.0.0 --extra-config=controller-manager.address=0.0.0.0

ここで、それぞれのオプションの意味は以下のようになっています。

  • bootstrapper:Bootstrapper(インストール前の環境をチェックするプログラム)の名前。デフォルトはkubeadm(今回明示的にやる必要は無かったかも)
  • --extra-config: key=valueのペアで各コンポーネントに渡される設定。value.で区切られており最初のパートは設定を追加するコンポーネント(kubelet, kubeadm, apiserver, controller-manager, etcd, proxy, schedulerなどが選択可能)、後のパートは渡す設定値を示すようです。具体的にはそれぞれのコンポーネントに対して以下のようなオプションを渡しています。
    • kubeletに対して渡すオプションとして--authentication-token-webhook=tureauthorization-mode=Webhookを渡しています(オプションの意味はこちら)。
    • schedulerに対して0.0.0.0にバインドするように設定
    • controller-managerに対して0.0.0.0にバインドするように設定

全体を通して、すごく重要そうなのはなさそうな印象ですね。ものによっては省略しても行けそうな気がしますが、変なところでつまりたくないので一旦公式に従っておきます。 ちなみに、それぞれのオプションの意味の詳細はここをご確認ください。 また、Kubernetesに関係するコンポーネントに関してはかこちらがよくまとまっています。

kube-prometheusはmetrics APIサーバーを含むたminikubeが持つmetrics-serverは不要となります。
以下のコマンドで、無効化されているか確認します。

$ minikube addons disable metrics-server
🌑  「metrics-server」アドオンは無効化されています

minikubeにkube-prometeusをインストールする

まず、動かしているクラスターのバージョンによって提供されているブランチを選択する必要があるようです。今回クラスターは1.20系で動かしているので、現状ではブランチはrelease-0.7HEADを使う必要がありそうです。その他詳細な対応表に関してはこちらを確認してください。
今回はrelease-0.7を利用したいと思います。
以下のようにソースコードをクローンしてきて、release-07ブランチをチェックアウトします。

$ git clone https://github.com/prometheus-operator/kube-prometheus.git

$ cd kube-prometheus/

$ git checkout -b release-0.7 origin/release-0.7

$ git branch
  main
* release-0.7

チェックアウトまでできたら、必要なコンポーネントクラスタにデプロイします。 kube-prometeusのセットアップは2段階で行なう必要があります。
* manifests/setupディレクトリに用意されているマニフェストでCRDs(Custom Resource Definition)とネームスペースを作成する * CDRとネームスペースができたらmanifests配下にあるリソースをデプロイする

具体的には以下のコマンドを実行します。

$ kubectl create -f manifests/setup
$ kubectl create -f manifests/

ちなみにkube-prometeusを削除したい場合は以下のコマンドをを実行します。

$ kubectl delete --ignore-not-found=true -f manifests/ -f manifests/setup

Grafanaのダッシュボードを確認する

デプロイされたGrafanaとPrometheusのダッシュボードにアクセスするためには以下のコマンドでポートフォワードを行います。

$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090 & \
 kubectl --namespace monitoring port-forward svc/grafana 3000
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

Prometheusはhttp://localhost:9090、Grafanaはhttp://localhost:3000でそれぞれアクセスできます。

Grafanaの画面にアクセスするとデフォルトで諸々の設定を行ってくれたダッシュボードを確認することができます。

f:id:yuya_hirooka:20210330164920p:plain

f:id:yuya_hirooka:20210330165215p:plain

NginxのPodをデプロイしてメトリクスを見てみる

最後に1つネームスペースを作ってNginxのPodをデプロイしてみて、可視化されたメトリクスを確認してみたいと思います。

こちらマニフェストを使ってNginxのPodを1つのレプリケイトで作るDeploymentを作成します。
まずは、ネームスペースを作成します。

$ kubectl create ns nginx-ns

次にDeoloymentを作成します。

$ kubectl -n nginx-ns apply -f https://raw.githubusercontent.com/samuraiball/settings/master/kubernetes/double-nginx-sample/deployment.yaml
deployment.apps/nginx-first created
deployment.apps/nginx-second created

$ kubectl -n nginx-ns get po
NAME                            READY   STATUS    RESTARTS   AGE
nginx-first-d6db6c668-mpbk7     1/1     Running   0          42s
nginx-second-6b8d5c9696-wdt75   1/1     Running   0          42s

この状態でGrafanaの左サイドメニューからSearchを選択し、「pod」で検索します。 検索結果一覧のなかからKubernetes / Compute Resources / Namespace (Pods)を選択します。
画面上部のnamespaceでnginx-nsを選択するとデプロイしたNginxのPodのメトリクスがきちんと

f:id:yuya_hirooka:20210330171048p:plain

MandrelでQuarkusのアプリをネイティブイメージ化する

はじめに

Quarkusのネイティブイメージ化したアプリを作ったことなかったのと、Mandrelという名前自体は聞いていたのですがQuarkusに関係するGraalVMぐらいの理解でしか無かったので、ちょっとまとめて動かしてみようかと思います。
基本的にはQuarkusのガイド(BUILDING A NATIVE EXECUTABLE)に従いつつやりますが、自分が気なったところを少しだけ深ぼってまとめるようにしようと思います。

Mandrelってなんぞ?

Oracle GraalVM Community Editionのダウンストリームに当たるGraalVMのディストリビューションの1つです。そのメインの目的としては、Quarkusのためにデザインされたネイティブイメージ化の方法を提供することにあります。基本的にアップストリームからの大きな変更はないようですが、Quakusのアプリに不要なものが取り除かれているようです。MandrelはOracle GraalVM CEと同じネイティブイメージ化の能力を提供しますが、polyglotなどのサポートが取り除かれているようです。Mandrelは現在、Linuxコンテナの環境におけるネイティブイメージのビルドだけの利用をが推奨されており、WindowsMacOSに対するネイティブイメージを作成する場合は、 Oracle GraalVMを利用することが推奨されるようです。
理解が足りてない部分があるかもですが、おそらくCI上でのビルドやDockerのマルチステージビルドなどでMandrelを使えば諸々のコストの削減になるのでは無いかと思われます。

ネイティブイメージ化してみる

環境

今回の動作環境は以下のとおりです。

$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment 18.9 (build 11.0.10+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode)

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

Mandrelに関してはこのブログ内でインストールします。

Mandrelをインストールする

ダウンロードはGitHubここのページからおこなうことが可能です。
具体的には以下の手順でセットアップをおこなします。

$ cd ${YOUR_GRAALVM_INSTALL_DIR}
$ wget https://github.com/graalvm/mandrel/releases/download/mandrel-21.0.0.0-Final/mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ tar -xf mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ export JAVA_HOME="$( pwd )/mandrel-java11-21.0.0.0-Final"
$ export GRAALVM_HOME="${JAVA_HOME}"
$ export PATH="${JAVA_HOME}/bin:${PATH}"

QuarkusアプリのネイティブイメージのビルドはQuarkusのMavenラッパーを使って行いますが、その際に自身で指定するGraalVMでのビルドを行いたい場合はPathがとおっているGraalVMに対してnative-imageコマンドがインストールされている必要があります。
通常、native-imageコマンドはgu等を使ってインストールしないと行けなかった気がしますが、Mandrelの場合最初から内包されているようです。

Nativeイメージ化するプロジェクトの作成

Quarkus - Start coding with code.quarkus.ioを使って、 こんな感じの設定でプロジェクトを作成します。
この際にExample CodeはYes, Pleaseを選択肢します(自分でコード書いても良いのですが、ちょいめんどくいさいので)。

f:id:yuya_hirooka:20210310194526p:plain

Exampleのコードを生成を有効にしたので、GreetingResource.javaというハンドラーのコードが生成されているはずです。起動して、/hello-resteasyのパスにアクセスするとHello RESTEasyという文字列が返ってきます。

$ ./mvnw compile quarkus:dev

# 別ターミナルで
$ curl localhost:8080/hello-resteasy
Hello RESTEasy

作成されたプロジェクトのPomをちょっと見てみる

作成されたプロジェクトのPomを見てみると以下のようなプロファイルの設定が記述されているのが確認できます。

    <profile>
      <id>native</id>
      <activation>
        <property>
          <name>native</name>
        </property>
      </activation>
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${surefire-plugin.version}</version>
            <executions>
              <execution>
                <goals>
                  <goal>integration-test</goal>
                  <goal>verify</goal>
                </goals>
                <configuration>
                  <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                  </systemPropertyVariables>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <properties>
        <quarkus.package.type>native</quarkus.package.type>
      </properties>
    </profile>

Maven Failsafe Pluginを用いたインテグレーションテストの設定が記述されています(詳しくは後述)。

ネイティブイメージをビルドする

Quarkusアプリをネイティブイメージでビルドする場合は以下のコマンドを用いて行います。

$ cd ${YOUR_PROJECT_DIR}
$ ./mvnw package -Pnative

この際にPathがとおっているGraalVMにnative-imageコマンドが無かった場合は、以下のようなログを出力し、Dockerイメージをプルしてきてビルドを行ってくれます。

[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image` Attempting to fall back to container build.

ビルドが完了するとデフォルトではtarget配下に実行可能なバイナリが${project.artifactId}-${project.version}-runnerの名前でできています。 今回作成されたバイナリは以下のように実行することができます。

$ ./target/native-image-1.0.0-SNAPSHOT-runner 
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-03-10 21:12:31,788 INFO  [io.quarkus] (main) native-image 1.0.0-SNAPSHOT native (powered by Quarkus 1.12.1.Final) started in 0.030s. Listening on: http://0.0.0.0:8080
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Profile prod activated. 
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

ネイティブイメージを使ったテストを実行する

ネイティブイメージ化すると、jarでビルドする際と比較して予想しない問題が起こることがありえます。そのため、ネイティブイメージで動くアプリのインテグレーションテストを行なって置くことが推奨されます。

今回は、Exampleコードの生成を有効化しているためデフォルトで以下のようなNativeGreetingResourceITというクラスが生成されていると思います。

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest
public class NativeGreetingResourceIT extends  {

    // Execute the same tests but in native mode.
}

@NativeImageTestを付与されたテストクラスにインテグレーションテストを記述しておくと、前述した、PomのMaven Failsafe Pluginの設定で指定されるネイティブイメージを利用したテストを実施することが可能です。

ExampleではGreetingResourceTestを拡張しており、このクラスはGreetingResourceをテストがRest Assuredで行われています。 このテストを実行されるには以下のコマンドを実行します。

$  ./mvnw verify -Pnative

このコマンドを実行すると、ネイティブイメージのビルドが行われ、その後、そのイメージを使ったテストが実施されます。

Goのembedを使う

はじめに

Goの1.16からembed packageがcoreライブラリに追加されています。結構面白い感じの機能だったのでちょっと試してみようかと思います。

go enbedとは

embedを利用すると静的ファイルをGoのプログラムに埋め込み、そこに対するアクセスを提供してくれます。embedでは以下の3つの形式でファイルを読み込むことができます

  • string
  • byte[]
  • FS

ファイルの読み込みはパッケージのディレクトリかもしくはそのサブディレクトリから読み込むことができます。

使ってみる。

環境

動作環境は以下です。

$ go version
go version go1.16 linux/amd64

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

単体のテキストファイルをstringやbyte[]として読み込む

まずは基本的な使い方として単体のテキストファイルの読み込みをしてみます。
//go:embedディレクティブをコメントとして記述することで利用することができます。
まずは以下のようなテキストファイルをembedディレクティブを記述するプログラムと同じファイル階層に置いておきます。

hello.txt

hello, world.
this is embedded.

このテキストファイルをembedでstringとして、読み込むためには以下のようにします。

package main

import (
    _ "embed"
    "fmt"
)

//go:embed hello.txt
var hello string

func main() {
    fmt.Println(hello)
}

実行してみます。

$ go build -o hello_embed
$ ./hello_embed 
hello, world.
this is embedded.

注目ポイントはふたつで、まずはembedを利用するためにパッケージのブランクインポートが必要です。
もう一つのポイントはembedディレクティブは関数の外側でパッケージグローバルに定義しておく必要があるということで例えばプログラムを以下のように変更するとエラーで落ちます。

func main() {
    //go:embed hello.txt
    var hello string
    fmt.Println(hello)
}
$ go build -o hello_embed
# github.com/samuraiball/go-sandbox
./main.go:10:4: go:embed cannot apply to var inside func

読み込んだファイルを[]byteで受け取りたい場合は単に以下のように書き換えるだけで大丈夫です。

//go:embed hello.txt
var hello []byte

func main() {
    fmt.Println(hello)
    fmt.Println(string(hello))
}

実行結果

[104 101 108 108 111 44 32 119 111 114 108 100 46 10 116 104 105 115 32 105 115 32 101 109 98 101 100 100 101 100 46]
hello, world.
this is embedded.

複数ファイルの読み込み

1つ以上の複数ファイルを読み込みたい場合はembed.FSを受け取りの型として利用することができます。
例えばhtmlディレクトリをembedを行なうプログラムファイルと同じ階層に作り以下の2つを用意します。

index1.html

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>

index2.thml

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

まずindex1.htmlと、index2.htmlを読み込むためには以下のようにします。

//go:embed html/*
var html embed.FS

func main() {

    htmlBytes, err := html.ReadFile("html/index1.html")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf(string(htmlBytes))

}

前述している通り、embed.FSで受け取ります。そして、個別のファイルを受け取る場合は[func (f FS) ReadFile(name string) ([]byte, error)](https://golang.org/pkg/embed/#FS.ReadFile)を利用し、読み込みたいディレクトリ/ファイル名を文字列で渡すと利用することができます。この戻り値は []byteになります。

実行結果は以下の通りになります。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

ここで、ファイルの読み込みはpath.Matchパターンでファイルを読み込むことができます例えば//go:embed html/*1.htmlに書き換えて先程のコードを実行すると、index1.htmlが読み込まれるようになるため、index2, err := html.ReadFile("html/index2.html")のところで落ちるようになります。

2021/03/05 19:12:12 open html/index2.html: file does not exist

また、embed.FSには他にも、func (f FS) Open(name string) (fs.File, error)func (f FS) ReadDir(name string) ([]fs.DirEntry, error)も定義されており、それぞれfs.Fileでファイルを読み込んだり、[]fs.DirEntryを読み込んだりすることができます。

複数ディレクトリからまとめてファイルを読み込む

embedは複数のディレクトリからファイルを読み込むことも可能です。
先程のindex1.htmlindex2.htmlをそれぞれhtml1html2ディレクトリを作成して格納し、それらを読み込みたい場合は以下のようにします。

//go:embed html1/* html2/*
var html embed.FS

func main() {

    index1, err := html.ReadFile("html1/index1.html")
    // エラーハンドリング省略

    index2, err := html.ReadFile("html2/index2.html")
    // エラーハンドリング省略

    fmt.Print(string(index1))

    fmt.Println()
    fmt.Printf("----------------------------------------------------")
    fmt.Println()

    fmt.Print(string(index2))
}

実行結果は先ほどと同じです。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>