Có một cách nhìn giúp gỡ rối gần như mọi hiểu lầm về Operator: CRD là một danh từ, Operator là một động từ. Khai một CRD giống như in ra một loại biểu mẫu mới cho công ty — có các ô bắt buộc, có ô nào được điền giá trị gì, thậm chí có sẵn ô đánh dấu để dễ tra. Tờ biểu mẫu đẹp, chặt chẽ, và tự nó không làm gì cả. Phải có một nhân viên ngồi đọc từng tờ mới nộp rồi thực sự đi làm việc ghi trên đó thì công ty mới chạy. Operator chính là nhân viên ấy. Bài này tôi tự in tờ biểu mẫu (CRD) rồi tự thuê nhân viên (Operator), và đo xem mỗi phần đáng giá bao nhiêu — cùng cái giá ẩn mà người ta hay quên.

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.

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ỉ.

Mẫu số chung: khai một kiểu dữ liệu thì rẻ và vô hành vi — giá trị nằm ở cái hành động trên nó

Bài này gói một sự thật kiến trúc đúng ở khắp nơi: định nghĩa cấu trúc là một chuyện, tạo ra hành vi là chuyện hoàn toàn khác — và người ta hay tưởng có cái đầu là có cái sau. 30 dòng YAML cho bạn một kiểu dữ liệu được kiểm tra, được lưu, có giao diện đẹp; nó không sinh ra một hành động nào. Hành động cần một tác nhân chủ động chạy liên tục, đối chiếu "cái mong muốn" với "cái đang có" và kéo cái sau về cái trước.

Trong thế giới Java, đây đúng là khác biệt giữa một @annotation và bộ xử lý annotation của nó: @NotNull, @Entity chỉ là metadata trơ, hoàn toàn vô nghĩa cho tới khi một annotation processor hay framework (Bean Validation, Hibernate) đọc và hành động. Khai annotation thì rẻ; viết cái diễn giải nó mới là phần việc thật — y hệt CRD so với Operator. Trong cơ sở dữ liệu, một cột CHECK (so_ban <= 5) cho bạn validation miễn phí do engine thi hành (đúng vai lược đồ OpenAPI ở đây), nhưng muốn hệ phản ứng — tự dựng bản ghi phụ khi có dòng mới — thì phải có trigger hoặc một job đối chiếu chạy thường trực, và nó mang đúng cái giá "poll hay event" của bài này. Và ở mọi nền tảng, phần lớn cơ chế nặng (thu gom rác, kiểm tra kiểu, điều phối) đã có sẵn trong runtime — dựng lại chúng bằng tay là tự chuốc lấy sáu dòng trong bảng thiếu ở trên.

Mẫu số chung: một lược đồ/kiểu/annotation cho bạn ràng buộc và nơi cất dữ liệu gần như miễn phí, nhưng không cho bạn hành vi — hành vi luôn cần một tác nhân chủ động, và cái giá lớn nhất của tác nhân đó là cách nó quan sát thay đổi (quét định kỳ ⇒ tốn tỉ lệ số đối tượng kể cả khi rảnh; theo dõi sự kiện ⇒ gần như không tốn lúc đứng yên). Nên câu hỏi đúng trước khi "mở rộng bằng một kiểu mới" không phải "khai có khó không" — khai luôn dễ — mà là "sau khi khai, ai sẽ hành động trên nó, tác nhân đó quan sát bằng cách nào, và tôi có thật sự cần một tiến trình chạy thường trực hay chỉ cần một bản mẫu dựng-một-lần".

Muốn tự kiểm, 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.