Kubernetes requests และ limits คือตัวเลขสองตัวที่ตัดสินว่า pod จะไปรันที่ไหน และจะเกิดอะไรขึ้นเมื่อมันใช้ทรัพยากรเกิน scheduler วาง pod โดยดูจาก requests คือ CPU และ memory ที่ container จองไว้ ส่วน kernel บังคับใช้ limits คือเพดานที่ container ใช้เกินไม่ได้ ถ้า CPU เกิน limit container จะโดน throttle ถ้า memory เกิน limit จะโดนฆ่าทิ้ง และจากสองค่านี้ Kubernetes ยังคำนวณ QoS class ให้ ซึ่งมีผลว่า pod ไหนจะโดนก่อนเมื่อ node เริ่มขาด memory
บทความนี้อธิบายว่า scheduler กรองและให้คะแนน node ยังไง, requests กับ limits ทำงานจริงยังไง, QoS class กับการ evict และวิธีกระจาย pod ด้วย anti-affinity, topology spread constraints และ taint เพื่อไม่ให้ node ตัวเดียวพังแล้วแอปล่มทั้งหมด
Kubernetes scheduler เลือก node ยังไง
เมื่อมี pod ใหม่ที่ยังไม่ได้ระบุ node kube-scheduler จะเลือก node ให้ในสองขั้น
1. Filtering (กรอง) ตัด node ที่รัน pod นี้ไม่ได้ออก:
- CPU หรือ memory ที่ยังไม่ถูกจองเหลือไม่พอกับ requests ของ pod
- ติด taint ที่ pod ไม่ tolerate
- ไม่ตรงกับ
nodeSelectorหรือ node affinity แบบบังคับ - จะผิดกฎ pod anti-affinity หรือ topology spread แบบบังคับ
- attach volume ใน zone นั้นไม่ได้ หรือ PVC ยังไม่ bind
2. Scoring (ให้คะแนน) จัดอันดับ node ที่ผ่านมา plugin ให้คะแนนจะชอบ node ที่ว่างเยอะกว่า, ใช้ CPU กับ memory สมดุลกว่า, มี image อยู่ใน cache แล้ว หรือช่วยให้ pod กระจายข้าม zone ได้ดีกว่า node ที่คะแนนสูงสุดได้ไป
ถ้าไม่มี node ไหนผ่าน filtering เลย pod จะค้างที่ Pending แต่ถ้าการเอา pod ที่ priority ต่ำกว่าออกจะทำให้มีที่ว่าง scheduler อาจ preempt pod นั้น (ดูเรื่อง PriorityClass ด้านล่าง) scheduler มีหน้าที่แค่ตัดสินใจ มันเขียน node ที่เลือกลงใน pod ผ่าน API server แล้ว kubelet บน node นั้นจะเป็นคนสั่งรัน container ภาพรวมว่า component เหล่านี้ประกอบกันยังไงอ่านได้ใน สถาปัตยกรรม Kubernetes: control plane และ node
Kubernetes requests กับ limits
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/api:1.8.2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 256Mirequestsคือสิ่งที่ container จองไว้ scheduler จะรวม requests ของทุก pod บน node แล้วเทียบกับ allocatable ของ node โดย ไม่ได้ดูการใช้งานจริงเลยlimitsคือเพดานสูงสุด kubelet และ container runtime ตั้ง cgroup ของ Linux ให้บังคับใช้
เรื่องหน่วย: 1 CPU คือหนึ่ง vCPU หรือหนึ่ง core ส่วน 250m คือหนึ่งในสี่ memory ใช้ Mi และ Gi (ฐาน 2) ส่วน M และ G เป็นฐาน 10 การใช้ปนกันเป็นที่มาของความคลาดเคลื่อนเล็ก ๆ ที่เจอบ่อย
เพราะ scheduler ตัดสินจาก requests ถ้าตั้งสูงเกินไป node จะดูเต็มทั้งที่แทบว่าง ถ้าตั้งต่ำเกินไป pod จะถูกอัดลง node เดียวมากเกิน แล้วแย่งทรัพยากรจริงกัน ให้วัดการใช้งานจริงจาก metric หรือจาก VPA โหมดแนะนำค่า แล้วตั้ง requests ใกล้กับการใช้งานปกติ ส่วน autoscaler พึ่งตัวเลขชุดเดียวกันนี้ยังไง อ่านได้ใน Kubernetes autoscaling ด้วย HPA, VPA และ Cluster Autoscaler
CPU throttling กับ OOMKill
ทรัพยากรสองแบบนี้มีพฤติกรรมต่างกันเมื่อ container ชน limit:
| CPU เกิน limit | Memory เกิน limit | |
|---|---|---|
| เกิดอะไรขึ้น | โดน throttle: container ต้องรอรอบเวลาถัดไป | โดน OOMKill: kernel ฆ่า process ทิ้ง |
| ทำไม | CPU บีบได้ งานแค่ช้าลง | memory บีบไม่ได้ จะเอาคืนได้ก็ต้องฆ่า process |
| อาการ | latency สูงขึ้น, timeout, start ช้า | container restart, Reason: OOMKilled, exit code 137 |
| ดูจากไหน | container_cpu_cfs_throttled_periods_total ใน Prometheus | kubectl describe pod ดู last state OOMKilled |
CPU throttling เป็นเรื่องที่มองข้ามได้ง่าย limit ถูกบังคับเป็นรอบละ 100ms service ที่มีหลาย thread และมี limit 500m อาจใช้ quota หมดตั้งแต่ 25ms แรกของรอบ แล้วต้องหยุดรออีก 75ms ทั้งที่ CPU เฉลี่ยดูต่ำ ถ้า p99 latency แย่แต่กราฟ CPU ดูปกติ ให้เช็ก metric ของ throttling
ถ้าโดน OOMKill ซ้ำ ๆ จะเห็นเป็นการ restart วนไปเรื่อย ๆ วิธีแยกว่าเป็น OOMKill หรือ probe ล้มเหลว อ่านได้ใน debug CrashLoopBackOff และ probe
ควรตั้ง CPU limit ไหม
เรื่องนี้เป็น trade-off จริง ๆ ไม่ใช่กฎตายตัว:
- Memory: ตั้ง limit เสมอ และส่วนใหญ่ให้เท่ากับ request การ overcommit memory นำไปสู่การโดนฆ่าแบบคาดเดาไม่ได้
- CPU: หลายทีมตั้ง CPU request แต่ ไม่ตั้ง CPU limit สำหรับ service ที่ไวต่อ latency เพราะ request ยังรับประกันส่วนแบ่งของแต่ละ pod เวลาแย่ง CPU กัน และ CPU ที่ว่างอยู่บน node ก็รองรับช่วงโหลดพุ่งได้แทนที่จะโดน throttle
- ตั้ง CPU limit เมื่อต้องการแยกกันเด็ดขาด เช่นคลัสเตอร์ multi-tenant, batch job ที่ถ้าไม่จำกัดจะกิน node ทั้งตัว หรือเมื่อต้องการ QoS แบบ Guaranteed
ถ้าตั้งแค่ limits ไม่ตั้ง requests Kubernetes จะ copy ค่า limits ไปเป็น requests ให้ ปลอดภัยดี แต่อาจจองมากกว่าที่ตั้งใจ
QoS class และการ evict
Kubernetes กำหนด QoS class ให้ทุก pod ตาม resource ที่ตั้งไว้:
| QoS class | เงื่อนไข | ใช้กับ |
|---|---|---|
Guaranteed | ทุก container ตั้ง CPU และ memory ทั้ง requests และ limits และ requests เท่ากับ limits | database, stateful service ที่สำคัญ |
Burstable | มีอย่างน้อยหนึ่ง container ตั้ง request หรือ limit ของ CPU หรือ memory แต่ไม่เข้าเงื่อนไข Guaranteed | web service ส่วนใหญ่ |
BestEffort | ไม่มี container ไหนตั้ง requests หรือ limits เลย | job ทิ้งได้ ไม่มีอะไรสำคัญ |
ดู class ของ pod:
kubectl get pod api-7d9f8c6b5-x2k4q -o jsonpath='{.status.qosClass}'เมื่อ node เริ่มขาด memory kubelet จะ evict pod เพื่อปกป้อง node โดยจัดลำดับจาก:
- การใช้งานของ pod เกิน requests หรือไม่
- priority ของ pod
- ใช้เกิน requests ไปมากแค่ไหน
ในทางปฏิบัติ BestEffort โดนก่อน เพราะใช้เท่าไรก็เกิน request ที่เป็นศูนย์ ตามด้วย Burstable ที่ใช้เกิน requests ส่วน Guaranteed และ Burstable ที่ใช้ไม่เกิน requests จะโดนท้ายสุด ถ้า memory หมดก่อนที่ kubelet จะทันตอบสนอง OOM killer ของ kernel จะเข้ามาทำงาน และมันก็ใช้ QoS ด้วย process ของ BestEffort มีโอกาสโดนสูงสุด ส่วน Guaranteed ต่ำสุด
workload ที่ห้ามโดน evict ให้ตั้งเป็น Guaranteed และ ให้ PriorityClass ที่สูงกว่า priority ยังทำให้ scheduler preempt pod ที่ priority ต่ำกว่าได้เมื่อคลัสเตอร์เต็ม
เพื่อกันไม่ให้มี pod แบบ BestEffort โผล่มาโดยไม่ตั้งใจ ให้ใส่ LimitRange ในทุก namespace มันจะเติม requests และ limits default ให้ container ที่ไม่ได้ระบุ ส่วน ResourceQuota ใช้จำกัดยอดรวมที่ namespace หนึ่ง request ได้
กระจาย pod เพื่อ reliability
ถ้า replica ทั้งสามตัวไปลง node เดียวกันแล้ว node นั้นพัง แอปก็ล่มทั้งที่ "มีสาม replica" Kubernetes มีเครื่องมือสามอย่างกันเรื่องนี้
Pod anti-affinity
ไม่ให้ replica ของแอปเดียวกันอยู่ node เดียวกัน:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: apiแบบ required... เป็นกฎบังคับ ถ้าทำไม่ได้ pod จะค้าง Pending ส่วน preferred... คือพยายามให้ได้ ใช้ required อย่างระวัง ถ้ามี 5 replica แต่มี 4 node ตัวที่ห้าจะไม่มีวัน schedule ได้
nodeAffinity ทำงานกลับทิศ คือดึง pod ไปหา node ที่มี label ที่กำหนด เช่น node ที่มี SSD
Topology spread constraints
anti-affinity บอกแค่ว่า "อย่าอยู่ด้วยกัน" ส่วน topology spread constraints บอกว่า "ให้จำนวนสมดุลกัน" ข้าม zone, rack หรือ node:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: apimaxSkew: 1 หมายความว่า zone ไหนก็ห้ามมี pod มากกว่า zone ที่มีน้อยที่สุดเกินหนึ่งตัว ถ้าเสีย zone หนึ่งไปในคลัสเตอร์สาม zone จะเสีย capacity ราวหนึ่งในสาม ไม่ใช่ครึ่งหนึ่งหรือทั้งหมด บน bare metal ให้ติด label rack ให้ node แล้วใช้เป็น topologyKey
Taints และ tolerations
affinity คือ pod เป็นฝ่ายเลือก node ส่วน taint คือ node เป็นฝ่ายปฏิเสธ pod:
kubectl taint nodes gpu-node-1 dedicated=gpu:NoScheduleเฉพาะ pod ที่มี toleration ตรงกันเท่านั้นที่ลง node นี้ได้:
spec:
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoScheduletoleration แค่ อนุญาต ให้ pod ลง node นั้นได้ ไม่ได้บังคับให้ไป ถ้าอยากกัน GPU node ไว้ให้งาน GPU และให้ pod ที่ใช้ GPU ไปลงที่นั่นด้วย ต้องใช้ taint คู่กับ node affinity บน label ของ node
Debug pod ที่ค้าง Pending
Pending แทบทุกครั้งแปลว่าทุก node ถูกตัดออกตอน filtering และ event จะบอกเหตุผล:
kubectl describe pod api-7d9f8c6b5-x2k4qดูในส่วน Events จะเจอข้อความคล้าย ๆ แบบนี้:
0/6 nodes are available: 3 Insufficient memory, 3 node(s) had untolerated taint {dedicated: gpu}.ไล่เช็กสาเหตุที่เจอบ่อย:
- CPU หรือ memory ไม่พอ: requests มากกว่า allocatable ที่เหลือของทุก node ให้ลด requests หรือให้ Cluster Autoscaler เพิ่ม node
- affinity หรือ nodeSelector ไม่ตรง: ไม่มี node ไหนมี label ที่ต้องการ
- ติด taint ที่ไม่ tolerate: ทุก node ที่มีที่ว่างติด taint อยู่
- topology หรือ anti-affinity: กฎแบบ
requiredหรือDoNotScheduleทำให้ไม่ได้กับ node ที่มีอยู่ - PVC ยังไม่ bind: pod รอ volume อยู่ ดู Kubernetes storage: PV, PVC และ StorageClass
คำถามที่พบบ่อย
requests กับ limits ใน Kubernetes ต่างกันยังไง
requests คือสิ่งที่ container จองไว้ และ scheduler ใช้ค่านี้วาง pod ส่วน limits คือเพดานตอนรันจริง CPU เกิน limit จะโดน throttle ส่วน memory เกิน limit จะโดน OOMKill
ทำไม pod โดน OOMKilled ทั้งที่ node ยังมี memory ว่าง
container ใช้เกิน memory limit ของตัวเอง memory ว่างของ node ไม่เกี่ยว เพราะ cgroup limit เป็นของแต่ละ container ให้เพิ่ม limit หรือลดการใช้ memory ของแอป
ทำยังไงให้ pod เป็น QoS แบบ Guaranteed
ทุก container ใน pod ต้องตั้ง CPU และ memory ทั้ง requests และ limits และ requests ต้องเท่ากับ limits รวมถึง init container ด้วย
scheduler ดูการใช้ CPU จริงไหม
ไม่ดู มันใช้ผลรวม requests บนแต่ละ node node ที่ใช้ CPU จริงแค่ 10% ก็ยังปฏิเสธ pod ได้ถ้า requests ถูกจองเต็มแล้ว
ไม่ตั้ง CPU limit เลยจะมีปัญหาไหม
ไม่จำเป็นต้องมีปัญหา ถ้า CPU requests ตั้งไว้ถูกต้อง การไม่ตั้ง CPU limit ช่วยเลี่ยง throttling และให้ pod ใช้ CPU ที่ว่างอยู่ได้ แต่ memory limit ควรตั้งไว้เสมอ
เช็กลิสต์ requests และ limits
- ตั้ง CPU และ memory requests ให้ทุก container จากการใช้งานที่วัดได้จริง
- ตั้ง memory limit เท่ากับ memory request ส่วน CPU limit ให้ตัดสินใจอย่างตั้งใจ
- ใส่
LimitRangeในทุก namespace เพื่อไม่ให้มี pod กลายเป็น BestEffort โดยไม่ตั้งใจ - workload สำคัญตั้งเป็น Guaranteed พร้อม PriorityClass ที่สูงกว่า
- กระจาย replica ข้าม node และ zone ด้วย topology spread constraints
- กัน hardware พิเศษด้วย taint และใช้คู่กับ node affinity
- เริ่มสืบ
Pendingทุกครั้งด้วยkubectl describe podและดู events
เมื่อตั้งค่าครบตามนี้ scheduler จะวาง pod ลง node ได้คุ้ม และ node ที่พังหรือ pod ที่กินทรัพยากรเกินตัวเดียวจะทำ service ล่มทั้งตัวไม่ได้ ถ้าอยากฝึกเรื่อง scheduling และการปรับ resource บนคลัสเตอร์จริง Vectorkub มี คอร์ส DevOps ฟรี
