メインコンテンツにスキップ
ブログコンテナ (Kubernetes、Docker)Kubernetesクラスタのためのプロアクティブ・スケーリング

Kubernetes クラスターのプロアクティブなスケーリング

Proactive-Scaling-for-Kubernetes-Clusters (プロアクティブ・スケーリング・フォー・クバーネット・クラスター)

この投稿は「Scaling Kubernetes Series」の一部です。 登録をクリックしてライブ視聴または録画にアクセスし、このシリーズの他の投稿をご覧ください。

クラスタのリソースが少なくなると、Cluster Autoscalerは新しいノードをプロビジョニングし、クラスタに追加します。すでにKubernetesのユーザーであれば、ノードの作成とクラスタへの追加に数分かかることにお気づきかもしれません。

この間、アプリはこれ以上スケールアップできないため、簡単に接続数をオーバーしてしまいます。

RPS(リクエスト/秒)に基づく予想スケーリングと、クラスターオートスケーラーに依存した場合に発生する実際のスケーリングプラトーとを示すスクリーンショット。
仮想マシンのプロビジョニングに数分かかることがあります。この間、アプリのスケーリングができなくなる可能性があります。

待ち時間の長さはどうしたら解決できるのか?

プロアクティブ・スケーリング、か。 

  • クラスタオートスケーラの仕組みを理解し、その有用性を最大化する。
  • Kubernetesスケジューラを使用して、ノードにPodを割り当てること、および
  • ワーカーノードのプロビジョニングをプロアクティブに行い、スケーリング不良を回避します。

もし、このチュートリアルのコードを読みたい場合は、LearnK8s GitHubで見つけることができます。

KubernetesにおけるCluster Autoscalerのしくみ

Cluster Autoscalerは、オートスケールをトリガーするときに、メモリやCPUの可用性を調べません。その代わり、Cluster Autoscalerはイベントに反応し、スケジューリング不能なPodがないかチェックします。スケジューラがポッドを収容できるノードを見つけられない場合、ポッドはスケジューラ不能になります。

クラスターを作成してテストしてみましょう。

bash
$ linode-cli lke cluster-create \
 --label learnk8s \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 1 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

以下の内容に注意が必要です。

  • 各ノードは4GBのメモリと2つのvCPU(つまり`g6-standard-2`)を搭載しています。
  • クラスタ内のノードが1つである。
  • クラスタオートスケーラは、1ノードから10ノードに成長するように構成されています。

でインストールが成功したことを確認できます。

bash
$ kubectl get pods -A --kubeconfig=kubeconfig

環境変数でkubeconfigファイルを書き出すと、通常はより便利です。

で行うことができます。

bash
$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

エクセレント!

アプリケーションのデプロイ
1GBのメモリと250m*のCPUを必要とするアプリケーションを導入してみましょう。
Note: m = thousandth of a core, so 250m = 25% of the CPU

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 1
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 1G
             cpu: 250m

でリソースをクラスタに投入することができます。

bash
$ kubectl apply -f podinfo.yaml

そうするとすぐに、いくつかのことに気がつくかもしれません。まず、3つのポッドがほぼ即座に実行され、1つは保留中です。

1つのノードでアクティブになっている3つのポッドと、そのノードの外側にある保留中のポッドを示す図。

そして、その後に。

  • 数分後、オートスケーラは追加のノードを作成します。
  • を実行すると、4つ目のPodが新しいノードにデプロイされます。
1つのノードに3つのPodがあり、4つ目のPodが新しいノードにデプロイされている様子を示した図。
最終的に、4つ目のPodは新しいノードにデプロイされる。

なぜ4番目のPodは最初のノードにデプロイされないのでしょうか? 割り当て可能なリソースを掘り下げてみましょう。

Kubernetes ノードで割り当て可能なリソース

KubernetesクラスタにデプロイされたPodは、メモリ、CPU、ストレージのリソースを消費します。

しかし、同じノード上では、OSとkubeletはメモリとCPUを必要とします。

