はじめに
CPUやメモリなどのResource Metrics API以外でHorizontal Pod Autoscaler(HPA)を利用したい時、例えばPrometheusのメトリクスを利用したい場合Prometheus AdapterやKube Metrics Adapterを利用することになるかと思います。
しかし利用したいデータソースに対するカスタム/外部メトリクスサーバがない場合、例えば社内の独自監視基盤のメトリクスをHPAで利用したくなった場合はどうすべきでしょうか?
そのような状況になっても「大丈夫です、作れますよ」と言えるようになるためにメトリクスサーバを作ってみました。
カスタム/外部メトリクスサーバとは?
@Ladicle さんの「独自メトリクスによるPodの水平スケール」という記事で詳しく説明されていますので、そちらを参照することをおすすめします。ここでも簡単に説明しておくと
- カスタムメトリクスサーバ: Kubernetes APIリソースに紐づくリソースのメトリクスを提供する
- 外部メトリクスサーバ: Kubernetes APIリソースに紐づかないメトリクスを提供する
これら両方を実装しているものもあれば、片方だけのものもあります。
カスタムメトリクスサーバだとメトリクスとKubernetesのオブジェクトの紐付けがなんとなくのイメージで面倒臭そうだったので、今回は外部メトリクスサーバを実装しました。
実装した外部メトリクスサーバについて
成果物はこちらのリポジトリにあります。
今回はPrometheus AdapterやKube Metrics Adapterもどきを作りました。単純に設定した外部メトリクスがKubernetesから呼び出されると、それに紐づくPromQLの結果を返すような外部メトリクスサーバです。試しに作っただけなので、かなり雑な作りとなっていますがとりあえず動きます。
以下のようなコンフィグを設定ファイルとして利用します。
# PrometheusのURL
url: http://prometheus.kube-system:9090
queries:
# External Metrics名: PromQL
prom_up: up{component="prometheus"}
http_requests_total: http_requests_total
go_gc_duration_seconds_count: go_gc_duration_seconds_count
Prometheusに対するクエリ結果に含まれるラベルを外部メトリクスのラベルとして利用できるようにしました。
そのためPromQLの段階でラベルを用いて絞り込めますが、HPAのselectorで絞り込むこともできます。
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
...
spec:
metrics:
- type: External
external:
metric:
name: http_requests_total
# Prometheusのメトリクスに含まれるラベルで絞り込み可能
selector:
matchLabels:
component: web-app
target:
type: AverageValue
averageValue: 30
実装
Custom Metrics ServerにはBoilerplateが公式から用意されており、それを利用していけば比較的簡単に作れます。やらなければならないのはインターフェースを一つ実装するだけです。このリポジトリにはメトリクスサーバを作る上で便利なヘルパー関数も含まれています。
サンプル実装がこちらにあり、カスタムメトリクスサーバの作り方に関してはドキュメントも用意されています。
実装するインターフェースはpkg/provider
の中にあり、Custom Metrics Serverを実装する場合はCustomMetricsProviderを実装します。
type CustomMetricsProvider interface {
// 条件にマッチするカスタムメトリクスのリストを返す
GetMetricByName(name types.NamespacedName, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error)
GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error)
// その時点におけるメトリクスサーバで利用可能なメトリクス情報のリストを返す
ListAllMetrics() []CustomMetricInfo
}
External Metrics Serverを実装する場合はExternalMetricsProviderを実装します。今回は外部メトリクスサーバなので、こちらを実装しています。
type ExternalMetricsProvider interface {
// 条件にマッチする外部メトリクスのリストを返す
GetExternalMetric(namespace string, metricSelector labels.Selector, info ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error)
// その時点におけるメトリクスサーバで利用可能なメトリクス情報のリストを返す
ListAllExternalMetrics() []ExternalMetricInfo
}
GetExternalMetric
GetExternalMetric
は呼び出される時、namespace(string)
とlabels.Selector
とExternalMetricInfo
が引数として渡されます。
第一引数のnamespaceはHPAのnamespaceが渡され、アクセス制御やメトリクスの識別のために利用しても利用しなくても良いみたいです。
第二引数にはHPAのspec.metrics.external.metric.selector
がlabels.Selector
として渡されます。labels.Selectorを利用するために、外部から取得するメトリクス(今回の場合はPrometheus)のラベルやタグをKubernetesのlabels.Setへ変換することで、labels.Selectorを利用できるようになるため、今回はそのような方法をとりました。
ls := labels.Set{}
// PrometheusのラベルをKubernetesのラベルへ変換
for k, v := range samples[i].Metric {
ls[string(k)] = string(v)
}
// labels.Selectorを満たすかどうか確認
if !metricSelector.Matches(ls) {
continue
}
戻り値にはExternalMetricValueList
を返します。名前の通りメトリクスのリストのオブジェクトとなります。それぞれのメトリクスにはタイムスタンプ、メトリクス名、ラベル、メトリクスの値などを含むオブジェクトとなっています。このリストに含まれるメトリクスの合計値がHPAの結果の値として計算されることとなり、以下はKubernetes側のその部分のコードです。
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/controller/podautoscaler/replica_calculator.go#L330-L337
// or
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/controller/podautoscaler/replica_calculator.go#L352-L359
metrics, timestamp, err := c.metricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("unable to get external metric %s/%s/%+v: %s", namespace, metricName, metricSelector, err)
}
utilization = 0
for _, val := range metrics {
utilization = utilization + val
}
第三引数のExternalMetricInfo
はメトリクス名だけを含んでいる情報です。今回作ったメトリクスサーバでは、この情報を用いて紐づくPromQLをひっぱってきました。
type ExternalMetricInfo struct {
Metric string
}
ListAllExternalMetrics
ListAllExternalMetrics
では外部メトリクスとして利用可能なメトリクス名の一覧を返す必要があります。コンフィグに記載されているメトリクス名を単純にExternalMetricInfo
のリストとして返すだけの実装としてあります。
Providerの登録
あとは上記の実装したProviderをAdapterBaseの関数WithExternalMetrics
に渡して、Run
するだけで、いい感じにAPI Aggregation LayerのAPIServiceに登録可能なカスタムAPIサーバとして起動してくれます。
(AdapterBaseという名前の通り、このような書き方をする想定のものだと思うのですが今回は簡単のために以下のように利用しました。)
下記のコードは一部説明を簡単にするために省略しています。
func main() {
cmd := basecmd.AdapterBase{}
provider, err := NewPrometheusProvider(config)
if err != nil {
klog.Fatal(err)
}
cmd.WithExternalMetrics(provider)
klog.Infof("starting sample metrics server")
if err := cmd.Run(wait.NeverStop); err != nil {
klog.Fatalf("unable to run custom metrics adapter: %v", err)
}
}
あとはこれを適切なRole/ClusterRoleを与えて動かし、APIServiceとして登録すれば動きます。
メトリクスサーバとしてどの程度権限が必要になるのかはきちんと調べていなかったため、とりあえずサンプル実装のものに対して外部メトリクス用に少し修正したものでデプロイしました。
正常にデプロイが完了し、設定が適切なものであれば、通常の外部メトリクスとしてHPAから利用できるようになっているはずです。
最後に
思っていたよりずっと簡単にメトリクスサーバを自作することができました。ドキュメントを見る限り、カスタムメトリクスサーバで必要なKubernetes APIリソースとの紐付けも、それほど面倒ではなさそうなので、そちらで実装しても良かったかもと思いました。これでどうやら社内監視基盤用にメトリクスサーバを作れ!と言われても「大丈夫です、作れます」と言えそうです。年末年始はテレビを見ながらこれをベースに、自分だけのメトリクスサーバの実装をして家で大人しく過ごそうと思います。