Hãy coi etcd như sổ địa chính của cả một thành phố. Nó chỉ là một xấp giấy mỏng — nhỏ đến mức bạn cầm gọn trong tay — nhưng mất nó thì không ai chứng minh nổi ai sở hữu cái gì, dù các toà nhà vẫn đứng nguyên đó. etcd của Kubernetes đúng như vậy: nó không giữ dữ liệu ứng dụng, nó giữ mô tả về những gì nên tồn tại. Pod vẫn chạy nếu etcd chết, nhưng mọi hiểu biết về việc nên chạy gì thì biến mất.

etcd là nơi duy nhất Kubernetes lưu trạng thái. Phần trước đo được nó chỉ chiếm 55 MB bộ nhớ — nhỏ hơn nhiều so với tiếng tăm của nó. Bài này mở nó ra xem bên trong có gì.

Toàn bộ cụm nằm trong 2,6 MB

VERSION 3.6.5    DB SIZE 2.6 MB    IN USE 2.6 MB    IS LEADER true

Hai phẩy sáu megabyte cho một cụm đang chạy vài deployment. Con số này nhỏ tới mức đáng để dừng lại một chút: etcd không lưu dữ liệu ứng dụng — nó chỉ lưu mô tả những gì cần chạy.

Xem phân bố khoá trong /registry:

Loại Số khoá
events 173
clusterroles 72
clusterrolebindings 58
serviceaccounts 45
apiregistration.k8s.io 21
pods 15
configmaps 13

Event nhiều gấp mười một lần pod.

Phần lớn nội dung etcd không phải ứng dụng của bạn — là RBAC (clusterroles + clusterrolebindings + serviceaccounts = 175 khoá) và event. Trên một cụm rỗng, phần thuộc về bạn gần như không đáng kể.

Event tự hết hạn sau một giờ (--event-ttl mặc định), nên chúng không tích luỹ vô hạn. Nhưng trên cụm bận — nhiều pod khởi động lại, nhiều lần triển khai — chúng là nguồn tăng DB SIZE thường gặp nhất, và cũng là thứ đầu tiên nên nhìn khi etcd phình ra.

Trần kích thước một object: 1.048.576 byte

Tạo ConfigMap với kích thước tăng dần:

100 kB    ->  created
500 kB    ->  created
1000 kB   ->  created
1500 kB   ->  Too long: may not be more than 1048576 bytes
2000 kB   ->  Too long: may not be more than 1048576 bytes

Đúng 1 MiB, và nó áp cho mọi loại object — ConfigMap, Secret, CRD, cả pod spec.

Đây là giới hạn hay bị đâm vào theo những cách bất ngờ:

  • Nhét chứng chỉ hoặc bộ dữ liệu vào ConfigMap.
  • CRD với status tích luỹ mãi — một số toán tử ghi lịch sử vào đó.
  • Pod có annotation khổng lồ, ví dụ kubectl.kubernetes.io/last-applied-configuration của một manifest rất dài.

Cách xử lý: dữ liệu lớn không thuộc về etcd. Dùng PersistentVolume, object storage, hoặc initContainer tải về lúc khởi động.

Ngoài trần từng object, etcd còn có trần toàn bộ cơ sở dữ liệu — --quota-backend-bytes, mặc định 2 GB (bảng trên hiện QUOTA 0 B nghĩa là chưa đặt riêng). Vượt là etcd chuyển sang chế độ chỉ đọc, và cả cụm mất khả năng ghi. Đó là một trong những sự cố Kubernetes khó chịu nhất, vì kubectl get vẫn chạy trong khi kubectl apply thì không — nửa sống nửa chết, khó chẩn đoán.

Độ trễ ghi

kubectl create configmap        29 ms
trừ đi kubectl khởi động        24 ms
------------------------------------
đường apiserver -> etcd           5 ms

Ghi vào etcd rẻ: 5 mili giây cho cả chuỗi xác thực, phân quyền, kiểm tra hợp lệ, ghi và đồng bộ.

Cái đắt là chính binary kubectl khởi động. 24 trong 29 mili giây — 83% thời gian của một lệnh — chưa chạm tới cụm.

Đây là phiên bản nhẹ của cái bẫy ở sê-ri Kafka, nơi công cụ dòng lệnh JVM tốn 0,86 giây. Với kubectl thì chỉ 24 ms, nhưng nguyên tắc giống hệt: đừng đo hiệu năng cụm bằng cách gọi lặp lại công cụ dòng lệnh.

Hệ quả thực dụng: một kịch bản chạy kubectl một nghìn lần tốn 24 giây chỉ để khởi động tiến trình. Gộp thành một lời gọi (kubectl apply -f cho cả thư mục) hoặc dùng thư viện client là khác biệt lớn.

