🇬🇧 Read the English version of this article on tanhdev.com
Answer-first: In-Place Pod Resizing (chính thức đạt trạng thái Stable/GA từ Kubernetes v1.35) cho phép cập nhật tài nguyên CPU và Memory trực tiếp trên container đang chạy thông qua subresource
/resizemà không cần khởi động lại Pod. Tính năng này yêu cầu container runtime hỗ trợ CRI APIUpdateContainerResources(containerd ≥ 1.6.9 hoặc CRI-O ≥ 1.25), kết hợp với chế độ VPAupdateMode: "InPlace"giúp loại bỏ hoàn toàn tình trạng gián đoạn kết nối, rút ngắn độ trễ cold start của AI inference từ hàng phút xuống còn dưới 1 giây và cắt giảm 40–60% chi phí hạ tầng.
1. Bối Cảnh Lịch Sử: Tại Sao Việc Restart Pod Lại Là Thảm Họa Kiến Trúc?
Trước phiên bản Kubernetes v1.35, tài nguyên CPU và Memory của container được gắn chặt vào thuộc tính bất biến (immutable specification) của Pod spec. Khi tải xử lý tăng đột biến và hệ thống cần thêm tài nguyên, Vertical Pod Autoscaler (VPA) hoặc kỹ sư vận hành buộc phải thực hiện thao tác Evict & Recreate (tiêu hủy Pod cũ và khởi tạo Pod mới trên Node khác).
Quy trình này gây ra hậu quả thảm khốc đối với nhiều loại workload trọng yếu:
- Mô hình AI / LLM Inference (vLLM, TGI, Triton): Một Pod nạp mô hình Llama-3-70B hoặc Qwen-72B cần tải 30 GB – 140 GB model weights từ NVMe storage hoặc mạng Ceph/S3 vào VRAM/RAM. Thời gian khởi động nguội (cold start) kéo dài từ 45 giây đến hơn 5 phút. Việc tiêu hủy Pod chỉ để tăng thêm 4 vCPU sẽ làm rớt toàn bộ các luồng suy luận streaming đang phục vụ khách hàng.
- Cơ sở dữ liệu Stateful (PostgreSQL, TiDB, Redis): Pod bị khởi động lại đồng nghĩa với việc đóng hàng nghìn kết nối TCP client, xóa sạch bộ nhớ đệm Buffer Pool / Shared Buffers, và kích hoạt quy trình failover réo gọi bầu cử Leader giữa các cụm lưu trữ phân tán.
- Các tác vụ xử lý lô thời gian dài (Long-running Data Pipelines): Một Job Apache Spark hoặc ETL Go worker đang chạy tới 95% sau 4 tiếng tính toán sẽ bị hủy ngang và phải chạy lại từ đầu nếu Pod bị evict để tăng RAM.
In-Place Pod Resizing giải quyết dứt điểm vấn đề này bằng cách tách rời việc điều chỉnh hạn mức tài nguyên Linux cgroups khỏi vòng đời tồn tại (lifecycle) của Pod.
flowchart LR
subgraph OldWay ["Trước K8s v1.35 (Eviction & Recreate)"]
A1["Tải Tăng Cao"] --> A2["VPA Evict Pod"]
A2 --> A3["Pod Chấm Dứt (SIGTERM/KILL)"]
A3 --> A4["Scheduler Tìm Node Mới"]
A4 --> A5["Pull Image & Nạp Weights (45s - 5m)"]
A5 --> A6["Pod Sẵn Sàng Trở Lại (Cold Start)"]
end
subgraph NewWay ["Từ K8s v1.35 (In-Place Pod Resizing)"]
B1["Tải Tăng Cao"] --> B2["Gửi Lệnh PATCH /resize"]
B2 --> B3["Kubelet gọi CRI UpdateContainerResources"]
B3 --> B4["Cập nhật tức thì cgroup limits (< 500ms)"]
B4 --> B5["Pod Tiếp Tục Chạy Không Gián Đoạn ✅"]
end
2. Bảng So Sánh Chi Tiết Các Cơ Chế Scaling Trong Kubernetes
| Đặc Điểm Kỹ Thuật | HPA (Horizontal Pod Autoscaler) | VPA Truyền Thống (updateMode: "Recreate") | In-Place Pod Resizing (VPA updateMode: "InPlace") |
|---|---|---|---|
| Phương thức scaling | Tăng/giảm số lượng bản sao Pods (Replicas) | Tăng/giảm CPU/RAM bằng cách tiêu hủy và tạo lại Pod | Cập nhật trực tiếp CPU/RAM trên container đang chạy |
| Ảnh hưởng kết nối | Không ảnh hưởng Pod cũ (nhưng Pod mới cần warm-up) | Rớt toàn bộ kết nối in-flight của Pod cũ | Không rớt bất kỳ kết nối mạng nào |
| Độ trễ hoàn tất scale | 30s – 120s (chờ container pull & ready) | 45s – 300s (cold start nạp dữ liệu/model) | < 1 giây (cập nhật cgroups v2 tức thì) |
| Phù hợp workload | Stateless Web APIs, Microservices | Batch Jobs không nhạy cảm thời gian | Stateful DBs, AI/LLM Inference, Java/Go Memory Caching |
| Tiêu tốn IP mạng | Mỗi Pod mới tiêu tốn 1 IP trong VPC CNI | Tạm thời tiêu tốn 2 IP trong giai đoạn bàn giao | Giữ nguyên 1 IP duy nhất của Pod |
| Yêu cầu K8s Version | Mọi phiên bản | v1.14+ | v1.35+ (Stable GA) |
3. Bản Chất Tầng Sâu: cgroups v2 & CRI UpdateContainerResources
Để hiểu tại sao việc resize có thể diễn ra trong mili-giây mà không làm sập tiến trình bên trong container, chúng ta cần xem xét cơ chế tương tác giữa Kubelet, Container Runtime (containerd/CRI-O) và Linux Kernel cgroups v2:
sequenceDiagram
autonumber
participant Client as k8s client-go / VPA
participant API as Kubernetes API Server
participant Kubelet as Node Kubelet
participant CRI as containerd (CRI Server)
participant Kernel as Linux Kernel (cgroups v2)
Client->>API: PATCH /api/v1/namespaces/default/pods/ai-worker/resize
API->>API: Xác thực hạn mức LimitRange & ResourceQuota
API->>API: Ghi nhận spec.containers[].resources & đặt status.resize = "Proposed"
Kubelet->>API: Watcher phát hiện thay đổi trên Pod Spec
Kubelet->>Kubelet: Kiểm tra tài nguyên Node còn đủ (Allocatable Resources)
alt Node không đủ tài nguyên
Kubelet->>API: Cập nhật status.resize = "Deferred"
else Đủ tài nguyên
Kubelet->>API: Cập nhật status.resize = "InProgress"
Kubelet->>CRI: gRPC UpdateContainerResourcesRequest(containerID, newLimits)
CRI->>Kernel: Ghi file /sys/fs/cgroup/system.slice/.../cpu.max
CRI->>Kernel: Ghi file /sys/fs/cgroup/system.slice/.../memory.max
Kernel-->>CRI: Thành công (Kernel áp dụng hạn mức mới tức thì)
CRI-->>Kubelet: UpdateContainerResourcesResponse(Success)
Kubelet->>API: Cập nhật status.containerStatuses[].resources = newLimits
Kubelet->>API: Cập nhật status.resize = "" (Hoàn tất)
end
Cách Linux Kernel Xử Lý Việc Thay Đổi Cgroup Limits:
- Đối với CPU (
cpu.max): Tệp tin/sys/fs/cgroup/.../cpu.maxchứa hai giá trị:quotavàperiod(mặc định period là 100.000 microseconds = 100ms). Khi tăng từ 4 vCPU lên 8 vCPU, containerd chỉ đơn giản là ghi đè giá trịquotatừ400000thành800000. CFS (Completely Fair Scheduler) của Linux Kernel lập tức cấp thêm thời gian CPU time slice cho tiến trình trong chu kỳ tiếp theo mà không cần khởi động lại tiến trình. - Đối với Memory (
memory.max): Containerd ghi giá trị byte mới vào/sys/fs/cgroup/.../memory.max.- Nếu Tăng Memory: Linux kernel mở rộng ngưỡng trần cấp phát bộ nhớ. Khi tiến trình gọi
malloc()hoặc mở rộng heap, kernel cho phép cấp thêm physical pages mà không kích hoạt OOM killer. - Nếu Giảm Memory: Linux kernel kiểm tra mức sử dụng bộ nhớ thực tế (Resident Set Size - RSS) hiện tại của container. Nếu mức limit mới thấp hơn RSS đang dùng, kernel sẽ cố gắng thu hồi Page Cache (file-backed memory). Nếu vẫn không đủ, kernel OOM Killer sẽ bị kích hoạt ngay lập tức! Do đó, giảm Memory limit luôn tiềm ẩn rủi ro OOM cao hơn tăng Memory.
- Nếu Tăng Memory: Linux kernel mở rộng ngưỡng trần cấp phát bộ nhớ. Khi tiến trình gọi
4. Cấu Hình Resize Policy Chuẩn Production (YAML Manifests)
Mỗi container bên trong Pod spec hỗ trợ trường resizePolicy, cho phép chỉ định rõ ràng hành vi khi thay đổi CPU hoặc Memory:
apiVersion: v1
kind: Pod
metadata:
name: vllm-llama3-70b
namespace: ai-inference
labels:
app: vllm-engine
workload: latency-sensitive
spec:
containers:
- name: inference-engine
image: vllm/vllm-openai:v0.6.2
imagePullPolicy: IfNotPresent
# Cấu hình chính sách resize cho từng loại tài nguyên
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # Điều chỉnh CPU hoàn toàn không cần restart
- resourceName: memory
restartPolicy: NotRequired # An toàn cho AI inference nhờ model weights được mmap
resources:
requests:
cpu: "8"
memory: "32Gi"
limits:
cpu: "16"
memory: "64Gi"
ports:
- containerPort: 8000
name: http-api
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 5
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: nfs-nvme-model-cache
Bảng Ý Nghĩa Các Trạng Thái status.resize Trong Kubernetes:
Trạng Thái (status.resize) | Ý Nghĩa Kỹ Thuật | Hành Động Cần Thiết |
|---|---|---|
"" (Rỗng) | Quá trình thay đổi tài nguyên đã hoàn tất thành công hoặc không có yêu cầu nào đang chờ. | Không cần can thiệp. Trạng thái bình thường. |
Proposed | Yêu cầu resize đã được API Server chấp nhận và ghi vào etcd, nhưng Kubelet trên Node chưa tiếp nhận. | Chờ Kubelet reconcile loop (thường < 500ms). |
InProgress | Kubelet đã nhận lệnh và đang gọi gRPC UpdateContainerResources tới containerd. | Đang trong quá trình cập nhật cgroup limits. |
Deferred | Node hiện tại không còn đủ tài nguyên nhàn rỗi (allocatable) để đáp ứng yêu cầu mới. | Kubelet sẽ thử lại khi có Pod khác trên Node giải phóng tài nguyên. Có thể cần Cluster Autoscaler scale thêm Node. |
Infeasible | Mức tài nguyên yêu cầu vượt quá tổng công suất tối đa (Node Capacity) của máy chủ vật lý. | Yêu cầu không thể đáp ứng trên Node này. Bắt buộc phải chuyển Pod sang Node có cấu hình phần cứng lớn hơn. |
5. Triển Khai Production Controller Trong Go Bằng client-go
Dưới đây là mã nguồn Go hoàn chỉnh xây dựng một Custom Controller chuyên nghiệp, sử dụng thư viện chuẩn k8s.io/client-go để theo dõi và thực thi In-Place Pod Resizing thông qua subresource /resize:
package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
)
var (
ErrResizeDeferred = errors.New("yêu cầu resize bị hoãn: Node không đủ tài nguyên nhàn rỗi")
ErrResizeInfeasible = errors.New("yêu cầu resize không khả thi: Vượt quá dung lượng vật lý của Node")
ErrResizeTimeout = errors.New("quá thời gian chờ hoàn tất quá trình resize")
)
// PodResizePatcher quản lý việc gửi yêu cầu cập nhật tài nguyên tới subresource /resize
type PodResizePatcher struct {
kubeClient kubernetes.Interface
timeout time.Duration
}
func NewPodResizePatcher(client kubernetes.Interface, timeout time.Duration) *PodResizePatcher {
if timeout <= 0 {
timeout = 45 * time.Second
}
return &PodResizePatcher{
kubeClient: client,
timeout: timeout,
}
}
// ResourceTarget chứa cấu hình CPU/Memory mong muốn cập nhật
type ResourceTarget struct {
ContainerName string
CPURequest string
CPULimit string
MemRequest string
MemLimit string
}
// InPlaceResizePod gửi lệnh PATCH trực tiếp tới subresource /resize của Pod
func (p *PodResizePatcher) InPlaceResizePod(ctx context.Context, namespace, podName string, target ResourceTarget) error {
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
// 1. Tạo patch payload theo chuẩn JSON Merge Patch
patchData := map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": target.ContainerName,
"resources": map[string]interface{}{
"requests": map[string]string{
"cpu": target.CPURequest,
"memory": target.MemRequest,
},
"limits": map[string]string{
"cpu": target.CPULimit,
"memory": target.MemLimit,
},
},
},
},
},
}
payloadBytes, err := json.Marshal(patchData)
if err != nil {
return fmt.Errorf("lỗi đóng gói patch json: %w", err)
}
slog.Info("Đang gửi yêu cầu in-place resize tới subresource /resize",
"pod", podName, "container", target.ContainerName, "payload", string(payloadBytes))
// 2. Gửi PATCH request vào subresource "resize"
_, err = p.kubeClient.CoreV1().Pods(namespace).Patch(
ctx,
podName,
types.MergePatchType,
payloadBytes,
metav1.PatchOptions{
FieldManager: "ai-capacity-controller",
},
"resize", // Chỉ định subresource = "resize"
)
if err != nil {
return fmt.Errorf("gửi patch /resize thất bại: %w", err)
}
// 3. Theo dõi trạng thái hoàn tất của Pod status.resize
return p.waitForResizeCompletion(ctx, namespace, podName, target)
}
func (p *PodResizePatcher) waitForResizeCompletion(ctx context.Context, namespace, podName string, target ResourceTarget) error {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
targetCPU := resource.MustParse(target.CPULimit)
targetMem := resource.MustParse(target.MemLimit)
for {
select {
case <-ctx.Done():
return fmt.Errorf("%w: %v", ErrResizeTimeout, ctx.Err())
case <-ticker.C:
pod, err := p.kubeClient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
slog.Warn("Lỗi khi kiểm tra pod status", "pod", podName, "error", err)
continue
}
// Kiểm tra các trạng thái lỗi từ Kubelet
switch pod.Status.Resize {
case corev1.PodResizeStatusDeferred:
slog.Warn("Yêu cầu resize đang ở trạng thái Deferred", "pod", podName)
return ErrResizeDeferred
case corev1.PodResizeStatusInfeasible:
slog.Error("Yêu cầu resize không khả thi (Infeasible)", "pod", podName)
return ErrResizeInfeasible
}
// Kiểm tra xem container status đã phản ánh mức tài nguyên mới hay chưa
for _, cStatus := range pod.Status.ContainerStatuses {
if cStatus.Name == target.ContainerName {
currentLimitCPU := cStatus.Resources.Limits[corev1.ResourceCPU]
currentLimitMem := cStatus.Resources.Limits[corev1.ResourceMemory]
if currentLimitCPU.Cmp(targetCPU) == 0 && currentLimitMem.Cmp(targetMem) == 0 && pod.Status.Resize == "" {
slog.Info("In-Place Pod Resize thành công hoàn hảo",
"pod", podName, "cpu", target.CPULimit, "memory", target.MemLimit)
return nil
}
}
}
}
}
}
6. Tích Hợp Vertical Pod Autoscaler (VPA) Với Chế Độ InPlace
Để hệ thống tự động co giãn tài nguyên theo tải thực tế mà không cần viết custom script, chúng ta kết hợp tính năng này với VPA v1.3+ bằng cách thiết lập updateMode: "InPlace":
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: vllm-inference-vpa
namespace: ai-inference
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-llama3-70b
updatePolicy:
updateMode: "InPlace" # Kích hoạt chế độ In-Place không restart Pod
resourcePolicy:
containerPolicies:
- containerName: inference-engine
minAllowed:
cpu: "4"
memory: "24Gi"
maxAllowed:
cpu: "32"
memory: "128Gi"
controlledResources: ["cpu", "memory"]
controlledValues: RequestsAndLimits
Khi VPA Recommender phát hiện tải token generation tăng cao, VPA Updater sẽ trực tiếp gọi subresource /resize để tăng CPU/Memory cho Pod mà không thực hiện lệnh Evict. Nhờ đó, khách hàng đang nhận câu trả lời qua Server-Sent Events (SSE) vẫn nhận trọn vẹn từng từ mà không bị ngắt kết nối.
7. Các Cạm Bẫy Cần Tránh Khi Triển Khai Thực Tế (Production Gotchas)
- Bẫy Đổi QoS Class (QoS Class Immutability): Kubernetes nghiêm cấm việc thay đổi QoS Class của Pod trong quá trình resize. Nếu một Pod ban đầu được tạo với QoS Class
Guaranteed(requests = limits), bạn không thể resize sao cho requests < limits (sẽ biến thànhBurstable). Thao tác này sẽ bị API Server từ chối ngay lập tức. Hãy đảm bảo công thức requests/limits đồng nhất trước và sau khi resize. - Bẫy OOM Khi Giảm Memory: Giảm giới hạn memory của các ứng dụng có Garbage Collector (JVM, Go runtime) rất nguy hiểm vì GC runtime có thể chưa kịp dọn dẹp bộ nhớ đệm. Nếu hạ
memory.maxxuống dưới ngưỡng Resident Set Size (RSS), Linux Kernel sẽ lập tức kích hoạt OOM Killer và kết liễu container. Giải pháp: Chỉ nên resize tự động tăng tài nguyên (scale-up); đối với thao tác giảm tài nguyên (scale-down), hãy lập lịch vào các khung giờ đêm vớirestartPolicy: RestartContainer. - Bị Ghi Đè Khi Deployment Rollout: Mọi thay đổi In-Place Resize áp dụng trực tiếp lên Pod sẽ bị mất đi khi Deployment thực hiện rolling update phiên bản mới. Do đó, sau khi VPA hoặc controller điều chỉnh Pod thành công, hãy cập nhật ngược thông số tài nguyên mới vào trường
spec.template.spec.containers[].resourcescủa Deployment cha để đảm bảo tính nhất quán lâu dài.
8. Các Câu Hỏi Thường Gặp (FAQ)
Tính năng In-Place Pod Resizing chính thức đạt trạng thái Stable (GA) từ phiên bản Kubernetes nào?
In-Place Pod Resizing chính thức đạt trạng thái Stable (GA) từ phiên bản Kubernetes v1.35 (phát hành tháng 12/2025). Ở phiên bản này, tính năng được kích hoạt mặc định trên mọi cụm K8s mà không cần bật bất kỳ feature gate nào.
Để sử dụng tính năng, hạ tầng container runtime của các worker nodes bắt buộc phải hỗ trợ CRI API UpdateContainerResources, cụ thể là containerd phiên bản ≥ 1.6.9 hoặc CRI-O phiên bản ≥ 1.25.
Làm thế nào để điều chỉnh CPU và Memory mà hoàn toàn không làm gián đoạn kết nối của container?
Để đảm bảo không gián đoạn dịch vụ, bạn cần thiết lập trường resizePolicy trong container spec với giá trị restartPolicy: NotRequired cho cả hai tài nguyên cpu và memory.
Khi nhận được yêu cầu PATCH tới subresource /resize, Kubelet sẽ gọi trực tiếp hàm gRPC UpdateContainerResources tới containerd. Container runtime sẽ ghi đè hạn mức mới trực tiếp vào các file cgroups v2 (cpu.max và memory.max) của Linux kernel trong dưới 500 miligiây mà không gửi tín hiệu SIGTERM hay SIGKILL tới tiến trình bên trong container.
Điều gì sẽ xảy ra nếu Worker Node không đủ tài nguyên khi thực hiện In-Place Resize?
Nếu Node hiện tại không còn đủ tài nguyên nhàn rỗi (allocatable capacity) để đáp ứng mức requests mới, Kubelet sẽ không khởi động lại Pod mà chuyển trường status.resize của Pod sang trạng thái Deferred.
Ở trạng thái này, Pod vẫn tiếp tục hoạt động bình thường với mức tài nguyên cũ. Kubelet sẽ định kỳ kiểm tra và tự động áp dụng mức tài nguyên mới ngay khi có các Pod khác trên cùng Node giải phóng tài nguyên. Trong trường hợp yêu cầu resize vượt quá tổng dung lượng phần cứng tối đa của Node, trạng thái sẽ chuyển thành Infeasible.
In-Place Pod Resizing mang lại lợi ích gì đặc biệt cho các hệ thống AI / LLM Inference?
Các mô hình AI Inference (như vLLM, Ollama, Triton) yêu cầu thời gian nạp trọng số mô hình (weights) rất lâu từ vài chục giây đến hàng phút (cold start).
Sử dụng In-Place Pod Resizing kết hợp với VPA updateMode: "InPlace" cho phép hệ thống tự động tăng CPU/Memory ngay khi lưu lượng truy cập tăng vọt mà không làm gián đoạn các luồng suy luận đang phản hồi khách hàng (streaming responses), loại bỏ hoàn toàn thời gian cold start và giúp doanh nghiệp tiết kiệm 40%–60% chi phí điện toán đám mây bằng cách hạ tài nguyên về mức tối thiểu khi ở trạng thái rảnh rỗi mà không cần tắt Pod.
Tại sao việc giảm giới hạn Memory (Scale-down) lại tiềm ẩn rủi ro cao hơn so với CPU?
Khi tăng CPU hoặc Memory, Linux kernel chỉ đơn giản là nới rộng hạn mức điều phối. Tuy nhiên, khi giảm giới hạn Memory, nếu mức limit mới thấp hơn lượng bộ nhớ thực tế mà ứng dụng đang chiếm dụng (Resident Set Size - RSS), Linux kernel sẽ lập tức kích hoạt Out-Of-Memory Killer (OOM Killer) để tiêu hủy tiến trình nhằm bảo vệ hệ điều hành.
Do đó, đối với các ứng dụng có bộ nhớ đệm lớn (như PostgreSQL shared_buffers hoặc Java heap), khuyến nghị nên thiết lập restartPolicy: RestartContainer khi thực hiện giảm Memory limit.
🔗 Đọc thêm các chuyên đề & Series liên quan:
- GitOps Hàng Khủng (At Scale): Kubernetes & ArgoCD Phục Vụ Microservices
- AWS EKS vs ECS: Kiến trúc, Chi phí & Thực tế Sử dụng
- Cải tiến Go 1.26 Green Tea GC & Cgo Performance Guide
- Phát Hiện Và Xử Lý Rò Rỉ Goroutine Trong Dịch Vụ Go Đang Chạy Production
