手っ取り早くKubernetesのHPAとNodeを時間帯でスケールしましょ

この記事はOpenSaaS Studio Advent Calendar 2019の8日目の記事。

オートスケーリングの現実

KubernetesにはHorizonal Pod Autoscaler(HPA)という便利な仕組みがあって、CPUやメモリ使用率やカスタムメトリクスによってPodの数を増減できる。とはいえPodだけ増えても増えた分のPodを配置できるだけのコンピューティングリソースが無いと意味がない。GKEであればPodを配置できるだけのリソースがなければ、Nodepoolのサイズを拡大してくれる(Quotaの確認も忘れるなよ!)。

しかし、弊社のプロダクトはメディアやゲーム、広告だったり色々あるわけで、そのほとんどがB・C問わず大規模なユーザーを抱えている。オートスケーリングは欠かせない仕組みだが、コンピューティングリソースが上昇してから発火するものなので、基本的に瞬間的なスパイクには耐えられない。スパイクの3分後とかに潤沢なリソースが整っても時既に遅し。

オートスケールのスケジュール

幸いにも、メディアだったらプッシュ通知やCM起因だったり、AbemaTVのこの時間の番組がやばい!とか、ゲームはイベントの開始等で需要を予測できる。全てを見越して最初から超潤沢な構成にするという手法ももちろんあるが、皆が寝静まってる時間帯にそこまでのリソースを使うのはコスト的に忍びない。

というわけで、当方が「手っ取り早く」やってるオートスケールのスケジュール方法を軽く紹介する。雑にyamlだけ貼るが、雰囲気はつかめると思う。

まずはHPAを操作するためのServiceAccountやClusterRoleを作る(NamespacedなRoleでも良い)。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: hpa-scheduler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: hpa-scheduler
rules:
- apiGroups:
  - autoscaling
  resources:
  - horizontalpodautoscalers
  verbs:
  - get
  - list
  - patch
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  - deployments/scale
  verbs:
  - get
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: hpa-scheduler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: hpa-scheduler
subjects:
- apiGroup: ""
  kind: ServiceAccount
  name: hpa-scheduler
  namespace: kube-system

次にCronJobを作り、適当な時間に発火させるようにする。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hpa-scaleout
  labels:
    app.kubernetes.io/name: hpa-scaleout
spec:
  schedule: "0 9 * * *" # 18:00+0900
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/name: hpa-scaleout
        spec:
          serviceAccountName: hpa-scheduler
          containers:
          - name: hpa-scheduler 
            image: lachlanevenson/k8s-kubectl:v1.14.7
            command:
              - "/bin/sh"
              - "-c"
              - |
                                kubectl -n istio-system patch hpa/istio-ingressgateway -p '{"spec":{"minReplicas":50}}'
          restartPolicy: Never

作成したServiceAccountを食わせ、kubectlを実行できるコンテナを用意。その中で、istio-ingressgatewayのminReplicasを変更してスケールアウトする(必要に応じてmaxReplicasを上げてもよい)。スケールインも同じようなCronJobを用意すれば良い。

HPAでdesiredなPodが増え、Nodeが足りない場合はPodがPendingになるので、それをフックにNodePoolのサイズがあがる。規模が大きくなるとスケール完了も遅くなるので、NodeもHPA発動前にある程度の量を確保できていた方がよい。

というわけで、GKEのNodePoolも次のようにCronJobでどうにかできる。HPAの5分前とかで十分。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: nodepool-scaleup
  labels:
    app.kubernetes.io/name: nodepool-scaleup
spec:
  schedule: "55 8 * * *" # 17:55+0900
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/name: nodepool-scaleup
        spec:
          containers:
          - name: nodepool-scaleup
            image: google/cloud-sdk:272.0.0-alpine
            command:
              - "/bin/sh"
              - "-c"
              - |
                gcloud auth activate-service-account --key-file=/secrets/gcp/credentials.json
                gcloud config set container/cluster $(CLUSTER_NAME)
                gcloud container node-pools update $(TARGET_NODE_POOL) --region $(TARGET_REGION) --min-nodes 50
                yes | gcloud container clusters resize $(CLUSTER_NAME) --node-pool $(TARGET_NODE_POOL) --region $(TARGET_REGION) --num-nodes 50 --async                
            env:
              - name: CLUSTER_NAME
                value: your-cluster-name
              - name: TARGET_NODE_POOL 
                value: your-nodepool-name
              - name: TARGET_REGION
                value: asia-northeast1 
            volumeMounts:
              - name: gcp-credentials
                mountPath: /secrets/gcp
                readOnly: true
          restartPolicy: Never
          volumes:
            - name: gcp-credentials
              secret:
                secretName: nodepool-scaleup-gcp-credentials

クラスタの中で、クラスタよりスコープが外にあるクラウドの設定をいじることの気持ち悪さが残るが、これでシュッと時間帯でのスケールができる。プレウォーミングみたいなものだ。

CRD作成中

この手の手法使ったの個人的には2プロダクト目だし、もうCRD化した方が良いので現在片手間でCRDを作っている(OSS化するのでもうちと待ってな)。時間帯もそうだが、もう少しインテリジェントなスケールの機構がどうしても必要。