istioのメトリクス(Custom Metrics)を使ってHPA(Horizontal Pod Autoscaler)を設定する

9 min読了の目安(約8400字TECH技術記事

こんにちは、Lapi(@dragoneena12)です。

最近SREインターンをさせていただいている株式会社HERPさんで、istioのメトリクス(custom metrics)をもとにHPAを設定する機会があったのですが、特に日本語だとなかなか情報が限定的だったので記事を書いてみました。できるだけ詳しく書いたので、参考になると幸いです。

HPAで使うメトリクスの種類

HPAは何らかのメトリクス値をもとにPod数を自動的に増減させる機能です。使えるメトリクスには以下の3種類があります(Support for metrics APIs | Horizontal Pod Autoscaler user guide)。

  • Resource Metrics
    • CPU使用率、メモリ使用率など
  • Custom Metrics
    • kubernetesクラスタ内のリソースと関連付けられるメトリクス(requests_per_second, 99_percentile_durationなど)
  • External Metrics
    • kubernetesクラスタ内のリソースとは関係しないメトリクス(外部のロードバランサーのメトリクスなど)

今回はrequests_per_secondをもとにHPAを設定したかったので、Custom Metricsを使うことにしました。

istioで得られるメトリクス

istioを導入するとRequest Countなど様々なメトリクスをサービスごとに収集することができます(Istio Standard Metrics)。今回はこの istio_requests_total を基にHPAを設定しました。

https://istio.io/latest/

prometheus-adapter

prometheus-adapterとは

istio_requests_total メトリクスはprometheusに収集されていますが、HPAでこの値を使うためにはkubernetesのカスタムメトリクスAPI(custom.metrics.k8s.io)に登録してあげる必要があります。
これをできるのが kubernetes-sigs/prometheus-adapter です。以前までDirectXMan12の個人リポジトリでしたが、最近kubernetes-sigsに移行されました

https://github.com/kubernetes-sigs/prometheus-adapter

インストール・設定方法

インストールはhelmで行えて、values.yaml でprometheusのどのような値をメトリクスAPIに登録するかを設定できます。詳しくは公式リポジトリ内のMetrics Discovery and Presentation Configuration に書かれていますが、簡単に言うと以下の4つにより構成されています。

  • Discovery
    • seriesQuery として設定する部分
    • adapterが登録するメトリクスのラベルを確認するクエリ
  • Association
    • resources として設定する部分
    • 得られたメトリクスをKubernetesのどのリソースと関連付けるかを設定する
  • Naming
    • name として設定する部分
    • メトリクスの命名
  • Querying
    • metricsQuery として設定する部分
    • 実際にメトリクス値を算出するクエリ

今回はこれらを以下のように設定しました。

rules:
  custom:
  - seriesQuery: 'istio_requests_total{reporter="destination", destination_service_namespace=~"ns1|ns2|ns3", destination_service_name=~"svc1|svc2|svc3"}'
    resources:
      overrides:
        destination_service_namespace: {resource: "namespace"}
        destination_service_name: {resource: "service"}
    name:
      as: "requests_per_second"
    metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>, reporter="destination"}[5m])) by (<<.GroupBy>>)'

ポイントとして、istio_requests_total はsourceとdestinationの双方から報告されるため、reporter="destination" と指定しています。また istio_requests_total はサービスごとに算出されているため、resources でサービスごとにメトリクス値を関連付けしていきます。

メトリクスが登録されていることを確認する

下記のコマンドでカスタムメトリクスがサービスに関連付けて登録されていることを確認できます。

$ kubectl get --raw '/apis/custom.metrics.k8s.io/v1beta1/namespaces/ns1/services/svc1/requests_per_second' | jq
{
  "kind": "MetricValueList",
  "apiVersion": "custom.metrics.k8s.io/v1beta1",
  "metadata": {
    "selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/ns1/services/svc1/requests_per_second"
  },
  "items": [
    {
      "describedObject": {
        "kind": "Service",
        "namespace": "ns1",
        "name": "svc1",
        "apiVersion": "/v1"
      },
      "metricName": "requests_per_second",
      "timestamp": "2021-02-03T11:21:30Z",
      "value": "34857m",
      "selector": null
    }
  ]
}

HPAの設定

Custom MetricsをもとにHPAを設定する

次にHPAのマニフェストファイルを書いていきます。今回は下記のように設定しました。

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa1
  namespace: ns1
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: deployment1
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Object
    object:
      describedObject:
        apiVersion: v1
        kind: Service
        name: svc1
      metric:
        name: requests_per_second
      target:
        type: AverageValue
        averageValue: 5