Kubernetesのワーカーノードでは、メモリとCPUが分割されています。

  1. OSやSSH、systemdなどのシステムデーモンを実行するために必要なリソース。
  2. Kubelet、コンテナランタイム、ノード問題検出器など、Kubernetesエージェントを実行するために必要なリソース。
  3. Podsが利用できるリソース
  4. 退去閾値のために予約されたリソース。
Kubernetesノードに割り当てられ予約されたリソースで、1.Eviction threshold、2.Podに残されたメモリとCPU、3.kubeletに予約されたメモリとCPU、4.OSに予約されたメモリとCPUから構成される。
Kubernetesノードに割り当てられ、予約されたリソース。

クラスタでkube-proxyなどのDaemonSetが動作している場合は、さらに使用可能なメモリとCPUを削減する必要があります。

そこで、すべてのポッドが1つのノードに収まるように、要件を下げてみましょう。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 0.8G # <- lower memory
             cpu: 200m    # <- lower CPU

で配置を修正することができます。

bash
$ kubectl apply -f podinfo.yaml

インスタンスを最適化するために、CPUとメモリの適切な量を選択することは、厄介なことかもしれません。Learnk8sのツールカリキュレーターを使えば、より素早くこれを行うことができるかもしれません。

1つの問題は解決しましたが、新しいノードを作成するのにかかる時間はどうでしょうか?

遅かれ早かれ、あなたは4つ以上のレプリカを持つことになるでしょう。新しいポッドが作成されるまで、本当に数分も待たなければならないのでしょうか?

簡単に言うと、「はい」です。

Linodeはゼロから仮想マシンを作成し、それをプロビジョニングし、クラスタに接続する必要があります。このプロセスには簡単に2分以上かかることがあります。

しかし、もう一つの方法があります。

必要なときに、すでにプロビジョニングされたノードをプロアクティブに作成することができるのです。

例えば、常に1つの予備ノードを持つようにオートスケーラーを構成することができます。予備ノードにPodがデプロイされると、オートスケーラはプロアクティブにさらに作成することができます。残念ながら、オートスケーラーにはこの機能が内蔵されていませんが、簡単に再現することができます。

ノードのリソースと同等のリクエストを持つポッドを作成することができます。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

でリソースをクラスタに投入することができます。

bash
kubectl apply -f placeholder.yaml

このポッドは全く何もしません。

ノード上のすべてのリソースを確保するためにプレースホルダーポッドを使用する方法を示す図。
ノード上のすべてのリソースを確保するために、プレースホルダーポッドを使用します。

ただ、ノードをフル稼働させるだけです。

次のステップは、スケーリングが必要なワークロードが発生したら、すぐにプレースホルダーポッドを退避させるようにすることです。

そのためには、Priority Classを使用することができます。

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning # <--
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

でクラスタに再提出してください。

bash
kubectl apply -f placeholder.yaml

これで設定は完了です。

自動スケーラがノードを作成するのに少し待つ必要があるかもしれませんが、この時点で2つのノードがあるはずです。

  1. 4つのポッドを持つノード。
  2. 置き型ポッドでもう一枚。

デプロイメントを5つのレプリカにスケールするとどうなりますか?オートスケーラが新しいノードを作成するのを待つ必要がありますか?

でテストしてみましょう。

bash
kubectl scale deployment/podinfo --replicas=5

観察する必要があります。

  1. 5つ目のポッドはすぐに作成され、10秒以内にRunningの状態になります。
  2. ポッドのスペースを確保するために、プレースホルダーのポッドを立ち退かせた。
プレースホルダーポッドを退去させて、通常のポッドのためのスペースを確保する様子を示した図。
プレースホルダーポッドは、通常のポッドのためのスペースを確保するために退去させられる。

そして、その後に。

  1. クラスタオートスケーラは、保留中のプレースホルダポッドに気付き、新しいノードをプロビジョニングしました。
  2. 新しく作成されたノードにプレースホルダーPodがデプロイされます。
保留中のポッドが、新しいノードを作成するクラスタ・オートスケーラをどのようにトリガーするかを示す図。
保留中のポッドは、新しいノードを作成するクラスタオートスケーラをトリガします。

