LoginSignup
4
1

Goで作る Kubernetes Podで稼働するコンテナメトリクスを取得するプログラム

Last updated at Posted at 2023-12-04

これは 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のマニフェストは次になります。

sample_pod.yaml
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.limitscpu: 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で稼働中のコンテナメトリクスを取得するプログラムの実装例は以下になります。

fetch_metrics.go
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型を持っており、型は次のように定義されています。

metrics/pkg/client/clientset/versioned/clientset.go
type Clientset struct {
	*discovery.DiscoveryClient
	metricsV1alpha1 *metricsv1alpha1.MetricsV1alpha1Client
	metricsV1beta1  *metricsv1beta1.MetricsV1beta1Client
}

client-gokubernetesパッケージで提供されるClientsetと同様にこちらのClientsetでも複数のGroup-Versionに対応するクライアントを持っています。
今回取得したいPodのメトリクスはmetrics Groupのv1beta1 Versionになるため、クライアントのMetricsV1beta1()メソッドを呼んでいます。

対象のNamespace名、Pod名を指定して結果を取得する流れも、client-gokubernetesパッケージでPodを取得する場合と同様です。

ここで、Get()メソッドは次のPodMetrics型の値を返します。

metrics/pkg/apis/metrics/v1beta1/types.go
// 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型は、以下のように定義されています。

metrics/pkg/apis/metrics/v1beta1/types.go
// 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モジュールで次のように定義されています。

api/core/v1/types.go
// 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を介してコンテナメトリクスが取得できることを確認できました。

注意点として、上記の実装例で取得したメトリクスはある時点でのスナップショット的な値になります。
実際のプログラムでは、これらのメトリクスは時間により変化するため、一定間隔で取得した平均を取るなどして集計すると良さそうです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1