2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2023

Day 6

apimachineryのQuantity型の扱い

Last updated at Posted at 2023-12-05

これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 6日目の記事です。

本記事では、GoでKubernetesリソースのメトリクスを扱う際に登場する、k8s.io/apimachineryモジュールのapi/resourceパッケージに含まれるQuantity型の扱いについてご紹介します。

k8s.io/metricsモジュール

Kubernetesクラスタ内のPodやNode、コンテナといったオブジェクトのリソース使用量を取得する際、Kubernetes Metrics APIを利用することができます。
Go言語でこれらのリソース使用量を取得するプログラムを実装する場合、k8s.io/metricsモジュールが提供する型やメソッドを利用することで、簡単にこれを実現することができます。

Quantity型の利用箇所

k8s.io/metricsモジュールの中でもコンテナメトリクスの値を持つContainerMetrics型では、対象のPodで稼働するコンテナ名とリソースの使用量を保持します。

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"`
}

リソース使用量はUsageフィールドから取得することができ、こちらのフィールドは以下で定義されるResourceList型を持ちます。

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型の値はmapになっており、cpu・memory・storage・epemeral-storageの文字列をキーとして渡すことで指定した指標に紐づくリソース使用量を取得できます。
この時、リソース使用量を表す値は以下で定義されるQuantity型を持っています。

apimachinery/pkg/api/resource/quantity.go
type Quantity struct {
	// i is the quantity in int64 scaled form, if d.Dec == nil
	i int64Amount
	// d is the quantity in inf.Dec form if d.Dec != nil
	d infDecAmount
	// s is the generated value of this quantity to avoid recalculation
	s string

	// Change Format at will. See the comment for Canonicalize for
	// more details.
	Format
}

Quantity型の扱い

Quantity型の定義と構成

Quantity型は数値を固定小数点で表します。

同じく数値を表す型で浮動小数点を扱うfloat32/64型がありますが、固定小数点の場合扱える桁数が限られる代わりに計算時の誤差が生じにくいというメリットがあります。
Quantity型で扱うことができる最も小さい桁数は10^-9です。

Quantity型の持つフィールドでは、int64AmountinfDecAmountstringFormat型の値を持っています。
ここでメトリクスの数値を表すのはint64AmountinfDecAmountstring型を持つフィールドとなっており、Fomatフィールドでは数値が表す単位系の情報を持っています。

Format型は次のように定義されています。

apimachinery/pkg/api/resource/quantity.go
// Format lists the three possible formattings of a quantity.
type Format string

const (
	DecimalExponent = Format("DecimalExponent") // e.g., 12e6
	BinarySI        = Format("BinarySI")        // e.g., 12Mi (12 * 2^20)
	DecimalSI       = Format("DecimalSI")       // e.g., 12M  (12 * 10^6)
)

コメントで例として書かれていますが、これらはそれぞれ以下のように数値を表します。

  • DecimalExponent:数値を10の累乗で表す。単位は上記の例のe6で表す。
  • BinarySI:数値を2の累乗で表す。単位は上記の例のMiで表す。
  • DecimalSI:数値を10の累乗で表す。単位は上記の例のMで表す。

数値を表す型のうち、int64AmountinfDecAmount型はそれぞれ次のように定義されています。

apimachinery/pkg/api/resource/amount.go
// int64Amount represents a fixed precision numerator and arbitrary scale exponent. It is faster
// than operations on inf.Dec for values that can be represented as int64.
// +k8s:openapi-gen=true
type int64Amount struct {
	value int64
	scale Scale
}
...省略
// infDecAmount implements common operations over an inf.Dec that are specific to the quantity
// representation.
type infDecAmount struct {
	*inf.Dec
}

infDecAmount型はinf.Dec型のポインタとなっており、inf.Dec型は以下のように定義されています。

inf/dec.go
type Dec struct {
	unscaled big.Int
	scale    Scale
}

このことから、それぞれのint64AmountinfDecAmount型はどちらも、整数値と小数点の位置を表すフィールドを合わせて表現される値だとわかります。

int64Amount・infDecAmount型が持つscaleの違い

ここで、それぞれが持つScale型はそれぞれ別のパッケージで定義されているものであり、その振る舞いが異なる点に注意してください。

