これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 5日目の記事です。
本記事では、Go言語でKubernetes Podで実行中のコンテナメトリクスを取得する方法をご紹介します。
Kubernetesでは、アドオンのコンポーネントであるmetrics-serverをデプロイすることで、Metrics APIを介してクラスタ内で実行中のPodやNodeのリソース使用量を取得することができます。
こちらのMetrics APIを利用して対象のPodで稼働中のコンテナメトリクスを取得するプログラムをGo言語で作成しつつ、各処理について説明します。
k8s.io/metrics
モジュールの紹介
Kubernetes Organization配下のリポジトリであるmetrics
リポジトリでは、k8s.io/metrics
モジュールを提供しています。
このモジュールには、Kubernetes Metrics APIに関連する型定義とクライアントが実装されています。
本記事で扱うプログラムではこちらのk8s.io/metrics
モジュールを利用して実行中のコンテナメトリクスを取得します。
事前準備
プログラムを動かす前準備として、まず各環境でのMetrics APIをの有効化が必要です。
本記事ではKubernetesクラスタの動作環境としてminikubeを利用しているため、次のコマンドでmetrics-serverを有効化します。
minikube addons enable metrics-server
Podをデプロイした後で、以下のコマンドを実行し、結果が取得できればMetrics APIが動作しています。
kubectl top pods
NAME CPU(cores) MEMORY(bytes)
run-stress 944m 1100Mi
メトリクス取得対象のコンテナを稼働させるPodのマニフェストは次になります。
apiVersion: v1
kind: Pod
metadata:
name: run-stress
spec:
containers:
- name: stress-ng
image: ubuntu:20.04
command:
- "/bin/bash"
- "-c"
- |
apt-get update && apt-get install -y stress-ng
stress-ng --cpu 1 --vm 1 --vm-bytes 1G
resources:
limits:
cpu: "1"
memory: "2G"
コンテナでの実行コマンドとしてstress-ng
コマンドを利用しています。
こちらのコマンドを利用することで対象のPodに一定の負荷をかけることができます。
sample_pod.yaml
ではstress-ng
コンテナのresources.limits
でcpu: 1, memory: 2G
を指定した上で、stress-ng
コマンドの実行引数でcpu: 1, memory: 1G
を指定しています。
そのため、コンテナのCPU、メモリ使用量の予測値はcpu: 1vCPU, memory: 1G
になります。
マニフェストの作成後は次のコマンドでメトリクス取得対象のPodを作成してください。
kubectl apply -f sample_pod.yaml
これまでの手順により、稼働中のコンテナのメトリクスを取得する前準備が完了しました。
まずはkubectl top
コマンドで、上記の手順でデプロイしたコンテナのメトリクスを取得してみます。
Podが作成されてしばらく経ってから、次のようにk top
コマンドを実行すると稼働中のコンテナのCPU、メモリ使用量を取得することができます。
k top pod run-stress --containers
POD NAME CPU(cores) MEMORY(bytes)
run-stress stress-ng 999m 1100Mi
またコマンドの実行結果から、ほぼ想定通りの使用量が取得できていることが確認できました。
次節では、Go言語で同様のコンテナメトリクスを取得するプログラムの実装例をご紹介します。
k8s.io/metrics
モジュールを使ったGoプログラムの実装例
k8s.io/metrics
モジュールを使って、Goで対象のPodで稼働中のコンテナメトリクスを取得するプログラムの実装例は以下になります。
package main
import (
"context"
"flag"
"fmt"
"os"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
metricsClient "k8s.io/metrics/pkg/client/clientset/versioned"
)
var (
kubeconfig string
podName string
)
func main() {
// コマンド引数の受け取り
flag.StringVar(&podName, "name", "", "Fetch metrics target Pod name")
flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
flag.Parse()
// クライアントの初期化
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
clientset, _ := metricsClient.NewForConfig(config)
// Podメトリクスの取得
podMetrics, err := clientset.MetricsV1beta1().PodMetricses("default").Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, containerMetrics := range podMetrics.Containers {
cpu := containerMetrics.Usage.Cpu()
memory := containerMetrics.Usage.Memory()
fmt.Printf("Cpu: %v core, Memory: %v bytes.", cpu.AsDec(), memory.AsDec())
}
}
利用する型・メソッドの説明
Metrics APIへのリクエストはpodMetrics, err := clientset.MetricsV1beta1().PodMetricses("default").Get(context.TODO(), podName, metav1.GetOptions{})
の箇所で行なっています。
リクエスト時のクライアントはk8s.io/metrics/pkg/client/clientset/versioned
パッケージのClientset
型を持っており、型は次のように定義されています。
type Clientset struct {
*discovery.DiscoveryClient
metricsV1alpha1 *metricsv1alpha1.MetricsV1alpha1Client
metricsV1beta1 *metricsv1beta1.MetricsV1beta1Client
}
client-go
のkubernetes
パッケージで提供されるClientset
と同様にこちらのClientset
でも複数のGroup-Version
に対応するクライアントを持っています。
今回取得したいPodのメトリクスはmetrics Groupのv1beta1 Versionになるため、クライアントのMetricsV1beta1()
メソッドを呼んでいます。
対象のNamespace名、Pod名を指定して結果を取得する流れも、client-go
のkubernetes
パッケージでPodを取得する場合と同様です。
ここで、Get()
メソッドは次のPodMetrics
型の値を返します。
// PodMetrics sets resource usage metrics of a pod.
type PodMetrics struct {
metav1.TypeMeta `json:",inline"`
// Standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// The following fields define time interval from which metrics were
// collected from the interval [Timestamp-Window, Timestamp].
Timestamp metav1.Time `json:"timestamp" protobuf:"bytes,2,opt,name=timestamp"`
Window metav1.Duration `json:"window" protobuf:"bytes,3,opt,name=window"`
// Metrics for all containers are collected within the same time window.
Containers []ContainerMetrics `json:"containers" protobuf:"bytes,4,rep,name=containers"`
}
PodMetrics
型のContainers
フィールドでは[]ContainerMetrics
型を持っているため、対象のPodで稼働するコンテナのメトリクスを取得するためにはこちらのフィールド値を参照します。
要素となる、ContainerMetrics
型は、以下のように定義されています。
// ContainerMetrics sets resource usage metrics of a container.
type ContainerMetrics struct {
// Container name corresponding to the one from pod.spec.containers.
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// The memory usage is the memory working set.
Usage v1.ResourceList `json:"usage" protobuf:"bytes,2,rep,name=usage,casttype=k8s.io/api/core/v1.ResourceList,castkey=k8s.io/api/core/v1.ResourceName,castvalue=k8s.io/apimachinery/pkg/api/resource.Quantity"`
}
コンテナのリソース使用量については、ContainerMetrics
型のUsage
フィールドの値を参照することで取得できることがわかります。
Usage
フィールドの値が持つ、v1.ResourceList
型については、k8s.io/api
モジュールで次のように定義されています。
// ResourceName is the name identifying various resources in a ResourceList.
type ResourceName string
// Resource names must be not more than 63 characters, consisting of upper- or lower-case alphanumeric characters,
// with the -, _, and . characters allowed anywhere, except the first or last character.
// The default convention, matching that for annotations, is to use lower-case names, with dashes, rather than
// camel case, separating compound words.
// Fully-qualified resource typenames are constructed from a DNS-style subdomain, followed by a slash `/` and a name.
const (
// CPU, in cores. (500m = .5 cores)
ResourceCPU ResourceName = "cpu"
// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
ResourceMemory ResourceName = "memory"
// Volume size, in bytes (e,g. 5Gi = 5GiB = 5 * 1024 * 1024 * 1024)
ResourceStorage ResourceName = "storage"
// Local ephemeral storage, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
// The resource name for ResourceEphemeralStorage is alpha and it can change across releases.
ResourceEphemeralStorage ResourceName = "ephemeral-storage"
)
...省略
// ResourceList is a set of (resource name, quantity) pairs.
type ResourceList map[ResourceName]resource.Quantity
こちらの実装を見ると、ResourceList
型の値はcpu・memory・storage・epemeral-storageをキーにして、それぞれに対応する値をQuantity
型で返すことがわかります。
fetch_metrics.go
ではCpu()
、Memory()
メソッドにより、これらcpu・memoryに対応する値を取得しています。
ここで取得できるQuantity
型の値は整数と固定小数点を持っています。
Quantity型についてここでは詳しく触れませんが、k8s.io/apimachinery
モジュールのapi/resource
パッケージで定義されているため、気になる方はご参照ください。
最後に、Quantity型の戻り値からAsDec()
メソッドを呼ぶことでgo-inf/inf
モジュールのinf
パッケージが提供するDec
型の値としてCPU、メモリの値を取得し、fmt.Printf
で出力しています。
補足として、今回は取得対象のPodで稼働するコンテナが1つであったため特に絞り込みをしませんでしたが、複数コンテナが稼働するPodを対象とする場合は、ただContainers
フィールドでループするだけだと全てのコンテナのCPU、メモリ使用量が出力されてしまいます。
この場合はContainerMetrics
型のName
フィールドの値を参照して出力対象の絞り込みを行うのが良さそうです。
実行結果
プログラムの実行結果は次になります。
go run fetch_container_metrics.go --kubeconfig $HOME/.kube/config --name run-stress
Cpu: 0.999806090 core, Memory: 1154260992 bytes.%
プログラムの実行結果から、k8s.io/metrics
モジュールのクライアントを使って稼働中のコンテナのCPU、メモリ使用量が取得できていることがわかります。
デフォルトのまま単位の変換をしていないため多少見づらいですが、それぞれの使用量はCPUで約1コア、メモリで約1.15Gとなっており、先ほどkubectl top
コマンドで取得したものとほぼ同じ値が取得できていることが確認できました。
まとめ
本記事では、k8s.io/metrics
モジュールを利用して、Kubernetes Podで稼働するコンテナのメトリクスを取得するプログラムの実装例を紹介しました。
こちらのモジュールを利用することにより、Metrics APIを介してコンテナメトリクスが取得できることを確認できました。
注意点として、上記の実装例で取得したメトリクスはある時点でのスナップショット的な値になります。
実際のプログラムでは、これらのメトリクスは時間により変化するため、一定間隔で取得した平均を取るなどして集計すると良さそうです。