ข้ามไปยังเนื้อหา

Horizontal Pod Autoscaling

HorizontalPodAutoscaler จับตา metric เทียบกับ target แล้วปรับ replicas ของ Deployment ให้อยู่ระหว่างค่าต่ำสุดกับสูงสุด เพื่อดึง metric นั้นให้ใกล้ target — แค่นั้นเอง

HorizontalPodAutoscaler (autoscaling/v2) ฟังดูง่าย แต่จะไม่ทำอะไรเลยแบบเงียบ ๆ ถ้าสองอย่างนี้ยังไม่จริง

  1. Pod ของ workload ต้องตั้ง resource requests ไว้ โดย default HPA scale ตาม CPU หรือ memory utilization และ utilization คำนวณเป็นเปอร์เซ็นต์ของสิ่งที่ Pod request ไว้ — ไม่มี requests ก็ไม่มีตัวหารมาคำนวณเปอร์เซ็นต์
  2. metrics-server (หรือ custom/external metrics adapter) ต้องถูกติดตั้งใน cluster HPA เองไม่วัดอะไรเลย แค่อ่าน metric ที่อะไรบางอย่างเก็บและ expose ผ่าน metrics API ไม่มีแหล่ง metric ก็ไม่มีการตัดสินใจ scale เกิดขึ้นเลย
k8s/horizontal-pod-autoscaling/hpa-norequests.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 2
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: registry.k8s.io/pause:3.10
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60

แม้จะติดตั้ง metrics-server แล้วและทำงานปกติ Deployment นี้ไม่มี resources.requests บน container เลย และผลจริงตรงตามที่ข้อบังคับทำนายไว้เป๊ะ

$ kubectl get hpa web-app
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
web-app Deployment/web-app cpu: <unknown>/60% 2 10 2 30s
$ kubectl describe hpa web-app
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True SucceededGetScale the HPA controller was able to get the target's current scale
ScalingActive False FailedGetResourceMetric the HPA was unable to compute the replica count: failed to get cpu utilization: missing request for cpu in container web-app of Pod web-app-6d4d8d45d8-fl6rg
Events:
Warning FailedGetResourceMetric failed to get cpu utilization: missing request for cpu in container web-app of Pod web-app-6d4d8d45d8-fl6rg

cpu: <unknown>/60% และ reason FailedGetResourceMetric จริงพร้อมข้อความ missing-request เป๊ะ ๆ — ไม่ใช่ error ที่แต่งขึ้น นี่คือสิ่งที่ cluster 1.37 จริงคืนกลับมาทันทีที่ metrics-server พยายามคำนวณ utilization แล้วไม่มีอะไรมาหาร

metrics-server ไม่ได้มากับ kind โดย default และต้องแพตช์จริงหนึ่งจุดเพื่อทน self-signed certificate ของ kubelet บน kind

scripts/install-metrics-server.sh
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl -n kube-system patch deployment metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deploy/metrics-server
$ kubectl top nodes
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
scheduling-control-plane 169m 1% 847Mi 9%
scheduling-worker 43m 0% 163Mi 1%

ถ้าไม่มี --kubelet-insecure-tls metrics-server บน kind จะ TLS verification ล้มเหลวเทียบกับ serving certificate ที่ self-signed ของ kubelet เอง แล้วไม่รายงานอะไรเลย — เป็นกับดักจริงที่พบบ่อยตอนรันครั้งแรก ไม่ใช่แค่สมมติฐาน

เมื่อตั้ง requests ไว้แล้วและ metrics-server รันอยู่ object HPA เองก็แค่ระบุ target กับ metric

k8s/horizontal-pod-autoscaling/hpa-v2.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60