Vì sao etcd là thành phần cần cẩn thận nhất

Nó nhỏ, nó nhanh, và mất nó là mất cả cụm. Không có bản sao lưu nào khác — pod đang chạy vẫn chạy (phần trước đã đo), nhưng mọi mô tả về việc nên chạy gì đã biến mất.

Ba việc bắt buộc trên cụm thật:

Sao lưu định kỳ.

etcdctl --endpoints=127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /backup/etcd-$(date +%F).db

Với DB SIZE 2,6 MB thì bản chụp gần như miễn phí. Không có lý do gì để không chạy nó mỗi giờ.

Số node lẻ. etcd dùng Raft, cần đa số. Ba node chịu được một chết; bốn node cũng chỉ chịu được một — thêm node chẵn không thêm khả năng chịu lỗi, chỉ thêm chi phí đồng thuận. Năm node chịu được hai.

Đĩa nhanh. etcd gọi fsync cho mọi lần ghi, nên độ trễ đĩa vào thẳng độ trễ mọi lời gọi API. Đây là chỗ ổ thể rắn không phải tuỳ chọn.

Theo dõi ba chỉ số

etcd_disk_wal_fsync_duration_seconds      p99 nên dưới 25 ms
etcd_disk_backend_commit_duration_seconds p99 nên dưới 25 ms
etcd_server_leader_changes_seen_total     nên gần như không đổi

Chỉ số thứ ba là chỉ báo sớm tốt nhất: etcd đổi leader liên tục nghĩa là mạng giữa các node đang có vấn đề, và mọi lời gọi API sẽ chậm dần trước khi có gì hỏng hẳn.

Cùng một bài toán "lưu trữ sự thật chung", đổi ngăn xếp

Hai điều bất ngờ nhất ở etcd — "bốn node không hơn gì ba" và "mỗi lần ghi phải fsync" — không phải đặc sản của Kubernetes. Chúng là hai định luật của bất kỳ kho lưu sự thật chung nào.

Trong thế giới cơ sở dữ liệu, chuyện số lẻ chính là công thức quorum: một hệ sao chép N bản với điều kiện ghi W và đọc R thoả W + R > N mới đảm bảo đọc thấy bản mới nhất — và muốn chịu được f node chết thì cần 2f + 1 node, tức luôn là số lẻ. Thêm một node chẵn chỉ nâng ngưỡng đa số mà không nâng số lỗi chịu được. Zookeeper cũng khuyến nghị ensemble lẻ vì đúng lý do đó. Và fsync thì là câu chuyện WAL kinh điển: PostgreSQL phải fsync WAL trước khi báo commit thành công, nên độ trễ commit không bao giờ thấp hơn độ trễ một vòng ghi đĩa — đó là vì sao có commit_delay và group commit để nhiều giao dịch chia nhau một lần fsync.

Trong thế giới Java, đúng chi phí đó hiện ra mỗi khi bạn gọi FileChannel.force(true) hay FileDescriptor.sync() để ép dữ liệu xuống đĩa vật lý — đây là lý do log append-only (Kafka broker, một WAL tự viết) luôn phải cân giữa "gọi fsync mỗi bản ghi" (bền nhất, chậm nhất) và "gom nhiều bản ghi rồi fsync một lần" (nhanh hơn, đánh đổi một cửa sổ mất mát).

Mẫu số chung: một kho lưu sự thật chung phải trả hai cái giá đồng bộ không né được — độ bền đòi một vòng ghi đĩa thật (fsync) trước khi dám xác nhận, và tính sẵn sàng đòi đa số tuyệt đối (nên số bản sao luôn là số lẻ, thêm node chẵn chỉ tốn thêm mà không chịu lỗi tốt hơn) — và vì mọi thứ phía trên phụ thuộc vào nó, hai cái giá này trở thành sàn hiệu năng của cả hệ. Nên câu hỏi đúng khi vận hành một kho kiểu etcd không phải "nó có nhanh không" mà là "đĩa của nó fsync nhanh cỡ nào, và tôi đã đặt đúng số node lẻ chưa" — vì hai con số đó quyết định trần của mọi thứ xây bên trên.

Muốn tự xem etcd của bạn đang chứa gì, chạy thử:

kubectl -n kube-system exec etcd-<node> -- etcdctl \
  --endpoints=127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry --prefix --keys-only \
  | sed 's|^/registry/||;s|/.*||' | grep . | sort | uniq -c | sort -rn | head

Nếu events không đứng đầu, cụm của bạn khác thường. Nếu một loại object nào đó bất ngờ đứng đầu — thường là CRD của một toán tử — đó là chỗ đáng xem trước khi DB SIZE thành vấn đề.

Phần sau đo API server: 83% thời gian một lệnh là chi phí client, vậy 17% còn lại đi đâu.