Chi phí ẩn của các Cluster nhàn rỗi
Vài năm trước, tôi thường dành các chiều thứ Sáu để kiểm tra các Kubernetes cluster của mình, và lần nào tôi cũng cảm thấy xót xa khi nhìn vào các chỉ số. Chúng tôi chạy hơn 40 microservices, nhưng hầu hết trong số đó—như webhook thông báo Slack hay trình tạo báo cáo hàng tuần—hoàn toàn không làm gì trong 99% thời gian. Về cơ bản, chúng tôi đang trả 600 USD mỗi tháng chỉ để duy trì các container nhàn rỗi trong khi chúng chiếm dụng bộ nhớ và CPU dự phòng 24/7.
“Hội chứng pod nhàn rỗi” này là một kẻ sát nhân thầm lặng đối với ngân sách. Trong một thiết lập Kubernetes tiêu chuẩn, bạn thường duy trì ít nhất một bản sao (replica) cho mỗi dịch vụ để đảm bảo nó có thể phản hồi yêu cầu ngay lập tức. Nếu bạn có 50 dịch vụ nhỏ, điều đó có nghĩa là 50 pod đang chiếm dụng không gian ngay cả lúc 3 giờ sáng khi không có lưu lượng truy cập. Các đội ngũ DevOps cuối cùng sẽ rơi vào tình cảnh phải trả tiền cho năng lực dự phòng thay vì hiệu suất thực tế.
Tại sao Autoscaling truyền thống của Kubernetes là chưa đủ
Bạn có thể cân nhắc sử dụng Horizontal Pod Autoscaler (HPA) tiêu chuẩn, nhưng đó là một cuộc chiến khó khăn đối với các luồng công việc (workflow) serverless. HPA điều chỉnh quy mô dựa trên các chỉ số như mức sử dụng CPU hoặc bộ nhớ. Ngay cả khi một pod đang nhàn rỗi, nó vẫn yêu cầu một mức bộ nhớ tối thiểu—thường là 64MB hoặc 128MB—chỉ để duy trì trạng thái hoạt động. Quan trọng hơn, HPA nguyên bản không thể giảm xuống còn 0 replica vì nó không có cách nào để “đánh thức” một dịch vụ khi có yêu cầu mới gửi đến một endpoint trống.
Hạn chế này nằm ở cách Kubernetes xử lý mạng. Các Service tiêu chuẩn là các bộ cân bằng tải nội bộ trỏ đến IP của các pod hiện có. Nếu không có pod nào tồn tại, kết nối sẽ thất bại với lỗi 503. Kubernetes thiếu một “phòng chờ” bản địa để đệm các yêu cầu trong khi chờ khởi tạo container. Đây chính là khoảng trống mà Knative lấp đầy.
So sánh các lựa chọn Serverless
Khi mới bắt đầu giải quyết vấn đề này, tôi đã đánh giá ba hướng đi chính:
- Cloud Functions (AWS Lambda, GCF): Những dịch vụ này scale về 0 rất tuyệt vời, nhưng rủi ro bị phụ phụ thuộc vào nhà cung cấp (vendor lock-in) là rất lớn. Việc chuyển 100 function từ AWS sang Azure không phải là một việc làm trong một sớm một chiều; đó là một cơn ác mộng di trú kéo dài nhiều tháng.
- KEDA với HPA: Bạn có thể thiết lập
minReplicas: 0bằng KEDA, nhưng việc quản lý các chỉ số tùy chỉnh và trigger sự kiện tạo ra gánh nặng cấu hình đáng kể. Cảm giác giống như bạn đang chống lại nền tảng hơn là sử dụng nó. - Knative: Nó thêm một lớp trừu tượng hướng yêu cầu (request-driven) lên trên cluster hiện có của bạn. Nó tự động xử lý logic “Scale-to-Zero” bằng cách chặn bắt lưu lượng truy cập, khiến nó trở thành lựa chọn cân bằng nhất cho các đội ngũ ưu tiên container (container-native).
Giải pháp Knative: Hiệu suất không lãng phí
Tôi nhận thấy rằng Knative là cách đáng tin cậy nhất để đạt được trải nghiệm serverless mà không làm mất đi tính linh hoạt của Docker. Sau khi triển khai giải pháp này trong môi trường dev, chúng tôi đã giảm gần 40% chi phí vận hành cluster. Nó cho phép bạn đóng gói bất kỳ ứng dụng nào dưới dạng image tiêu chuẩn trong khi vẫn chạy nó với hiệu quả của một function. Hãy cùng bắt đầu.
Bạn sẽ cần một Kubernetes cluster đang hoạt động và quyền truy cập kubectl để thực hiện theo hướng dẫn.
Bước 1: Cài đặt Knative Serving CRDs
Đầu tiên, chúng ta áp dụng các Custom Resource Definitions (CRDs) cho phép Knative quản lý các kiểu service đặc thù của nó.
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-crds.yaml
Bước 2: Triển khai các thành phần Serving cốt lõi
Tiếp theo, chúng ta cài đặt các controller xử lý logic serverless thực tế và quản lý vòng đời của pod.
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-core.yaml
Bước 3: Đơn giản hóa mạng với Kourier
Knative cần một gateway để điều hướng lưu lượng. Mặc dù Istio là tiêu chuẩn công nghiệp, nhưng nó thường quá nặng đối với các dự án nhỏ. Kourier là một lựa chọn thay thế nhẹ nhàng giúp hoàn thành công việc mà không cần thêm các sidecar dư thừa.
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.12.0/kourier.yaml
# Cấu hình Kourier làm ingress mặc định
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
Bước 4: Cấu hình DNS tự động
Để thử nghiệm, chúng ta sử dụng sslip.io để xử lý DNS tự động. Điều này giúp chúng ta không phải chỉnh sửa file /etc/hosts mỗi khi triển khai một dịch vụ mới.
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-default-domain.yaml
Triển khai một dịch vụ có khả năng Scale to Zero
Bây giờ là lúc tận hưởng thành quả. Chúng ta sẽ triển khai một ứng dụng web bằng đối tượng Service của Knative. Lưu ý rằng đây không phải là Service K8s tiêu chuẩn; nó kết hợp deployment, routing và scaling vào một tài nguyên duy nhất.
Tạo file hello-knative.yaml:
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: hello-world
namespace: default
spec:
template:
spec:
containers:
- image: gcr.io/knative-samples/helloworld-go
env:
- name: TARGET
value: "Chuyên gia Knative"
resources:
limits:
cpu: "200m"
memory: "128Mi"
Triển khai với một câu lệnh duy nhất:
kubectl apply -f hello-knative.yaml
Chứng kiến “phép thuật” Scale-to-Zero
Sau khi quá trình triển khai hoàn tất, hãy theo dõi cluster của bạn:
kubectl get pods -w
Một pod sẽ khởi chạy ngay lập tức để xử lý thiết lập ban đầu. Bây giờ, hãy đợi khoảng 60 đến 90 giây. Bạn sẽ thấy pod chuyển sang trạng thái Terminating và biến mất. Dịch vụ của bạn hiện đã được scale về 0, tiêu tốn đúng 0 CPU và RAM trên các worker node.
Để đánh thức nó, hãy lấy URL dịch vụ của bạn:
kubectl get ksvc hello-world
Gửi một yêu cầu curl đến URL đó. Bạn sẽ nhận thấy một khoảng trễ ngắn—thường từ 1,5 đến 2,5 giây. Đây chính là “cold start” (khởi động lạnh). Trong khoảng thời gian này, Activator của Knative sẽ đệm yêu cầu đang gửi đến và ra tín hiệu cho Autoscaler khởi động một pod mới. Ngay khi container ở trạng thái healthy, yêu cầu sẽ được giải phóng và xử lý.
Kinh nghiệm thực chiến khi chạy Production
Chạy Knative trên production đã dạy tôi rằng scale-to-zero không phải lúc nào cũng là câu trả lời đúng cho mọi loại workload. Đây là cách tôi tinh chỉnh nó:
- Scaling dựa trên yêu cầu: Thay vì scale dựa trên CPU, hãy sử dụng giới hạn đồng thời (concurrency limits). Việc thêm
autoscaling.knative.dev/target: "10"đảm bảo Knative sẽ thêm một pod ngay khi bạn đạt đến 11 yêu cầu đồng thời. Cách này phản hồi nhanh hơn nhiều đối với lưu lượng web. - Loại bỏ Cold Starts: Đối với các API ưu tiên cao, nơi mà độ trễ 2 giây là không thể chấp nhận được, hãy thiết lập
autoscaling.knative.dev/min-scale: "1". Bạn vẫn nhận được mô hình triển khai Knative đơn giản hóa, nhưng dịch vụ luôn được giữ ấm (warm). - Giới hạn tài nguyên chặt chẽ: Serverless không có nghĩa là tài nguyên vô hạn. Luôn xác định các giới hạn (limit) của bạn để ngăn một function bị lỗi ngốn sạch ngân sách của toàn bộ cluster.
Lời kết
Chuyển sang Knative là một trong những cách hiệu quả nhất mà tôi đã thực hiện để tối ưu hóa hạ tầng của mình. Nó loại bỏ gánh nặng tâm trí khi phải quản lý các ngưỡng HPA và cho phép các nhà phát triển tập trung vào việc phát hành code. Bằng cách triển khai scale-to-zero, bạn biến cluster của mình thành một cỗ máy năng động chỉ hoạt động khi thực sự có việc cần làm. Nó hiệu quả, linh hoạt và thực sự hoạt động tốt.