ค่านี้หมายความว่า คุม average CPU utilization ของ Pod web-app ทุกตัวให้อยู่ใกล้ 60% ของ CPU ที่ request ไว้ โดย scale Deployment ระหว่าง 3 ถึง 20 replica เพื่อรักษาระดับนั้น HPA ยังรองรับการ scale ตาม custom metric ที่ไม่ใช่ resource ด้วย เช่น queue depth หรือ requests-per-second ผ่าน API custom.metrics.k8s.io และ external.metrics.k8s.io สำหรับทีมที่ bottleneck ไม่ได้อยู่ที่ CPU หรือ memory เลย

autoscaling/v1 เทียบ v2: object เดียว สองรูปร่าง พิสูจน์แล้ว

หัวข้อที่มีชื่อว่า “autoscaling/v1 เทียบ v2: object เดียว สองรูปร่าง พิสูจน์แล้ว”

autoscaling/v1 มีมาก่อน custom/external metric และรองรับหลาย metric — มันรู้จักแค่ targetCPUUtilizationPercentage แบบเรียบ ๆ ตัวเดียว autoscaling/v2 แทนที่ด้วย array metrics: [] ที่เก็บ metric หลายประเภทพร้อมกันได้ ทั้งสองชื่ออ้างถึง object เดียวกัน — API server แปลงระหว่างสองรูปร่างให้เอง พิสูจน์ — apply เป็น v1 อ่านกลับเป็น v2

k8s/horizontal-pod-autoscaling/hpa-v1.yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 60
$ kubectl get hpa.v2.autoscaling web-app -o yaml
spec:
metrics:
- resource:
name: cpu
target:
averageUtilization: 60
type: Utilization
type: Resource
minReplicas: 2
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: web-app}

object ถูกสร้างด้วย targetCPUUtilizationPercentage: 60 ผ่าน autoscaling/v1 และการอ่าน object เดียวกันเป๊ะ ๆ กลับผ่าน endpoint v2 แสดงว่ามันถูกแปลงอัตโนมัติเป็น metrics[0].resource.target.averageUtilization: 60 — ไม่ต้อง re-apply ไม่มี object แยก แค่การแปลงเวอร์ชันในตัวของ API server ทำหน้าที่ของมัน

autoscaling/v2 เพิ่ม behavior.scaleUp/behavior.scaleDown แต่ละตัวมี stabilizationWindowSeconds และ policies ที่จำกัดอัตราหนึ่งตัวหรือมากกว่า

k8s/horizontal-pod-autoscaling/hpa-behavior.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: load-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: load-app
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
behavior:
scaleDown:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 1
periodSeconds: 30
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15

scaleDown.stabilizationWindowSeconds: 60 ทำให้ HPA มองย้อนกลับไป 60 วินาทีล่าสุดของคำแนะนำ แล้วเลือกจำนวน replica สูงสุด ที่เห็นในช่วงนั้นก่อนจะ scale ลง — นี่คือสิ่งที่หยุดไม่ให้การลดลงของ traffic ชั่วคราวครั้งเดียวไปลด replica ที่จะต้องใช้อีกครั้งในอีกนาทีถัดไปทันที scaleUp.stabilizationWindowSeconds: 0 พร้อม policy Percent/100/periodSeconds: 15 หมายความว่า “ตอบสนองทันที และอนุญาตให้ replica count เพิ่มเป็นสองเท่าได้ทุก 15 วินาที” — จงใจไม่สมมาตร: ระวังตอนลง ดุดันตอนขึ้น

Deployment ที่รัน stress --cpu 1 (busy-loop หนึ่งตัวต่อ Pod) เทียบกับ request cpu: "100m" พร้อม behavior ข้างบน

$ kubectl get hpa load-app
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
load-app Deployment/load-app cpu: 201%/50% 1 5 2 49s
...
load-app Deployment/load-app cpu: 200%/50% 1 5 4 84s
$ kubectl describe hpa load-app
Events:
Normal SuccessfulRescale New size: 2; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale New size: 5; reason: cpu resource utilization (percentage of request) above target

