Scheduler กับ Resource Requests
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Scheduler วาง Pod ลงบน Node ที่มี capacity ว่างพอจะรองรับ requests ของตัวเอง ส่วน limits คือเพดานที่จำกัดว่า Pod ใช้ resource ได้มากแค่ไหนตอนที่รันอยู่จริง — และเมื่อไม่มี capacity เหลือพอที่ไหนเลย priority กับ preemption จะเป็นตัวตัดสินว่า Pod ที่ priority ต่ำกว่าจะถูก evict เพื่อเปิดที่ว่างหรือไม่
requests คือพื้นล่าง limits คือเพดานบน
หัวข้อที่มีชื่อว่า “requests คือพื้นล่าง limits คือเพดานบน”ทุก container ใน Pod ประกาศ resources.requests และ resources.limits สำหรับ CPU และ memory ได้ และตัวเลขสองตัวนี้ทำหน้าที่ต่างกันโดยสิ้นเชิง
requestsคือขั้นต่ำที่ container ต้องการแน่ ๆ scheduler จะนับ Node เป็นตัวเลือกก็ต่อเมื่อ Node นั้นมี capacity ที่ ยังไม่ถูกจอง พอจะครอบคลุม requests ของทุก container — ไม่สนใจ limits เลยตอนตัดสินใจว่า Pod ไปลงที่ไหนได้limitsคือเพดานแข็งที่ถูกบังคับใช้ตอน runtime หลังจาก Pod รันไปแล้ว CPU เป็น resource ที่บีบอัดได้ ดังนั้นการใช้เกิน limit ของ CPU จะถูก throttle ไม่ใช่ถูกฆ่า ส่วน memory บีบอัดไม่ได้ container ที่ใช้ memory เกิน limit จะโดน OOMKilled แล้ว restart
apiVersion: v1kind: Podmetadata: name: payments-workerspec: containers: - name: worker image: payments-worker:1.4.0 resources: requests: cpu: "500m" memory: "256Mi" limits: cpu: "500m" memory: "256Mi"# See which node a Pod landed on, and check the node's spare capacitykubectl get pod payments-worker -o jsonpath='{.spec.nodeName}{"\n"}'kubectl describe node <node-name> | grep -A5 "Allocated resources"เพราะ scheduler มองแค่ requests เท่านั้น Pod ที่ไม่ตั้ง requests เลยจึงมองไม่เห็นในสายตาของ capacity planning คือถูก schedule ไปที่ไหนก็ได้ และยังแย่ง resource จน Pod ข้าง ๆ ที่ตั้ง requests ไว้ถูกต้องอาจอดได้ด้วย
requests กับ limits ตอน runtime พิสูจน์จริง: throttling กับ OOMKilled
หัวข้อที่มีชื่อว่า “requests กับ limits ตอน runtime พิสูจน์จริง: throttling กับ OOMKilled”สองรูปแบบความล้มเหลวข้างบนไม่สมมาตรกัน คุ้มค่าที่จะเห็นทั้งคู่จริง ๆ ไม่ใช่แค่เชื่อตามคำบอก
CPU ที่เกิน limit จะถูก throttle ไม่มีวันถูกฆ่า container ที่ตั้ง limits.cpu: "200m" แล้วรัน stress --cpu 1 (ที่พยายามใช้ core เต็ม) แสดง cgroup throttling accounting จริงจากข้างในกันเอง
apiVersion: v1kind: Podmetadata: name: cpu-hogspec: restartPolicy: Never containers: - name: hog image: polinux/stress command: ["stress"] args: ["--cpu", "2"] resources: requests: cpu: "100m" limits: cpu: "200m"$ kubectl exec cpu-hog -- sh -c 'cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/cpu.stat'20000 100000usage_usec 2739831user_usec 2723945system_usec 15885nr_periods 136nr_throttled 136throttled_usec 12185810cpu.max อ่านได้ 20000 100000 คือการยืนยันของ kernel เองว่า limit 200m (quota 20ms ต่อ period 100ms) nr_throttled 136 จาก nr_periods 136 แปลว่า ทุก accounting period เดียว โดนใช้ quota จนหมดและถูก throttle — throttled_usec 12185810 คือมากกว่า 12 วินาทีของเวลาจริงที่ container นี้หยุดพัก ไม่ใช่ถูกฆ่า รอ quota window ถัดไป container ยังคงเป็น Running ตลอดเวลา
Memory ที่เกิน limit จะโดน OOMKilled พร้อม exit code จริง
apiVersion: v1kind: Podmetadata: name: mem-hogspec: restartPolicy: OnFailure containers: - name: hog image: polinux/stress command: ["stress"] args: ["--vm", "1", "--vm-bytes", "300M", "--vm-hang", "1"] resources: requests: memory: "64Mi" limits: memory: "100Mi"$ kubectl get pod mem-hogNAME READY STATUS RESTARTS AGEmem-hog 0/1 OOMKilled 2 (16s ago) 20s
$ kubectl describe pod mem-hog Last State: Terminated Reason: OOMKilled Exit Code: 137Exit code 137 คือ 128 + 9 (SIGKILL) — kernel’s OOM killer ไม่ใช่ Kubernetes เป็นคนจบ process ทันทีที่ cgroup memory limit ถูกข้าม kubelet แค่รายงานผลแล้วด้วย restartPolicy: OnFailure ก็ restart container ซึ่งก็โดน OOMKilled ซ้ำอีกด้วย workload เดิม
QoS class ตัดสินว่าใครโดน evict ก่อน
หัวข้อที่มีชื่อว่า “QoS class ตัดสินว่าใครโดน evict ก่อน”Kubernetes คำนวณ Quality of Service (QoS) class ให้ทุก Pod อัตโนมัติจาก requests และ limits ของตัวเอง — คุณไม่ต้องตั้งค่า field นี้เอง
- Guaranteed — ทุก container มี
requestsเท่ากับlimitsทั้ง CPU และ memory - Burstable — มีอย่างน้อยหนึ่ง container ตั้ง requests ไว้ แต่ค่าต่างจาก limits หรือมีแค่บาง resource ที่ตั้งทั้งคู่
- BestEffort — ไม่มี container ไหนตั้ง requests หรือ limits เลย
พิสูจน์ ด้วย Pod 3 ตัวที่ตรงตาม spec ข้างบนพอดี อ่านค่ากลับ
apiVersion: v1kind: Podmetadata: name: qos-guaranteedspec: containers: - name: c image: registry.k8s.io/pause:3.10 resources: requests: { cpu: "200m", memory: "128Mi" } limits: { cpu: "200m", memory: "128Mi" }---apiVersion: v1kind: Podmetadata: name: qos-burstablespec: containers: - name: c image: registry.k8s.io/pause:3.10 resources: requests: { cpu: "100m", memory: "64Mi" } limits: { cpu: "500m", memory: "256Mi" }---apiVersion: v1kind: Podmetadata: name: qos-besteffortspec: containers: - name: c image: registry.k8s.io/pause:3.10$ for p in qos-guaranteed qos-burstable qos-besteffort; do echo -n "$p: "; kubectl get pod $p -o jsonpath='{.status.qosClass}{"\n"}' doneqos-guaranteed: Guaranteedqos-burstable: Burstableqos-besteffort: BestEffortQoS class จะมีผลก็ต่อเมื่อ Node เริ่มขาด resource เมื่อ kubelet ต้องเรียก memory คืนภายใต้ pressure จะ evict Pod ระดับ BestEffort ก่อน ตามด้วย Burstable และจะแตะ Pod ระดับ Guaranteed ก็ต่อเมื่อไม่เหลือทางเลือกอื่นแล้วเท่านั้น
LimitRange: แอบใส่ requests ที่คุณไม่เคยเขียน
หัวข้อที่มีชื่อว่า “LimitRange: แอบใส่ requests ที่คุณไม่เคยเขียน”LimitRange ตั้งค่า default ต่อ container ให้ namespace ดังนั้น Pod ที่ไม่มี resources block เลยจะไม่กลายเป็น BestEffort แบบเงียบ ๆ — มันจะได้ default ของ namespace ถูกฉีดเข้ามาตอน admission พิสูจน์
apiVersion: v1kind: LimitRangemetadata: name: default-limitsspec: limits: - type: Container defaultRequest: cpu: "250m" memory: "128Mi" default: cpu: "500m" memory: "256Mi"apiVersion: v1kind: Podmetadata: name: no-resourcesspec: containers: - name: c image: registry.k8s.io/pause:3.10$ kubectl get pod no-resources -o jsonpath='{.spec.containers[0].resources}{"\n"}'{"limits":{"cpu":"500m","memory":"256Mi"},"requests":{"cpu":"250m","memory":"128Mi"}}manifest ของ Pod ไม่ได้พูดถึง resources เลยแม้แต่นิดเดียว — LimitRange ใน namespace ของมันฉีด requests 250m/128Mi และ limits 500m/256Mi เข้าไปก่อนที่ object จะถูก persist ด้วยซ้ำ นี่คือเหตุผลที่ Pod ตัวนี้เป็น Burstable ไม่ใช่ BestEffort และทำไม “ไม่มี resources block” ไม่ได้แปลว่า “มองไม่เห็นในสายตา capacity planning” เสมอไป — ขึ้นอยู่กับว่า namespace นั้นมี LimitRange หรือเปล่า
Allocatable กับ Capacity
หัวข้อที่มีชื่อว่า “Allocatable กับ Capacity”kubectl describe node รายงานตัวเลขสองชุดแยกกัน และมีแค่ตัวเดียวที่ scheduler ใช้จริง
$ kubectl describe node scheduling-workerCapacity: cpu: 11 memory: 9144196KiAllocatable: cpu: 11 memory: 9144196KiCapacity คือ hardware ทั้งหมดของ Node Allocatable คือ capacity ลบด้วยส่วนที่ kubelet กับ system daemon จองไว้ให้ตัวเอง (--system-reserved/--kube-reserved) และ Allocatable ไม่ใช่ Capacity ที่ Filter phase ของ scheduler ใช้เทียบกับ requests ของ Pod บน kind node นี้ทั้งสองค่าเท่ากันเพราะ kind ไม่ได้ตั้ง flag การจองใด ๆ — บน cloud-managed node pool จริง Allocatable มักต่ำกว่า Capacity 5-10% เป็นปกติ และ Pod ที่คำนวณขนาดจากตัวเลขผิดตัวอาจดูเหมือนพอดีบนกระดาษแล้วไม่เคย schedule ได้เลยจริง ๆ
PriorityClass กับ preemption พิสูจน์จริง
หัวข้อที่มีชื่อว่า “PriorityClass กับ preemption พิสูจน์จริง”ทุก Pod มี priority — 0 โดย default PriorityClass ที่ตั้ง value สูงกว่าทำสองอย่าง: ดัน Pod ข้ามหน้า Pod priority ต่ำกว่าใน scheduling queue และถ้า cluster ยังไม่มีที่ว่างให้ scheduler จะพยายาม preempt (evict) Pod priority ต่ำกว่าที่อื่นเพื่อเปิดที่ว่างนั้น
apiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: low-priorityvalue: 100globalDefault: falsedescription: "Low priority batch work, preemptable."---apiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: high-priorityvalue: 1000000globalDefault: falsedescription: "High priority workload, may preempt lower-priority pods."globalDefault: true บน PriorityClass ทำให้มันมีผลกับทุก Pod ที่ไม่ระบุ priorityClassName — มีได้แค่ PriorityClass เดียวใน cluster ที่ตั้งค่านี้เป็น true และ API จะปฏิเสธตัวที่สอง
พิสูจน์ pin Pod low-priority สองตัวที่ request cpu: "5" แต่ละตัวไปที่ Node เดียว (10 จาก 11 CPU ของมัน) แล้วสร้าง Pod high-priority บน Node เดียวกันที่ request cpu: "3" — มากกว่า 1 CPU ที่เหลืออยู่
$ kubectl get pods -o wideNAME READY STATUS NODEfiller-54bbb658fb-lpwpj 1/1 Running scheduling-workerfiller-54bbb658fb-xs2pg 1/1 Running scheduling-workerimportant-pod 1/1 Running scheduling-worker
$ kubectl get events --sort-by=.lastTimestampWarning FailedScheduling pod/important-pod 0/4 nodes are available: 1 Insufficient cpu, 1 node(s) had untolerated taint(s), 2 node(s) didn't match Pod's node affinity/selector. preemption: found a potential placement for pod on node scheduling-worker, preempting 1 victimsNormal Killing pod/filler-54bbb658fb-xs2pg Stopping container fillerNormal Preempted pod/filler-54bbb658fb-xs2pg Preempted by pod bd29493f-c306-4440-bded-95dc1aa81181 on node scheduling-workerNormal Scheduled pod/important-pod Successfully assigned sched-demo/important-pod to scheduling-workerWarning FailedScheduling pod/filler-54bbb658fb-5db89 0/4 nodes are available: 1 Insufficient cpu, ... preemption: 0/4 nodes are available: 1 No preemption victims found for incoming pod, 3 Preemption is not helpful for scheduling.important-pod ได้ Scheduled หลังจาก Pod filler ตัวหนึ่งถูก Preempted (event จริง reason จริง) เพื่อเปิด 5 CPU ของมัน — แล้ว Pod replacement ของ ReplicaSet ก็ค้างอยู่ที่ Pending จริง ๆ เพราะตอนนี้มันเองกลายเป็น Pod priority ต่ำกว่าที่ไม่เหลืออะไรให้ preempt แล้ว
preemptionPolicy: Never ให้ Pod priority สูงเลือกที่จะ ไม่ evict ใครเลย — มันยังข้ามหน้า queue แต่รอ capacity ว่างขึ้นเองแทน
apiVersion: v1kind: Podmetadata: name: patient-podspec: priorityClassName: high-priority-nonpreempting containers: - name: c image: registry.k8s.io/pause:3.10 resources: requests: cpu: "5"$ kubectl describe pod patient-podEvents: Warning FailedScheduling 0/4 nodes are available: 1 Insufficient cpu, ... preemption: not eligible due to preemptionPolicy=Never.reason ที่แม่นยำนั้น — not eligible due to preemptionPolicy=Never — คือหลักฐานจริง: Pod ค้างที่ Pending เต็ม ๆ แทนที่จะ evict เพื่อนบ้าน priority ต่ำกว่าที่ปกติแล้วเป็นเป้าได้
Scheduler เลือก Node อย่างไร filter score bind
หัวข้อที่มีชื่อว่า “Scheduler เลือก Node อย่างไร filter score bind”สำหรับทุก Pod ที่ยังไม่ถูก schedule scheduler จะรัน pipeline ของ extension point (scheduling framework) ก่อนจะฟันธง แต่ละจุดถูก implement ด้วย plugin หนึ่งตัวหรือมากกว่า
PreFilter/Filter— ตัด Node ที่รัน Pod นี้ไม่ได้เด็ดขาดออกไปทั้งหมดNodeResourcesFitfilter ตาม requests เทียบ Allocatable,TaintTolerationfilter ตาม taint ที่ไม่ tolerate,InterPodAffinityกับPodTopologySpreadfilter ตามกฎ affinity/anti-affinity และ spread ที่เหลือคือชุด Node ที่ เป็นไปได้PostFilter— รันก็ต่อเมื่อไม่มี Node ไหนเป็นไปได้เลย นี่คือจุดที่DefaultPreemptionมองหา victim ที่ต่ำกว่าให้ evict ตามที่พิสูจน์ข้างบนPreScore/Score/NormalizeScore— จัดอันดับ Node ที่เป็นไปได้ (กระจายข้าม zone, อัด density แบบ bin-pack และอื่น ๆ) แล้วเลือกตัวที่คะแนนสูงสุดReserve/Permit/PreBind/Bind/PostBind— การจอง Node ที่ชนะจะถูก commit แล้ว scheduler จะ bind Pod เข้ากับ Node นั้นโดยเขียนspec.nodeNameแล้ว kubelet บน Node นั้นจะรับช่วงต่อและสั่ง start container
flowchart LR pod["Pod spec\n(requests + limits + priority)"] --> filter["Filter:\nwhich Nodes CAN run it"] filter -->|feasible Nodes exist| score["Score:\nrank the feasible Nodes"] filter -->|none feasible| postfilter["PostFilter:\nDefaultPreemption looks for victims"] postfilter -->|victim found| score score --> bind["Bind:\nPod assigned to winning Node"] bind --> node["Node"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกอย่างในบทนี้คือเรื่องเดียวกัน: scheduler ให้เหตุผลแค่กับ requests เท่านั้น ไม่เคยดู limits ตอนตัดสินใจวาง Pod ดังนั้น requests คือหน่วยจริงของ “Pod ตัวนี้ใหญ่แค่ไหน” ในมุมของ scheduling และทุกอย่างที่ตามมาก็ไหลจากตรงนี้ QoS class คือชื่อเรียกของ รูปร่าง ของ requests/limits ของ Pod (เท่ากัน ต่างกัน หรือไม่มีเลย) และจะเห็นผลก็ต่อเมื่อ memory ตึงเท่านั้น priority กับ preemption มีไว้สำหรับตอนที่ requests อย่างเดียวไม่พอจะหาที่ว่างได้ — Pod priority สูงกว่าบังคับให้ Pod priority ต่ำกว่าหลีกทางได้ เว้นแต่จะปฏิเสธไว้ชัดเจนด้วย preemptionPolicy: Never LimitRange กับ QoS ต่างก็กำหนดรูปร่างว่า requests/limits ของ Pod จะกลายเป็นอะไร — scheduler กับ kubelet ทำงานกับตัวเลขสุดท้ายที่ resolve แล้วเท่านั้น ไม่ใช่สิ่งที่คุณพิมพ์หรือไม่พิมพ์ใน manifest
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
requests == limits (Guaranteed QoS) | ไม่ถูก throttle ต่ำกว่า request เลย ถูก evict หลังสุดภายใต้ pressure (พิสูจน์ข้างบน) | ไม่มีที่ว่างให้ burst; over-provision สำหรับ workload ที่ spike |
requests < limits (Burstable QoS) | burst เกิน baseline ได้เมื่อ Node มี capacity เหลือ | ถูก evict ก่อน Guaranteed Pod ภายใต้ memory pressure; CPU ยังถูก throttle ที่ limit ได้ (พิสูจน์ข้างบน) |
ไม่มี requests/limits เลย (BestEffort) | ไม่ต้องตั้งค่าอะไรเลย | ถูก evict ก่อนใครเมื่อมี pressure ใด ๆ; มองไม่เห็นในสายตา capacity planning; LimitRange ใน namespace override ได้โดยไม่เตือน (พิสูจน์ข้างบน) |
PriorityClass value สูงพร้อม preemptionPolicy default | ถูก schedule ได้แม้ cluster เต็ม โดย evict Pod priority ต่ำกว่า (พิสูจน์ข้างบน) | Deployment priority สูงที่ตั้งค่าผิดอาจ cascade-evict workload priority ต่ำที่ไม่เกี่ยวข้องทั่วทั้ง Node |
preemptionPolicy: Never บน priority class สูง | ไม่เคย disrupt Pod อื่นที่กำลังรันเพื่อให้ตัวเองถูก schedule (พิสูจน์ข้างบน) | อาจรอ capacity ที่ไม่มีวันว่างขึ้นเองไปตลอดกาล |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “scheduler เช็ค limits ด้วย ไม่ใช่แค่ requests ตอนตัดสินใจว่า Pod ไปลงที่ไหน” พิสูจน์ข้างบน (และใน
kubectl explain/scheduler docs): มีแค่requestsเท่านั้นที่ถูกเทียบกับ Allocatable capacity ของ Node Pod ที่ตั้งlimits.memoryสูงมากยังถูก schedule ไปที่ Node ที่ memory ว่างน้อยได้ ถ้าrequests.memoryเล็ก - “container ที่เกิน CPU limit จะถูกฆ่า เหมือน memory” พิสูจน์ข้างบนด้วย cgroup accounting จริง: CPU ถูก throttle (
nr_throttled/throttled_usec) container ยังรันต่อ มีแค่เกิน memory limit เท่านั้นที่ทำให้ถูกฆ่า (OOMKilled, exit137) - “Pod ที่ไม่มี
resourcesblock เลยเป็น BestEffort เสมอ” พิสูจน์ข้างบน: ถ้า namespace มีLimitRangeพร้อมdefaultRequest/defaultadmission controller จะฉีด requests/limits จริงก่อน object ถูกสร้าง ทำให้กลายเป็นBurstableแทน - “preemption หาทางเปิดที่ว่างให้ Pod priority สูงกว่าได้เสมอ” พิสูจน์ข้างบนด้วย
preemptionPolicy: Never: Pod priority สูงตั้งค่าให้ไม่ evict ใครเลยได้ แล้วมันจะค้างที่Pending— reason จริงnot eligible due to preemptionPolicy=Never— แทนที่จะบังคับให้ capacity ว่างขึ้นมา - “Allocatable กับ Capacity คือสิ่งเดียวกัน แค่คำต่างกัน” พิสูจน์ข้างบน: มันเท่ากันแค่บังเอิญบน kind node ที่ไม่ได้ตั้งค่าอะไร scheduler filter เทียบกับ Allocatable ซึ่งบน cluster จริงคือ capacity ลบด้วยการจองของ kubelet/system
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review Pod spec, Deployment หรือ PriorityClass ที่ AI สร้าง เช็คสิ่งเหล่านี้
- container ที่ตั้ง
limits.memoryแต่ไม่ตั้งrequests.memory(หรือกลับกัน) — มันเป็นBurstableไม่ใช่Guaranteedและจะถูก evict ก่อน Pod ที่ตั้งทั้งคู่เท่ากัน พิสูจน์ข้างบน - โค้ดหรือ docs ที่สมมติว่า container ที่เกิน CPU limit จะถูกฆ่า — พิสูจน์ข้างบนแล้วว่ามันถูก throttle ไม่ใช่ถูกฆ่า มีแค่ memory เท่านั้นที่ทำให้ถูกฆ่าจริง (
OOMKilled, exit137) globalDefault: trueที่ copy ใส่PriorityClassใหม่โดยไม่เช็คว่า cluster มีตัวหนึ่งอยู่แล้วหรือเปล่า — API จะปฏิเสธglobalDefault: trueตัวที่สอง และถ้าใช้กับทุก Pod ที่มีอยู่แบบไม่ตั้งใจ นั่นมักไม่ใช่เจตนาจริงPriorityClassที่ตั้งไว้เพื่อรับประกันการ schedule โดยไม่คิดถึงpreemptionPolicyเลย — ค่า default (PreemptLowerPriority) evict workload อื่นที่แชร์ Node เดียวกันได้ ถ้าเจตนาคือ “สำคัญแต่ไม่รบกวนใคร” ต้องใช้preemptionPolicy: Neverพิสูจน์ข้างบน- สมมติฐานเรื่อง capacity planning ที่อ้างอิงจากบรรทัด Capacity ของ
kubectl describe nodeแทนที่จะเป็น Allocatable — พิสูจน์ข้างบนว่ามันเท่ากันแค่บังเอิญบน kind node ที่ไม่ได้ตั้งค่า
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: Podmetadata:name: web-appspec:containers: - name: web image: web-app:3.2.0 resources: limits: cpu: "500m" memory: "512Mi"เฉลย
Pod นี้ตั้ง limits แต่ไม่ตั้ง requests เลย ดูเหมือนปลอดภัย — มีเพดาน memory อยู่ — แต่ไม่มี requests ทำให้มันไม่ใช่ Guaranteed API server จะ default requests ให้เท่ากับ limits ก็ต่อเมื่อทั้งคู่ถูกตั้งพร้อมกันใน client tooling รุ่นใหม่เท่านั้น และถ้า namespace นี้ไม่มี LimitRange QoS class จริงของ Pod จะกลายเป็น Burstable โดย scheduler มอง requests ของมันเป็นค่าที่ resolve จาก limits ตอน admission ซึ่งง่ายที่จะเข้าใจผิดว่า “ฉันตั้ง limits แล้ว ก็อธิบายครบแล้ว” พิสูจน์ข้างบน: วิธีที่เชื่อถือได้ในการได้ QoS Guaranteed (evict หลังสุด ไม่ถูก throttle ต่ำกว่าพื้น) คือตั้ง requests กับ limits ให้เป็นค่าเดียวกันชัดเจนสำหรับทุก resource ในทุก container ไม่ใช่ตั้งแค่ limits แล้วหวังว่าจะได้ผล