int64Amount型は0以上の大きな桁数を持つ数値を扱うのに特化しており、infDecAmount型は小数点以下の桁を持つ数値を扱うのに特化しています。

例えば1,000を表す場合、それぞれの型では以下のような構造体で表現されます。
k8s.io/api/resourceパッケージのScaleを使ったint64Amount型と、infDecAmount型の内部で利用されているinfパッケージのDec型ではscaleの値の正負が異なることがわかります。

// int64Amountの場合
&int64Amount{
  value: 1,
  scale: 3,
}

// inf.Dec(infDecAmount)の場合
&inf.Dec{
  unscaled: 1,
  scale: -3,
}

Quantity型の構造体から各フィールドの値を取り出す際のメソッドを見ると、それぞれの型が扱う値の違いが現れています。

Quantity型ではFormatフィールド以外privateとなっており、メソッドを通してのみ取得可能になっています。
Quantity型は多くのメソッドを持っており、iint64Amount型), dinfDecAmount型)フィールドの値を取得するためのメソッドとして、それぞれAsInt64()AsDec()メソッドが用意されています。

AsInt64()メソッドは次のように実装されています。

apimachinery/pkg/api/resource/quantity.go
// AsInt64 returns a representation of the current value as an int64 if a fast conversion
// is possible. If false is returned, callers must use the inf.Dec form of this quantity.
func (q *Quantity) AsInt64() (int64, bool) {
	if q.d.Dec != nil {
		return 0, false
	}
	return q.i.AsInt64()
}
apimachinery/pkg/api/resource/amount.go
// AsInt64 returns the current amount as an int64 at scale 0, or false if the value cannot be
// represented in an int64 OR would result in a loss of precision. This method is intended as
// an optimization to avoid calling AsDec.
func (a int64Amount) AsInt64() (int64, bool) {
	if a.scale == 0 {
		return a.value, true
	}
	if a.scale < 0 {
		// TODO: attempt to reduce factors, although it is assumed that factors are reduced prior
		// to the int64Amount being created.
		return 0, false
	}
	return positiveScaleInt64(a.value, a.scale)
}

Quantity型のAsInt64()メソッドは内部でInt64Amount型のAsInt64()メソッドを呼んでいます。
Int64Amount型のAsInt64()メソッドではscaleフィールドの値が0以下である場合に値として0を返すようになっています。

一方で、Quantity型のAsDec()メソッドは次のように実装されています。

apimachinery/pkg/api/resource/quantity.go
// AsDec returns the quantity as represented by a scaled inf.Dec.
func (q *Quantity) AsDec() *inf.Dec {
	if q.d.Dec != nil {
		return q.d.Dec
	}
	q.d.Dec = q.i.AsDec()
	q.i = int64Amount{}
	return q.d.Dec
}
apimachinery/pkg/api/resource/amount.go
// AsDec returns an inf.Dec representation of this value.
func (a int64Amount) AsDec() *inf.Dec {
	var base inf.Dec
	base.SetUnscaled(a.value)
	base.SetScale(inf.Scale(-a.scale))
	return &base
}

こちらは内部で、Int64Amount型のAsDec()メソッドを呼び出し、その戻り値をQuantity型のdフィールドにセットしています。
ここでInt64Amount型のAsDec()メソッドではInt64Amount型のscaleの値の正負を逆転させて、inf.Dec型の値への変換を行なってます。
このことからそれぞれの型の構造体が持つscaleフィールドの正負が逆であるということがわかるかと思います。

名前からすると当然かもしれませんが、Int64Amount型が表すscaleの値は整数部分の桁数、InfDecAmount型が表すscaleの値は小数部分の桁数を表すことがわかりました。

Quantity型を扱う例

次に実際にQuantity型の値を取得し、数値の大きさによって確認しやすい形式に変換して出力してみます。

例として、以下のようにコンテナのCPU・メモリ使用量を取得するプログラムを作成し、取得した値を出力してみます。

fetch_container_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)

	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.\nMemory: %#v bytes.\n", cpu, memory)
	}
}

こちらのプログラムでは、実行時にname引数で指定した値と名前が一致するPodを取得し、k8s.io/metricsモジュールを利用してそのPodで稼働するコンテナのCPU・メモリ使用量を取得し、出力しています。
fmt.Printf()で出力しているcpumemory変数はQuantity型の値を持っています。