cpu: 200%/50% คือของจริง: Pod stress แต่ละตัวใช้เกิน request 100m ไปมาก metrics-server รายงานมัน และ HPA เพิ่ม replica count เป็นสองเท่าทุก window 15 วินาที (1 → 2 → 4 → ถูกจำกัดที่ maxReplicas: 5) ตรงตาม policy Percent/100 ของ scale-up อนุญาต — ทุก event SuccessfulRescale ข้างบนมาจาก controller reconcile จริงเทียบกับ metric จริง ไม่ใช่ trace จำลอง

minReplicas ตั้งเป็น 0 ได้ แต่ docs พูดชัดเจน ว่าการ scale ไปและกลับจากศูนย์ทำงานได้แค่กับ custom หรือ external metric เท่านั้น — ไม่ใช่ resource metric CPU/memory ธรรมดา เหตุผลตามมาจากสองข้อบังคับข้างบนตรง ๆ: พอ replica เหลือศูนย์ ไม่มี Pod เหลือให้รายงาน utilization ของ CPU/memory เลย ดังนั้น HPA แบบ resource-metric ไม่มี signal อะไรให้ scale กลับขึ้นเลย custom/external metric (queue depth, จำนวน message) ไม่ต้องพึ่ง Pod ที่รันอยู่เพื่อสร้างค่า จึงกระตุ้น replica แรกได้

ควรพูดให้ชัดตรงเส้นแบ่งนี้ เพราะมี autoscaler อีกสองตัวที่ฟังดูคล้ายกันแต่ทำหน้าที่คนละอย่างโดยสิ้นเชิง

  • HorizontalPodAutoscaler เปลี่ยนจำนวน replica ของ Deployment หรือ StatefulSet นี่คืองานทั้งหมดของตัวเอง
  • VerticalPodAutoscaler (VPA) เปลี่ยน requests/limits ของ Pod ที่มีอยู่แล้ว คือปรับขนาด Pod แทนที่จะเพิ่มจำนวน — พูดถึงเพื่อเปรียบเทียบเท่านั้น คอร์สนี้ไม่ได้ติดตั้งจริง ดูรายละเอียดเต็มได้ที่ Kubernetes VPA docs
  • Cluster Autoscaler เปลี่ยนจำนวน Node ของ cluster เพิ่มหรือลดเครื่องที่อยู่ข้างใต้ทุกอย่าง (บทถัดไปพูดถึงเรื่องนี้)
flowchart LR
  pods["Deployment Pods"] --> ms["metrics-server"]
  ms --> hpa["HorizontalPodAutoscaler controller"]
  hpa -->|"scaleUp: fast, per behavior policy"| replicas["Deployment.spec.replicas"]
  hpa -->|"scaleDown: stabilized over a window"| replicas
metrics-server ป้อน CPU utilization ของ Pod เข้า HPA controller ซึ่งปรับ replica count ของ Deployment ภายใต้ขั้นตอนที่ behavior จำกัดไว้

HPA คือ controller แบบสัดส่วนที่มี input เดียวเป๊ะ (ค่า metric ปัจจุบันเทียบกับ target ของมัน) และ output เดียวเป๊ะ (replica count) และทุกข้อบังคับในบทนี้มีไว้เพื่อทำให้ input นั้นมีความหมาย: requests ให้ตัวหารกับ utilization, metrics-server ให้ตัวตั้ง, behavior กำหนดว่า output จะเคลื่อนไหวได้ดุดันแค่ไหนเมื่อรู้ input แล้ว autoscaling/v1 กับ v2 ไม่ใช่ controller สองตัวที่ต่างกัน — มันคือมุมมองสองแบบของ object เดียวกัน พิสูจน์ข้างบนด้วยการแปลงของ API server เอง ดังนั้น “ควรใช้เวอร์ชันไหน” จริง ๆ แล้วคือ “อยากอ่านและเขียนรูปร่างไหน” ไม่ใช่ทางเลือกเชิงพฤติกรรม