spec.metrics.typeには以下の4種類が設定できます。

  • Resource
    • CPU使用率、メモリ使用率などのリソースメトリクス
  • Pods
    • Podごとに関連付けられたカスタムメトリクス
  • Object
    • Pod以外と関連付けられたカスタムメトリクス
  • External
    • kubernetesクラスタ内のリソースとは関係しないメトリクス

したがって今回はObjectを設定することになります。
describedObject ではカスタムメトリクスが関連付けられたリソースを設定し、metric でメトリクス名を指定します。
target ではメトリクス値に対しPod数をどのように増減させるかを指定します。これには ValueAverageValue の2つの指定方法があります。
どちらもメトリクス値が設定値より大きければPodを増やし、設定値より一定以上下回ればPodを減らすという点では同じですが、Value はメトリクスAPIから得られた値と設定値をそのまま比較し、AverageValue はメトリクスAPIから得られた値を現在のPod数で割った値と設定値を比較する点で異なります。したがってPod数が増えることで減少する値(99_Percentile_durationなど)をメトリクス値として用いる場合は Value を、Pod数とは関係のない値(serviceに対するrequests_countなど)をメトリクス値として用いる場合は AverageValue を使うのが適当です。

Object metrics support target types of both Value and AverageValue. With Value, the target is compared directly to the returned metric from the API. With AverageValue, the value returned from the custom metrics API is divided by the number of Pods before being compared to the target.
https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics

ArgoCDを使っている場合の注意点

deploymentなどにおけるreplicasの指定を無視する

HPAによって対象となるdeploymentなどにおける replicas の設定値が書き換えられますが、ArgoCDによって管理している場合はこれによって元のdeploymentがOutOfSyncになってしまいます。これは下記のようなignoreDifferencesの設定によって回避することができます。

ignoreDifferences:
  - group: apps
    kind: Deployment
    name: deployment1
    namespace: ns1
    jsonPointers:
    - /spec/replicas

HPAリソースが常にOutOfSyncとなってしまう問題

HPA ControllerがHPAのマニフェストを書き換えてしまうため、ArgoCDで常にOutOfSyncとなってしまう問題がこちらのissue で報告されています。リソースメトリクスを用いている場合にはspec.metricsの順番を入れ替える(memoryが先でcpuは後)ことで回避できます。

For Horizontal Pod Autoscaling (HPA) objects, the HPA controller is known to reorder spec.metrics in a specific order. See kubernetes issue #74099. To work around this, you can order spec.metrics in Git in the same order that the controller prefers.
https://argoproj.github.io/argo-cd/user-guide/diffing/#diffing-customization

しかしカスタムメトリクスを用いている場合ではspec.metrics.object.target.valueがHPA Controllerによって差し込まれるため、以下のように何らかの正の値に設定してあげる必要がありました。これについてはあまり情報が見つからなかったです。

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa1
  namespace: ns1
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: deployment1
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Object
    object:
      describedObject:
        apiVersion: v1
        kind: Service
        name: svc1
      metric:
        name: requests_per_second
      target:
        type: AverageValue
        averageValue: 5
        value: 1  # HPA Controller によって自動的に差し込まれ、OutOfSyncとなるのを防ぐ設定

HPAの動作確認

以上でHPAが設定できたはずなので、以下のコマンドで動作状況を確認できます。

$ kubectl get hpa.v2beta2.autoscaling hpa1 -n ns1 -o yaml
...
status:
  conditions:
    - lastTransitionTime: '2021-02-04T06:45:53Z'
      message: recommended size matches current size
      reason: ReadyForNewScale
      status: 'True'
      type: AbleToScale
    - lastTransitionTime: '2021-02-09T01:00:33Z'
      message: >-
        the HPA was able to successfully calculate a replica count from external
        metric requests_per_second(nil)
      reason: ValidMetricFound
      status: 'True'
      type: ScalingActive
    - lastTransitionTime: '2021-02-09T11:58:49Z'
      message: the desired replica count is less than the minimum replica count
      reason: TooFewReplicas
      status: 'True'
      type: ScalingLimited
  currentMetrics:
    - object:
        current:
          averageValue: 389m
          value: '0'
        describedObject:
          kind: ''
          name: ''
        metric:
          name: requests_per_second
      type: Object
  currentReplicas: 1
  desiredReplicas: 1

おわりに

autoscaling/v2beta2 APIで提供されたカスタムメトリクスによるHPAについて、Istioのistio_requests_total を使ってHPAを動かす例に沿って解説しました。ほかにもdatadogと連携するなどでき、とても自由度が高く便利なシステムだと思うので、ぜひ使ってみてください。

参考文献

Horizontal Pod Autoscaler Walkthrough
Horizontal Pod Autoscaler
Horizontal Pod Autoscaler with Arbitrary Metrics
Prometheus Adapterを利用したPodのオートスケール