LoginSignup
5

More than 3 years have passed since last update.

自作メトリクスサーバでHorizontalPodAutoscalerが使いたい!

Last updated at Posted at 2020-12-24

はじめに

CPUやメモリなどのResource Metrics API以外でHorizontal Pod Autoscaler(HPA)を利用したい時、例えばPrometheusのメトリクスを利用したい場合Prometheus AdapterKube Metrics Adapterを利用することになるかと思います。

しかし利用したいデータソースに対するカスタム/外部メトリクスサーバがない場合、例えば社内の独自監視基盤のメトリクスをHPAで利用したくなった場合はどうすべきでしょうか?
そのような状況になっても「大丈夫です、作れますよ」と言えるようになるためにメトリクスサーバを作ってみました。

カスタム/外部メトリクスサーバとは?

@Ladicle さんの「独自メトリクスによるPodの水平スケール」という記事で詳しく説明されていますので、そちらを参照することをおすすめします。ここでも簡単に説明しておくと

  • カスタムメトリクスサーバ: Kubernetes APIリソースに紐づくリソースのメトリクスを提供する
  • 外部メトリクスサーバ: Kubernetes APIリソースに紐づかないメトリクスを提供する

これら両方を実装しているものもあれば、片方だけのものもあります。
カスタムメトリクスサーバだとメトリクスとKubernetesのオブジェクトの紐付けがなんとなくのイメージで面倒臭そうだったので、今回は外部メトリクスサーバを実装しました。

実装した外部メトリクスサーバについて

成果物はこちらのリポジトリにあります。
今回はPrometheus AdapterやKube Metrics Adapterもどきを作りました。単純に設定した外部メトリクスがKubernetesから呼び出されると、それに紐づくPromQLの結果を返すような外部メトリクスサーバです。試しに作っただけなので、かなり雑な作りとなっていますがとりあえず動きます。

スクリーンショット 2020-12-22 9.44.51.png

以下のようなコンフィグを設定ファイルとして利用します。

# 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.SelectorExternalMetricInfoが引数として渡されます。
第一引数のnamespaceはHPAのnamespaceが渡され、アクセス制御やメトリクスの識別のために利用しても利用しなくても良いみたいです。
第二引数にはHPAのspec.metrics.external.metric.selectorlabels.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リソースとの紐付けも、それほど面倒ではなさそうなので、そちらで実装しても良かったかもと思いました。これでどうやら社内監視基盤用にメトリクスサーバを作れ!と言われても「大丈夫です、作れます」と言えそうです。年末年始はテレビを見ながらこれをベースに、自分だけのメトリクスサーバの実装をして家で大人しく過ごそうと思います。

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
5