これは 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で稼働するコンテナ名とリソースの使用量を保持します。
// 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
型を持ちます。
// 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
型を持っています。
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
型の持つフィールドでは、int64Amount
・infDecAmount
・string
・Format
型の値を持っています。
ここでメトリクスの数値を表すのはint64Amount
・infDecAmount
・string
型を持つフィールドとなっており、Fomat
フィールドでは数値が表す単位系の情報を持っています。
Format
型は次のように定義されています。
// 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で表す。
数値を表す型のうち、int64Amount
・infDecAmount
型はそれぞれ次のように定義されています。
// 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
型は以下のように定義されています。
type Dec struct {
unscaled big.Int
scale Scale
}
このことから、それぞれのint64Amount
・infDecAmount
型はどちらも、整数値と小数点の位置を表すフィールドを合わせて表現される値だとわかります。
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
型は多くのメソッドを持っており、i
(int64Amount型
), d
(infDecAmount型
)フィールドの値を取得するためのメソッドとして、それぞれAsInt64()
・AsDec()
メソッドが用意されています。
AsInt64()
メソッドは次のように実装されています。
// 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()
}
// 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()
メソッドは次のように実装されています。
// 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
}
// 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・メモリ使用量を取得するプログラムを作成し、取得した値を出力してみます。
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()
で出力しているcpu
、memory
変数は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使用量についてvCPU
→ mCPU
に単位を変更したいケースがあるかもしれません。
この場合は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型は数値と単位を持つこと
- 数値を表す型は、数値の大きさにより使い分けできること
- それぞれの型で表される数値は整数値と小数点の桁数から表現されること