Khi triển khai máy chủ trò chơi chuyên dụng (dedicated game servers) trên Kubernetes, việc mở rộng quy mô là chỉ một nửa vấn đề — phần còn lại, và thường khó hơn, là thu nhỏ cụm một cách an toàn khi tải giảm. Khác với các ứng dụng web thông thường, mỗi phiên trò chơi duy trì trạng thái bộ nhớ thời gian thực và kết nối TCP bền vững từ nhiều client. Việc loại bỏ một nút đang chạy phiên hoạt động sẽ gây gián đoạn nghiêm trọng, dẫn đến mất tiến trình và trải nghiệm người chơi bị phá vỡ.
Do đó, cơ chế tự động thu nhỏ (scale-down) mặc định của các nền tảng như Google Kubernetes Engine (GKE) không phù hợp: Cluster Autoscaler giả định rằng mọi Pod đều có thể di chuyển hoặc khởi tạo lại ngay lập tức — điều hoàn toàn mâu thuẫn với yêu cầu tính liên tục của máy chủ trò chơi.
Giải pháp khả thi đòi hỏi ba thành phần phối hợp:
- Tập trung hóa việc lập lịch: Đảm bảo các máy chủ trò chơi được nhóm trên ít nút nhất có thể để tránh phân mảnh tài nguyên.
- Cấm lập lịch mới trên nút mục tiêu (cordon): Gắn cờ nút để ngăn Kubernetes đặt thêm Pod mới, trong khi vẫn duy trì các phiên hiện hữu.
- Xóa nút đã rảnh hoàn toàn: Chỉ loại bỏ nút khi không còn bất kỳ máy chủ trò chơi nào đang chạy và sau khoảng thời gian chờ cấu hình.
Tập trung máy chủ trò chơi bằng PodAffinity
Thay vì phân tán các Pod máy chủ trò chơi ngẫu nhiên khắp cụm, ta sử dụng podAffinity với chính sách preferredDuringSchedulingIgnoredDuringExecution để khuyến khích Kubernetes đặt các máy chủ cùng loại vào những nút đã có sẵn máy chủ tương tự.
Dưới đây là mẫu PodSpec được cập nhật, đánh dấu các máy chủ bằng nhãn sessions: game và ưu tiên lập lịch trên cùng một nút (dựa vào kubernetes.io/hostname):
apiVersion: v1
kind: Pod
metadata:
generateName: "match-"
labels:
sessions: game
spec:
hostNetwork: true
restartPolicy: Never
nodeSelector:
type: gaming-node
containers:
- name: arena-server
image: registry.example.com/arena:2.4.1
env:
- name: MATCH_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
limits:
cpu: "125m"
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
podAffinityTerm:
labelSelector:
matchLabels:
sessions: game
topologyKey: kubernetes.io/hostname
Cấm nút dựa trên mức độ sử dụng CPU
Hàm điều khiển cordonNodes xác định số nút cần cấm bằng cách so sánh lượng CPU còn dư thừa với ngưỡng đệm cấu hình (bufferCount). Thay vì chọn ngẫu nhiên, thuật toán sắp xếp các nút theo số lượng máy chủ trò chơi đang chạy tăng dần — ưu tiên cấm những nút ít tải nhất để tối đa hóa khả năng giải phóng sớm.
Đoạn mã sau minh họa logic chọn lọc và gắn cờ:
func (c *Controller) cordonNodes(availableCPU int64) error {
nodes := c.listNodes()
sort.Slice(nodes, func(i, j int) bool {
return len(c.listGamePodsOnNode(nodes[i].Name)) < len(c.listGamePodsOnNode(nodes[j].Name))
})
targetCordon := int((availableCPU - c.bufferCount) / c.cpuPerServer)
if targetCordon <= 0 {
return nil
}
for i := 0; i < targetCordon && i < len(nodes); i++ {
if err := c.api.CoreV1().Nodes().Patch(
context.TODO(),
nodes[i].Name,
types.StrategicMergePatchType,
[]byte(`{"spec":{"unschedulable":true}}`),
metav1.PatchOptions{},
); err != nil {
return fmt.Errorf("failed to cordon %s: %w", nodes[i].Name, err)
}
}
return nil
}
Xóa nút đã được cấm và rảnh hoàn toàn
Hàm deleteCordonedNodes kiểm tra hai điều kiện trước khi xóa nút:
- Nút đã bị cấm và vượt quá thời gian chờ cấu hình (
shutdownDelay). - Không còn Pod nào thuộc nhóm
sessions: gameđang chạy trên nút đó.
Bên cạnh đó, hệ thống luôn đảm bảo tổng số nút không giảm dưới ngưỡng tối thiểu (minNodeNumber) để duy trì khả năng phục vụ cơ bản:
func (c *Controller) deleteCordonedNodes() error {
nodes := c.listCordonedNodes()
now := time.Now()
for _, node := range nodes {
if c.isNodeIdle(node.Name) {
cordonTime := c.getCordonTimestamp(node)
if now.After(cordonTime.Add(c.shutdownDelay)) {
if c.nodeCount() > c.minNodeNumber {
if err := c.api.CoreV1().Nodes().Delete(context.TODO(), node.Name, metav1.DeleteOptions{}); err != nil {
return err
}
}
}
}
}
return nil
}