プログラムの実行結果は次のようになります。

go run fetch_container_metrics.go --kubeconfig $HOME/.kube/config --name run-stress

Cpu: &resource.Quantity{i:resource.int64Amount{value:1000453996, scale:-9}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"1000453996n", Format:"DecimalSI"} core.
Memory: &resource.Quantity{i:resource.int64Amount{value:1154269184, scale:0}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"", Format:"BinarySI"} bytes.

出力からCPU使用量の値はDecimalSIの値を持ち、メモリ使用量の値はBinarySIの値を持つことがわかります。
また、どちらの値もint64Amountの値として表されていることがわかります。

メモリ使用量はバイトで表されますが、CPU使用量はコア数で表されます。
一般にメモリ使用量は数Kiバイト以上になることが多く、比較的大きい整数値を持ちます。

一方で、CPUのコア数に関しては多くても2桁程度の値になることが多いかと思います。
またクラウドを利用する場合、VMは仮想コアを持ち、例えば0.5コアに相当する500 mCPUが割り当てられるなど小数点以下の値を持つコア数として表現されるケースがあります。

それでは、上記の例で取得したCPU・メモリ使用量を表すQuantity型の値を先ほど紹介したAsInt64()メソッドとAsDec()メソッドを使って確認しやすい値に変換します。
小数点以下の値も支配的であるCPU使用量はAsDec()型で取得し、大きな整数値となることが予想されるメモリ使用量はAsInt64()型で取得します。

先に示した例を微修正し、AsDec()AsInt64()メソッドを通して取得したCPU・メモリの使用量を出力するようにします。

	for _, containerMetrics := range podMetrics.Containers {
		cpu := containerMetrics.Usage.Cpu()
		memory := containerMetrics.Usage.Memory()
		memoryInt64Amount, _ := memory.AsInt64()
		fmt.Printf("Cpu: %v core.\nMemory: %v bytes.\n", cpu.AsDec(), memoryInt64Amount)
	}

修正後のプログラムの実行結果は次になります。

go run fetch_container_metrics.go --kubeconfig $HOME/.kube/config --name run-stress

Cpu: 0.999451490 core.
Memory: 1154269184 bytes.

このようにしてQuantity型の戻り値から、適切なメソッドを利用して確認しやすい表示形式で出力することができました。

(余談) vCPU → mCPUへの単位変換

CPU使用量についてvCPUmCPUに単位を変更したいケースがあるかもしれません。

この場合はinf.Dec型が持つRoundメソッドにより、Scaleを変更し、Unscaled()メソッドによりunscaledフィールドの値を取得することでvCPU(コア数)からmCPU形式での値に変換可能です。

		cpu := containerMetrics.Usage.Cpu().AsDec()
		roundedCPU := cpu.Round(cpu, 3, inf.RoundDown)
        unscaledVal, _ := roundedCPU.Unscaled()
        scaleVal := roundedCPU.Scale()
  		fmt.Printf("Cpu unscaled: %v, Cpu scale: %v., unscaledVal, scaleVal)

実装を修正し、再度プログラムを実行すると次の出力を得ます。

go run fetch_container_metrics.go --kubeconfig $HOME/.kube/config --name run-stress     

Cpu unscaled: 994, Cpu scaled: 3

Round()メソッドの第3引数でRoundDownを指定しているため、Scaleで指定された以下の値は切り捨てられています。
ここでは3を指定しているため、小数点以下第4位で値が切り捨てられています。

CPUの使用量は0.994 * 10^(-3) vCPUとなっているため、10^3をかけた値がmCPU形式での値になります。
そのため、Round()メソッドの戻り値からUnscaled()メソッドで取得したunscaledフィールドの値がそのままmCPU形式の値になり、単位の変換を行うことができました。

まとめ

本記事では、KubernetesのコンテナメトリクスをGoで扱う際に登場するQuantity型の扱いについてご紹介しました。

Quantity型と各フィールドの値について扱う際には以下の観点を意識しておくと、混乱しないかと思います。

  • Quantity型は数値と単位を持つこと
  • 数値を表す型は、数値の大きさにより使い分けできること
  • それぞれの型で表される数値は整数値と小数点の桁数から表現されること

参考

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?