もっと多くのノードを持つことができるのに、なぜ積極的に1つのノードを作るのか?

プレースホルダーPodは、複数のレプリカにスケールさせることができます。各レプリカは、標準的なワークロードを受け入れる準備ができているKubernetesノードを事前にプロビジョニングします。しかし、これらのノードは、クラウド請求書に対してカウントされますが、アイドル状態で何もしません。そのため、注意深く、あまり多く作成しないようにする必要があります。

クラスターオートスケーラーとホリゾンタルポッドオートスケーラーの組合せ

このテクニックの意味を理解するために、クラスターオートスケーラーとHorizontal Pod Autoscaler(HPA)を組み合わせてみましょう。HPAは、デプロイメントにおけるレプリカを増やすためのものです。

アプリケーションのトラフィックが増えると、より多くのリクエストを処理するために、オートスケーラにレプリカの数を調整させることができます。

Podが利用可能なリソースをすべて使い切ると、クラスタオートスケーラが新しいノードの作成を開始し、HPAがさらにレプリカの作成を続けられるようにします。

新しいクラスターを作成してテストしてみましょう。

bash
$ linode-cli lke cluster-create \
 --label learnk8s-hpa \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 3 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa

でインストールが成功したことを確認できます。

bash
$ kubectl get pods -A --kubeconfig=kubeconfig-hpa

kubeconfigファイルを環境変数で書き出すとより便利です。

で行うことができます。

bash
$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods

エクセレント!

Helm を使ってPrometheus をインストールし、デプロイメントからメトリクスをスクレイピングしてみましょう。
Helmのインストール方法は、公式サイトに記載されています。

bash
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus

Kubernetesは、レプリカを動的に増減させるためのコントローラをHPAに提供しています。

残念ながら、HPAにはいくつかの欠点があります。

  1. 箱から出しても動きません。Metrics Serverをインストールして、メトリクスを集計し、公開する必要があります。
  2. PromQLのクエリーは、そのままでは使えません。

幸いなことに、HPAコントローラーを拡張し、いくつかの追加機能(Prometheus からのメトリクスの読み取りなど)を持つKEDAを使用することができます。

KEDAは3つのコンポーネントで構成されるオートスケーラーです。

  • スケーラー
  • メトリックスアダプター
  • Aコントローラー
KEDAアーキテクチャのイメージ図
KEDA建築。

HelmでKEDAをインストールすることができます。

bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

Prometheus と KEDA がインストールされたので、デプロイメントを作成しましょう。

この実験では、1秒間に一定数のリクエストを処理するように設計されたアプリを使用します。 

各ポッドは1秒間に最大10個のリクエストを処理することができる。もしポッドが11番目のリクエストを受け取ると、そのリクエストを保留にしておいて後で処理することになります。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
     annotations:
       prometheus.io/scrape: "true"
   spec:
     containers:
       - name: podinfo
         image: learnk8s/rate-limiter:1.0.0
         imagePullPolicy: Always
         args: ["/app/index.js", "10"]
         ports:
           - containerPort: 8080
         resources:
           requests:
             memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 8080
 selector:
   app: podinfo

でリソースをクラスタに投入することができます。

bash
$ kubectl apply -f rate-limiter.yaml

トラフィックを発生させるために、Locustを使用することになります。

以下のYAML定義では、分散型ロードテストクラスターを作成しています。

yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: locust-script
data:
 locustfile.py: |-
   from locust import HttpUser, task, between
 
   class QuickstartUser(HttpUser):
       @task
       def hello_world(self):
           self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-primary
 template:
   metadata:
     labels:
       app: locust-primary
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--master"]
         ports:
           - containerPort: 5557
             name: comm
           - containerPort: 5558
             name: comm-plus-1
           - containerPort: 8089
             name: web-ui
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script
---
apiVersion: v1
kind: Service
metadata:
 name: locust
