KubernetesのHPA(Horizontal Pod Autoscaling)を利用する場合、希望する数のPodがデプロイされるまで時間がかかることがあります(クラスタ・オートスケールのスケールアウト時など)。 それまではサービス品質が低下する可能性があり、例えばトラフィック量が急激に増加した場合、応答レイテンシが非常に大きくなる可能性があります。 本記事では、過去のデータなどからリクエスト量の推 移を大まかに推測できることを前提に、予想されるトラフィック増加の前にKubernetesのCronJobを使ってHPAの最小レプリカ数を調整することで、サービス低下を緩和する方法を紹介します。 また、CronJobのテンプレートと、CSVファイルの値を使用してテンプレートからCronJobマニフェストを生成するPythonスクリプトを記述します。
背景
ほとんどのウェブアプリケーションは、ランチタイムは1秒あたりのリクエストが多く、オフィスアワーは控えめで、夜間は少ないというような、日々のトラフィックのトレンドを持っているかと思います。 KubernetesのHPA(Horizontal Pod Autoscaling)と呼ばれる機能では、設定したメトリックターゲットに基づいてPodの数を動的に変更することができますが、HPAの仕組み上、Pod数が最適になるまでタイムラグが発生するのは避けられません。 それまでは、例えば急にトラフィックが増えた場合、リクエストのレイテンシが非常に大きくなる可能性があります。 しかし、実際のシナリオでは、統計的な指標値を利用することで、事前にリクエスト量をおおよそ見積もることができるかもしれません。 この記事では、予想されるトラフィック増加の前に、KubernetesのCronJobを使用してHPAメトリクスを調整することで、サービス低下を緩和する方法を紹介しま す。
ステップ
1. デモアプリのデプロイ
デモ用にシンプルなToDoアプリを使ってみましょう。 このアプリは、NGINXでホストされたフロントエンドのReact、NodeJSで書かれたバックエンドのAPIサーバー、データベース用のMongoDBで構成されています。
リポジトリをクローンしましょう:
# リポジトリをクローンする
git clone https://github.com/ryojp/todo.git
# リポジトリに移動する
cd todo
その後、READMEに従ってアプリをデプロイします:
Create MongoDB-related secret:
cp .env.dev .env
vim .env # set secure passwords
kubectl create ns todo
kubectl -n todo create secret generic todosecret --from-env-file .envInstall NGINX Ingress Controller
helm upgrade --install ingress-nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx --create-namespace
Apply Kubernetes configuration files:
kubectl apply -f k8s/
Visit https://localhost and you will be redirected to the login page if success.
上記のステップでは、以下のものを名前空間 todo
にデプロイしました:
frontend
とapi
用の ClusterIP、Deployment、HPAmongo
用の ClusterIP と StatefulSet/api
リクエストをapi
に、/
リクエストをfrontend
に(リバース)プロキシするIngress
特に、2つのHPAをデプロイしました:
$ kubectl -n todo get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
frontend Deployment/frontend 0%/50% 1 10 1 2m13s
api Deployment/api 0%/50% 2 10 2 2m13s
2. 適切なロールを持つServiceAccountの作成
CronJobからHPAにkubectl patch
コマンドを実行します。
そこで、KubernetesのAPIサーバーにアクセスするための適切なロールを持つServiceAccountを作成しましょう:
apiVersion: v1
kind: ServiceAccount
metadata:
name: hpa-scheduler
namespace: todo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: hpa-scheduler
namespace: todo
rules:
- apiGroups:
- autoscaling
resources:
- horizontalpodautoscalers
verbs:
- get
- list
- patch
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: hpa-scheduler
namespace: todo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: hpa-scheduler
subjects:
- apiGroup: ""
kind: ServiceAccount
name: hpa-scheduler
namespace: todo
上記のマニフェストは自己説明的かと思われます。
なお、apiGroups
とresources
はkubectl api-resources
の出力から見つけること ができます:
$ kubectl api-resources | grep hpa
horizontalpodautoscalers hpa autoscaling/v2 true HorizontalPodAutoscaler
それではapply
しましょう:
$ kubectl apply -f k8s/hpa-scheduler/sa.yml
serviceaccount/hpa-scheduler created
role.rbac.authorization.k8s.io/hpa-scheduler created
rolebinding.rbac.authorization.k8s.io/hpa-scheduler created
3. 1日のスケジュールをCSVで定義する
ここでは、ランチタイム(12:00-13:00)に突然リクエスト量が増加すると仮定します。
遅延なく低レイテンシーを維持するために、12:00の5分前にHPAのminReplicas
フィールドを増やすことにします。
以下はCronJobの1日のスケジュールです。
NAMESPACE,HPA,HOUR,MINUTE,MIN_REPLICAS,MAX_REPLICAS
todo,frontend,11,55,3,10
todo,frontend,13,10,1,10
todo,api,05,55,3,10
todo,api,11,55,6,10
todo,api,13,10,3,10
todo,api,22,10,1,10
4. CronJob テンプレートの作成
CronJobマニフェストを生成するためにCSVの値を使用します。下記はそのテンプレートです。
apiVersion: batch/v1
kind: CronJob
metadata:
name: cj-__HPA__-__HOURMIN__
namespace: __NAMESPACE__
labels:
target: __HPA__
app.kubernetes.io/name: cj-__HPA__-__HOURMIN__
spec:
schedule: __CRON__ # Timezone UTC(+0000)
startingDeadlineSeconds: 120
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
metadata:
labels:
sidecar.istio.io/inject: "false"
spec:
serviceAccountName: hpa-scheduler
containers:
- name: hpa-scheduler
image: bitnami/kubectl:1.26
command:
- /bin/sh
- -c
- |
kubectl -n __NAMESPACE__ patch hpa/__HPA__ -p '{"spec":{"minReplicas":__MIN__, "maxReplicas":__MAX__}}'
restartPolicy: OnFailure
このテンプレートでは、__HPA__
のようなダブルアンダースコアで囲まれた変数は、次のステップで書くPythonスクリプトを使ってCSVファイルの値に置き換えられます。
Kubernetes v1.27以降、.spec.timeZone
(doc)でタイムゾーンを指定できるようになりました。
v1.27 はこの記事を書いている時点では Azure のようなクラウドプロバイダーで GA されていないため、このテンプレートでは使用していません。
代わりに、Pythonスクリプトでタイムゾーンを変換することにします。
5. CSV から CronJob マニフェストを生成する Python スクリプトの作成
CSVを読み込んでCronJobマニフェストを生成するスクリプトを以下に示します:
#!/usr/bin/env python3
from collections import namedtuple
import csv
import os
BASE_DIR = "k8s/hpa-scheduler/"
OUTPUT_DIR = os.path.join(BASE_DIR, "generated/")
CRONJOB_TEMPLATE_FILE = os.path.join(BASE_DIR, "cronjob-template.yml")
CRONJOB_CSV_FILE = os.path.join(BASE_DIR, "cronjob.csv")
TIMEZONE_DIFF = int(os.environ.get("TIMEZONE_DIFF", +9)) # UTC+0900
Config = namedtuple("Config", "NAMESPACE,HPA,HOUR,MINUTE,MIN_REPLICAS,MAX_REPLICAS")
def substitute_template_yml(
config: Config, template: str = CRONJOB_TEMPLATE_FILE
) -> str:
# read the template file content
with open(template, "r") as f:
content = f.read()
# convert "3" -> "03" etc.
minute, hour = config.MINUTE.rjust(2, "0"), config.HOUR.rjust(2, "0")
# convert timezone to UTC
utc_hour = (int(hour) - TIMEZONE_DIFF) % 24
utc_hour = str(utc_hour).rjust(2, "0")
return (
content.replace("__CRON__", f"{minute} {utc_hour} * * *")
.replace("__NAMESPACE__", config.NAMESPACE)
.replace("__HPA__", config.HPA)
.replace("__HOURMIN__", hour + minute)
.replace("__MIN__", config.MIN_REPLICAS)
.replace("__MAX__", config.MAX_REPLICAS)
)
def read_csv(csv_filename: str) -> list[Config]:
with open(csv_filename) as f:
reader = csv.reader(f)
# make sure the CSV header matches the `Config` type
assert Config._fields == tuple(next(reader)) # RHS: reading the CSV header
return list(map(Config._make, reader))
def write_yml(filename: str, content: str) -> None:
with open(filename, "w") as f:
f.write(content)
def generate_filename(config: Config) -> str:
return (
f'cj-{config.HPA}{config.HOUR.rjust(2, "0")}{config.MINUTE.rjust(2, "0")}.yml'
)
def generate_yml_from_csv(csv_filename: str, outdir: str) -> None:
os.makedirs(outdir, exist_ok=True)
for config in read_csv(csv_filename=csv_filename):
_filename = generate_filename(config)
_content = substitute_template_yml(config=config)
write_yml(os.path.join(outdir, _filename), _content)
if __name__ == "__main__":
generate_yml_from_csv(CRONJOB_CSV_FILE, OUTPUT_DIR)
実行権限を与えましょう:
chmod +x k8s/hpa-scheduler/generate.py
6. 動作確認
それでは動作を確認します。 まずPythonスクリプトを実行してCronJobマニフェストを生成しましょう:
TIMEZONE_DIFF=0 ./k8s/hpa-scheduler/generate.py
ここで、TIMEZONE_DIFF
を変更してください。
ほとんどのシステムではCronJobはUTCタイムゾーンを使用しますので、もしあなたのタイムゾーンがUTC+0900であれば、TIMEZONE_DIFF=9
を渡すことができます。
しかしMicroK8sでは、Zennの記事によるとローカルのタイムゾーンになります。
次に、生成されたCronJobマニフェストを適用します:
kubectl apply -f k8s/hpa-scheduler/generated/
それではCronJobsとHPAの状態を確認します:
# List CronJobs that target frontend or api
$ kubectl -n todo get cj -l "target in (frontend, api)"
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cj-api-0555 55 05 * * * False 0 <none> 13s
cj-api-1155 55 11 * * * False 0 <none> 13s
cj-api-1310 10 13 * * * False 0 <none> 13s
cj-api-2226 26 13 * * * False 0 <none> 13s
cj-frontend-1155 55 11 * * * False 0 <none> 13s
cj-frontend-1310 10 13 * * * False 0 <none> 13s
cj-api-2235 35 22 * * * False 1 4s 13s
# Get the log of the job that ran 4 seconds ago
$ kubectl -n todo logs cj-api-2235-28126895-572c5
horizontalpodautoscaler.autoscaling/api patched
# Verify that the MINPODS for the hpa/api is reduced from 2 to 1
$ kubectl -n todo get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
frontend Deployment/frontend 0%/50% 1 10 1 120m
api Deployment/api 0%/50% 1 10 1 120m
以上から、22:35にスケジュールされた hpa/api
の CronJob cj-api-2235
が正常に完了し、hpa/api
の MINPODS
が 2
から 1
に変更されたことが確認できました。
7. クリーンアップ
kubectl delete ns todo ingress-nginx
最後に
この記事では、サービス品質(応答レイテンシなど)を維持しながら、HPAを活用してPodの数を動的に微調整できる、シンプルかつ堅牢なソリューションを紹介しました。 また個人的には、ServiceAccount、Role、CronJobを学ぶ機会になりました。