CRD mở rộng API của Kubernetes bằng kiểu tài nguyên của riêng bạn. Operator là bộ điều khiển làm cho kiểu đó có ý nghĩa. Bài này tự viết cả hai rồi đo xem chúng đáng giá bao nhiêu — và tốn bao nhiêu.
30 dòng YAML mua được gì
Tôi khai một TrangWeb với anh và soBan. Đổi lại, không viết dòng mã nào:
Kiểm tra dữ liệu đầu vào
soBan: 99 -> spec.soBan in body should be less than or equal to 5
thiếu anh -> spec.anh: Required value
Lược đồ OpenAPI được API server thi hành trước khi ghi vào etcd. Không có cách nào lách qua — kể cả kubectl apply --force.
Giá trị mặc định
không khai soBan -> soBan = 1
Cột hiển thị riêng
NAME ANH BAN SAN SANG
blog nginx:1.27-alpine 2 2
Kèm tên viết tắt tw, subresource status, và kubectl get/describe/edit/patch hoạt động đầy đủ như với tài nguyên gốc.
Đó là một món hời. Nhưng nó không làm gì cả: sau khi tạo TrangWeb, namespace vẫn có 0 Deployment và 0 pod.
CRD chỉ là một chỗ để cất dữ liệu có kiểm tra. Nó là một cái bảng có ràng buộc, không phải một hệ thống.
38 dòng Python biến nó thành Operator
Vòng lặp đối chiếu, viết bằng thư viện chuẩn:
while True:
for o in liet_ke("trangwebs"):
tao_hoac_sua_deployment(o)
ghi_status(o, san_sang=doc_deployment(o))
time.sleep(5)
Kết quả đo:
soBan 2 -> 4 Deployment thành 4/4 sau ~5 giây
xoá TrangWeb Deployment tự biến mất
Chỗ thứ hai không phải mã tôi viết. ownerReferences khiến bộ thu gom rác của Kubernetes xoá Deployment khi chủ của nó biến mất — ba dòng khai báo, không có logic xoá nào.
Đây là điều đáng hiểu nhất về Operator: phần lớn cơ chế đã có sẵn. Bạn không tự làm đối chiếu trạng thái, không tự làm thu gom rác, không tự làm kiểm tra dữ liệu. Bạn chỉ viết phần "từ mong muốn ra hiện thực".
Chi phí thật nằm ở phần 38 dòng đó không làm
Độ trễ phản ứng bằng chu kỳ quét. Đo ba lần: 4,3 / 5,1 / 5,0 giây. Đó là chu kỳ ngủ của tôi, không phải tốc độ của Kubernetes. Dùng watch thì phản ứng dưới một giây.
Tải lên API server chạy liên tục. Với 20 đối tượng, trong 60 giây:
220 lượt xử lý đối tượng
660 lệnh gọi API
11 lệnh/giây — kể cả khi không có gì thay đổi
Đây mới là chỗ đáng lo. Với 200 đối tượng, cùng mã đó gọi 110 lệnh mỗi giây và không bao giờ nghỉ. Một watch gần như không tốn gì khi hệ thống đứng yên.
Rút ngắn chu kỳ ngủ để phản ứng nhanh hơn thì tải tăng tuyến tính. Hai thứ đó kéo ngược nhau, và watch là cách duy nhất thoát khỏi đánh đổi đó.
Và danh sách những thứ chưa có:
| Thiếu | Hậu quả khi vào production |
|---|---|
watch thay cho quét |
Chậm và tốn, như đo ở trên |
| Hàng đợi + giải lui khi lỗi | Một đối tượng hỏng làm cả vòng lặp quay cuồng |
| Bầu chọn thủ lĩnh | Hai bản sao ghi đè lẫn nhau |
Xử lý xung đột resourceVersion |
Ghi đè mất thay đổi của người khác |
| Chỉ số | Không biết nó có đang chạy không |
| Webhook kiểm tra | Không kiểm được thứ lược đồ không diễn đạt nổi |
Đó là lý do controller-runtime và Kubebuilder tồn tại. Chúng không giúp bạn viết vòng lặp đối chiếu — chúng cho sẵn sáu dòng trong bảng trên.
Khi nào nên viết Operator
Câu hỏi đúng không phải "có làm được không" mà "có đáng không".
Đáng, khi có kiến thức vận hành cần mã hoá: nâng cấp một cụm cơ sở dữ liệu theo đúng thứ tự, chuyển vai trò chính, khôi phục từ bản sao lưu, xoay vòng chứng chỉ. Đó là những việc phải làm đúng lúc 3 giờ sáng và không ai nhớ hết các bước.
Không đáng, khi chỉ để tránh gõ YAML. Một TrangWeb bọc quanh Deployment như trong bài này là lỗ ròng: nó thêm một thứ phải bảo trì, một chỗ nữa để hỏng, và che mất Deployment thật khi gỡ rối. Chart Helm hay overlay Kustomize giải quyết đúng nhu cầu đó mà không cần tiến trình nào chạy thường trực.
Ranh giới thực dụng: Operator đáng viết khi nó phải phản ứng với những gì xảy ra sau khi triển khai. Chỉ dựng ra một lần rồi thôi thì đó là việc của bản mẫu.
Ba điều nên biết trước khi bắt đầu
CRD là phạm vi toàn cụm. Cài một CRD là mọi namespace thấy nó. Hai phiên bản của cùng một Operator trong hai namespace vẫn dùng chung một định nghĩa — đây là nguồn xung đột thường gặp khi hai đội cùng cài một Operator.
Xoá CRD là xoá mọi đối tượng của nó. Không cảnh báo, không hỏi lại, và không đảo ngược được. Đó cũng là lý do Helm cố ý không xoá CRD lúc uninstall, như đã đo ở phần trước.
status phải là subresource. Khai subresources: {status: {}} thì spec và status ghi qua hai đường khác nhau — người dùng không vô tình sửa status, và controller không vô tình sửa spec. Thiếu nó thì vòng lặp đối chiếu tự kích hoạt chính nó mỗi lần ghi status, và nó quay không nghỉ.
Thử ba mươi giây
Xem cụm của bạn đã có bao nhiêu CRD và ai cài chúng:
kubectl get crd -o json | python3 -c '
import sys, json
from collections import Counter
c = Counter()
for x in json.load(sys.stdin)["items"]:
g = x["spec"]["group"]
who = x["metadata"].get("labels", {}).get("app.kubernetes.io/managed-by", "?")
c[(g, who)] += 1
for (g, who), n in sorted(c.items(), key=lambda kv: -kv[1]):
print(f"{n:4d} {g:45} cai boi: {who}")
'
Con số này thường lớn hơn người ta nghĩ — mỗi ingress controller, mỗi service mesh, mỗi bộ giám sát đều mang theo vài chục CRD. Nhóm nào có cai boi: ? là nhóm không thuộc release nào, tức là ai đó kubectl apply bằng tay và không ai còn nhớ.
Phần sau: Argo CD và GitOps — đo thời gian từ lúc đẩy commit tới lúc cụm thật sự đổi.