ทางเลือกได้อะไรเสียอะไร
Resource metric CPU/memoryไม่ต้องมี infrastructure เพิ่มนอกจาก metrics-serverไม่มี signal เลยถ้าไม่มี requests (พิสูจน์ข้างบน); scale ไป/กลับจากศูนย์ไม่ได้
Custom/external metricscale ตามสิ่งที่สำคัญจริง ๆ ได้ (queue depth, RPS); รองรับ minReplicas: 0ต้องมี metrics adapter และ pipeline จริงป้อนให้ — ชิ้นส่วนเคลื่อนไหวมากขึ้น
scaleUp behavior ที่ดุดัน (พิสูจน์ข้างบน: เพิ่มเป็นสองเท่าทุก 15 วินาที)ตอบสนอง traffic spike จริงได้เกือบทันทีอาจ overshoot บน spike สั้น ๆ ที่ไม่ยั่งยืน over-provision ชั่วคราว
scaleDown.stabilizationWindowSeconds แบบระวังดูดซับการลดลงสั้น ๆ ได้โดยไม่ flap replica count ลงแล้วขึ้นปล่อย capacity คืนช้าลงหลัง load ลดลงจริง ๆ เสียเงินนานกว่าที่จำเป็น
autoscaling/v1 (targetCPUUtilizationPercentage)ง่าย field เดียวไม่รองรับหลาย metric ไม่มี behavior block — พิสูจน์ข้างบน มันคือ subset ที่แคบกว่าของสิ่งที่ v2 แสดงออกได้
  • “HPA วัด CPU/memory usage เอง” พิสูจน์ข้างบน: HPA อ่านจาก metrics API ถ้าไม่มี metrics-server (หรือ adapter) ติดตั้งไว้ มันไม่มีอะไรให้อ่านเลย รายงาน <unknown>
  • “autoscaling/v1 กับ v2 เป็น autoscaler แยกกัน ตั้งค่าอิสระต่อกัน” พิสูจน์ข้างบน: apply เป็น v1 แล้วอ่านกลับเป็น v2 แสดงว่า API server แปลง field ของ object เดียวกันให้อัตโนมัติ — มี object เดียว สองรูปร่าง API
  • “Deployment ที่ตั้ง limits.cpu แต่ไม่ตั้ง requests.cpu ทำงานกับ HPA ได้ปกติ” พิสูจน์ข้างบน (ดู FailedGetResourceMetric): เปอร์เซ็นต์ utilization ของ HPA อ้างอิงกับ requests เสมอ ไม่มี request จะได้ error จริง ไม่ใช่ fallback ไป limits
  • “minReplicas: 0 ใช้ได้กับ HPA ทุกแบบ” พิสูจน์ผ่าน docs ที่อ้างอิงข้างบน: รองรับแค่ custom/external metric — HPA แบบ resource metric ไม่มีทางตรวจ load ได้เลยเมื่อไม่มี Pod รันอยู่ให้วัด
  • “scale-up กับ scale-down ระมัดระวังเท่ากันโดย default” พิสูจน์ข้างบนด้วย behavior block จริง: ตัวอย่างของคอร์สนี้จงใจไม่สมมาตร (scale-up ทันทีและเพิ่มเป็นสองเท่า เทียบกับ scale-down ที่ stabilize 60 วินาที) และความไม่สมมาตรนี้เป็น pattern การใช้งานจริงที่จงใจทำ ไม่ใช่กรณีพิเศษ