spec:
 ports:
   - port: 5557
     name: communication
   - port: 5558
     name: communication-plus-1
   - port: 80
     targetPort: 8089
     name: web-ui
 selector:
   app: locust-primary
 type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-worker
 template:
   metadata:
     labels:
       app: locust-worker
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--worker", "--master-host=locust"]
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script

でクラスタに提出することができます。

bash
$ kubectl locust.yaml

イナゴは次のように読みます。 locustfile.pyであり、ConfigMap に格納される。

py
from locust import HttpUser, task, between
 
class QuickstartUser(HttpUser):
 
   @task
   def hello_world(self):
       self.client.get("/")

このファイルは、URLへのリクエストを行う以外、特別なことは何もしません。Locustのダッシュボードに接続するには、そのロードバランサのIPアドレスが必要です。

以下のコマンドで取得することができます。

bash
$ kubectl get service locust -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

ブラウザを開いて、そのIPアドレスを入力してください。

エクセレント!

1つだけ足りないのは、Horizontal Pod Autoscalerです。
KEDA autoscalerはHorizontal AutoscalerをScaledObjectという特定のオブジェクトでラッピングしています。

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: podinfo
spec:
scaleTargetRef:
  kind: Deployment
  name: podinfo
minReplicaCount: 1
maxReplicaCount: 30
cooldownPeriod: 30
pollingInterval: 1
triggers:
- type: prometheus
  metadata:
    serverAddress: http://prometheus-server
    metricName: connections_active_keda
    query: |
      sum(increase(http_requests_total{app="podinfo"}[60s]))
    threshold: "480" # 8rps * 60s

KEDAは、Prometheus で収集したメトリクスをブリッジして、Kubernetesに供給します。

最後に、これらのメトリクスを使用してHPA(Horizontal Pod Autoscaler)を作成します。

でHPAを手動で検査することができます。

bash
$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo

でオブジェクトを提出することができます。

bash
$ kubectl apply -f scaled-object.yaml

スケーリングがうまくいくかどうか、テストするときです。

Locustのダッシュボードで、以下の設定で実験を開始します。

  • ユーザー数 300
  • 産卵率。 0.4
  • ホスト http://podinfo
オートスケーラを使用して、保留中のポッドでスケーリングすることを示す画面録音のGIF。
クラスターと水平ポッドオートスケーラーを組み合わせる。

レプリカの数が増えている!

素晴らしい!しかし、お気づきですか?

デプロイが8ポッドにスケールした後、新しいノードにさらにポッドが作成されるまで数分待つ必要があります。

この期間では、現在の8つのレプリカがそれぞれ10リクエストしか処理できないため、1秒あたりのリクエスト数は停滞する。

スケールダウンして実験を繰り返してみよう。

bash
kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods

今回は、プレースホルダーPodでノードをオーバープロビジョニングしてみましょう。

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.9G

でクラスタに提出することができます。

bash
kubectl apply -f placeholder.yaml

Locustのダッシュボードを開き、以下の設定で実験を繰り返してください。

ギフ
クラスタと水平ポッドオートスケーラをオーバープロビジョニングで組み合わせる。

今度はバックグラウンドで新しいノードが作成され、1秒あたりのリクエスト数は平坦化することなく増加します。素晴らしい出来栄えです。

この記事で学んだことを振り返ってみましょう。

  • cluster autoscalerは、CPUやメモリの消費量を追跡しません。その代わり、保留中のポッドを監視します。
  • を使用すると、Kubernetesノードをプロビジョニングするために使用可能なメモリとCPUの合計を使用するPodをプロアクティブに作成することができます。
  • Kubernetesノードは、kubelet、オペレーティングシステム、およびeviction threshold用のリソースを予約しています。
  • では、Prometheus と KEDA を組み合わせて、PromQL クエリでポッドをスケールさせることができます。

Kubernetesのスケーリングウェビナーシリーズをフォローしたいですか?KEDAを使用してKubernetesクラスターをゼロからスケールする方法について、まずはご登録ください。


コメント 

コメントを残す

あなたのメールアドレスは公開されません。必須項目には*印がついています。