ตอน review HPA ที่ AI สร้าง เช็คสิ่งเหล่านี้

  • HPA ที่ target Deployment ที่ container ไม่มี resources.requests.cpu/memory — พิสูจน์ข้างบน สิ่งนี้ทำให้เกิด FailedGetResourceMetric/<unknown> จริงตอน runtime ไม่ใช่ autoscaler ที่ converge ช้า
  • minReplicas: 0 บน HPA แบบ resource metric CPU/memory ธรรมดา — พิสูจน์ข้างบนผ่าน docs รองรับแค่ custom/external metric เท่านั้น และเป็นความไม่ตรงกันของ config แบบเงียบ ๆ ถ้าไม่ใช่ (ถูกยอมรับตอน apply แค่ไม่มีวัน scale กลับขึ้นจากศูนย์ได้)
  • manifest ที่ผสม field name ของ autoscaling/v1 (targetCPUUtilizationPercentage) กับ field ของ autoscaling/v2 (metrics:, behavior:) ใน object เดียวกัน — พิสูจน์ข้างบน ทั้งสองคือรูปร่างต่างกันของ API เดียว manifest จริงต้องเลือก apiVersion เดียวแล้วใช้แค่ field ของรูปร่างนั้น
  • ไม่มี behavior block เลยบน workload ที่ traffic spike ได้ — default คือ scale-down stabilization window 5 นาทีและ scale-up ไม่จำกัดอัตรา ซึ่งไม่ใช่ default ที่ถูกต้องเสมอไป พิสูจน์ข้างบน behavior block ที่ชัดเจนคือวิธีปรับจริง
  • สมมติว่า metrics-server มากับทุก cluster โดย default — พิสูจน์ข้างบนบน kind มันต้องติดตั้งแยกและแพตช์ --kubelet-insecure-tls แค่เพื่อให้รายงานอะไรได้เลย

🐛 หา bug ในโค้ดนี้

k8s/horizontal-pod-autoscaling/web-app-hpa.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 2
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: web-app:3.2.0
resources:
limits:
cpu: "500m"
memory: "512Mi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
เฉลย

Deployment ตั้ง limits.cpu แต่ไม่เคยตั้ง requests.cpu เลย HPA นี้ดูเหมือนตั้งค่าถูกต้องสมบูรณ์แบบ — target, min/max, metric type ล้วนโอเค — แต่มันจะไม่มีวัน scale ตาม CPU ได้เลย พิสูจน์ข้างบนด้วย error จริงเป๊ะ ๆ: failed to get cpu utilization: missing request for cpu in container web-app of Pod ... การคำนวณ utilization ของ HPA อ้างอิงกับ requests เสมอ (current usage / requests.cpu) และไม่ fallback ไป limits เมื่อไม่มี requests kubectl get hpa บน setup นี้เป๊ะ ๆ จะรายงาน cpu: <unknown>/60% ตลอดกาล และ kubectl describe hpa แสดง ScalingActive: False, Reason: FailedGetResourceMetric — ทางแก้คือเพิ่ม requests.cpu ที่ชัดเจน (มักเท่ากับหรือเป็นสัดส่วนของ limits.cpu) ไม่ใช่ปรับอะไรใน object HPA เอง

ต้องติดตั้งอะไรใน cluster ก่อน HPA จะตัดสินใจ scale ได้เลย
ทำไม resource requests ถึงเป็นข้อบังคับสำหรับ HPA ที่ scale ตาม CPU/memory
HPA นี้ถูก apply ด้วย apiVersion: autoscaling/v1 และ targetCPUUtilizationPercentage: 60 ตอนแรก output นี้พิสูจน์อะไร
$ kubectl get hpa.v2.autoscaling web-app -o yaml
spec:
  metrics:
  - resource:
      name: cpu
      target:
        averageUtilization: 60
        type: Utilization
behavior.scaleDown.stabilizationWindowSeconds คุมอะไร
Events แสดง SuccessfulRescale สองครั้ง: ไปที่ 2 แล้วไปที่ 4 behavior policy ไหนทำให้เกิด pattern เพิ่มเป็นสองเท่านี้
$ kubectl get hpa load-app
NAME       REFERENCE             TARGETS         REPLICAS
load-app   Deployment/load-app   cpu: 200%/50%   4
ใช้ minReplicas: 0 กับ HPA แบบ CPU utilization ธรรมดาได้ไหม
HorizontalPodAutoscaler เปลี่ยนอะไร เทียบกับ VerticalPodAutoscaler และ Cluster Autoscaler