Có một phân biệt mà nếu nắm được thì cả bài này sáng ra: cài đặt một bản mới khác hẳn kích hoạt nó. Giống khi bạn cập nhật hệ điều hành điện thoại — bản mới đã chạy, nhưng vài tính năng mới vẫn tắt cho tới khi bạn gạt một công tắc riêng, và một khi đã dùng chúng thì dữ liệu mới ghi ra bản cũ không đọc nổi. Nâng cấp Kafka đúng y như vậy, và chỗ hay bị bỏ quên nằm đúng ở cái công tắc thứ hai đó.
Nâng cấp Kafka không cần dừng dịch vụ, và quy trình ngắn hơn nhiều người nghĩ. Bài này chạy nó thật trên một cụm đang có producer và consumer hoạt động — rồi phát hiện việc chưa xong.
Nâng cấp cuốn chiếu
Cụm 3 broker chạy Kafka 3.8.1, topic 6 partition với replication-factor=3 và min.insync.replicas=2. Producer gửi đều 5.000 tin/giây với acks=all, consumer đọc liên tục.
Thay từng broker một, mỗi lần đợi ISR đầy đủ trở lại trước khi sang broker kế:
| ISR đầy đủ lại sau | Partition thiếu bản sao | |
|---|---|---|
| thay u3 | 1 giây | 0 |
| thay u2 | 2 giây | 0 |
| thay u1 | 22 giây | 0 |
Không mất tin nào, và không lúc nào có partition thiếu bản sao.
Con số 22 giây của u1 lớn hơn hẳn vì nó đang là controller: nó phải nhường vai trước khi tắt, và node mới phải tham gia lại quorum. Đây là lý do quy tắc "nâng cấp controller sau cùng" tồn tại — làm ngược lại thì bạn trả chi phí bầu lại controller ở mỗi bước.
Với cụm tách vai (phần 21), thứ tự là: broker trước, controller sau.
Nhưng cụm chưa thật sự lên 3.9
Sau khi cả ba broker đã chạy nhị phân 3.9.0:
Feature: metadata.version
SupportedMaxVersion: 3.9-IV0 <- nhị phân đã 3.9
FinalizedVersionLevel: 3.8-IV0 <- giao thức vẫn 3.8
Nhị phân đã mới, giao thức siêu dữ liệu vẫn cũ. Cụm chạy các tính năng của 3.8.
Phải nâng riêng một bước nữa:
kafka-features.sh --bootstrap-server u1:9092 upgrade --metadata 3.9
# metadata.version was upgraded to 21.
Đây là thiết kế có chủ ý, không phải thiếu sót: chừng nào chưa nâng metadata.version, bạn còn lùi được về 3.8.1 bằng cách đổi lại ảnh container. Nâng rồi thì các tính năng mới bắt đầu ghi vào log siêu dữ liệu, và bản cũ không đọc được nữa.
Nghĩa là quy trình đúng có hai giai đoạn tách rời:
- Nâng nhị phân cuốn chiếu. Lùi được. Chạy như vậy vài ngày.
- Nâng
metadata.version. Từ đây trở đi khó lùi.
Bỏ qua bước 2 thì cụm chạy mãi ở chế độ tương thích ngược và bạn không có tính năng nào của bản mới — một tình trạng dễ kéo dài hàng năm mà không ai để ý, vì mọi thứ vẫn chạy.
Trong phép đo này, hạ metadata.version từ 3.9 về 3.8 thành công. Đừng suy rộng: nhiều bước nhảy phiên bản không hạ được, và ghi chú phát hành của từng bản là nơi duy nhất nói chắc chắn.
Sau nâng cấp, cụm chạy lệch nặng
p0 Leader: 2 p1 Leader: 2 p2 Leader: 2
p3 Leader: 2 p4 Leader: 2 p5 Leader: 2
Cả sáu partition đều do một broker phục vụ. u1 và u3 nằm trong ISR — chúng sao chép đủ, tốn đủ đĩa và băng thông — nhưng không phục vụ một yêu cầu nào.
Đây là chính hiện tượng phần 13 đã đo, và một lần nâng cấp cuốn chiếu ba broker làm nó tệ nhất có thể: mỗi lần một broker tắt, leader của nó chuyển sang máy khác, và nó không bao giờ tự lấy lại.
kafka-leader-election.sh --bootstrap-server u1:9092 \
--election-type preferred --all-topic-partitions
auto.leader.rebalance.enable bật mặc định sẽ tự làm việc này, nhưng theo chu kỳ 300 giây. Sau một lần nâng cấp, đó là năm phút cụm chạy trên một phần ba năng lực — và nếu bạn nâng cấp vào giờ cao điểm, năm phút đó rất dài.
Chạy tay lệnh trên là bước cuối cùng của mọi lần nâng cấp.
Quy trình đầy đủ
Trước:
# ghi lại trạng thái để so sánh sau
kafka-topics.sh --bootstrap-server u1:9092 --describe --under-replicated-partitions
kafka-topics.sh --bootstrap-server u1:9092 --describe --unavailable-partitions
kafka-features.sh --bootstrap-server u1:9092 describe
Hai lệnh đầu phải im lặng. Nâng cấp một cụm đang có partition thiếu bản sao là cộng thêm một sự cố lên một sự cố.
Mỗi broker:
- Dừng đàng hoàng (
docker stop, khôngkill -9). Phần 20 đo được 1,2 giây với 974 MB dữ liệu. - Đổi ảnh, khởi động lại.
- Đợi ISR đầy đủ trước khi sang broker kế. Đây là bước không được rút ngắn: nâng cấp hai broker cùng lúc với
min.insync.replicas=2là dừng ghi.
until [ "$(kafka-topics.sh --bootstrap-server u1:9092 --describe \
--under-replicated-partitions | wc -l)" = 0 ]; do sleep 2; done
Sau:
kafka-leader-election.sh --bootstrap-server u1:9092 --election-type preferred --all-topic-partitions
# chạy vài ngày, rồi:
kafka-features.sh --bootstrap-server u1:9092 upgrade --metadata 3.9
Bốn thứ dễ quên
Client không cần nâng cùng lúc. Giao thức Kafka tương thích hai chiều rất tốt: client 3.5 nói chuyện được với broker 3.9 và ngược lại. Nâng broker trước, client sau, theo lịch riêng.
Nhảy nhiều phiên bản có giới hạn. Kafka hỗ trợ nâng trực tiếp trong một khoảng phiên bản nhất định. Từ 2.x lên 3.9 thường phải qua một bản trung gian, và bỏ qua bước đó cho lỗi ở lần khởi động.
Cấu hình bị bỏ đi giữa các bản. Nâng lên bản đã gỡ một tham số thì broker có thể từ chối khởi động vì cấu hình không nhận ra. Đọc phần "Notable changes" của ghi chú phát hành trước khi bắt đầu, không phải trong lúc broker đang không lên.
Ứng dụng Kafka Streams có bước riêng. Đổi processing.guarantee hay nâng phiên bản Streams có thể cần upgrade.from trong hai lần triển khai liên tiếp. Đây là nơi hay có sự cố nhất sau nâng cấp broker.
Cùng một lằn ranh "cài đặt vs kích hoạt", đổi ngăn xếp
Chuyện "nhị phân đã mới mà giao thức vẫn cũ, và bước kích hoạt là một chiều" không phải đặc sản Kafka. Nó là hình dạng đúng của mọi lần nâng cấp một hệ có trạng thái, và ở đâu cũng có cùng một lời khuyên: tách bước đảo ngược được khỏi bước không quay lại.
Trong thế giới cơ sở dữ liệu, đây chính là mẫu expand/contract (parallel-change): trước hết triển khai code đọc được cả lược đồ cũ lẫn mới và backfill dữ liệu (bước mở rộng, đảo ngược được), chạy yên vài ngày, rồi mới DROP cột cũ (bước thu hẹp, một chiều). pg_upgrade cũng vậy — bạn chạy nhị phân mới trên catalog cũ được, nhưng một khi nâng phiên bản catalog thì không có đường hạ.
Trong thế giới Java, đó là tiến hoá lược đồ serialize: với serialVersionUID tương thích, hay thêm field trong Avro/Protobuf, bạn deploy class mới mà vẫn đọc được dữ liệu cũ — nhưng khoảnh khắc bạn ghi theo định dạng mới và dựa vào field mới, reader bản cũ gãy. Cài đặt class mới là một chuyện; thật sự ghi định dạng mới là cái công tắc một chiều.
Mẫu số chung: nâng cấp một hệ có trạng thái luôn gồm hai bước khác nhau về bản chất — cài đặt bản mới (đảo ngược được, vì bản cũ vẫn đọc được dữ liệu hiện có) và kích hoạt định dạng hay giao thức mới (một chiều, vì từ đó dữ liệu được ghi theo cách bản cũ không đọc nổi) — nên quy trình an toàn là giữ cửa sổ lùi mở càng lâu càng tốt bằng cách hoãn bước kích hoạt, và "đã cài" không bao giờ đồng nghĩa với "đã bật". Nên câu hỏi đúng trước mỗi lần nâng cấp không phải "bản mới chạy chưa" mà là "bước nào trong lần này là điểm không quay lại, tôi đã tách nó ra khỏi bước cài đặt chưa, và tôi định để cửa sổ lùi mở bao lâu trước khi gạt công tắc đó".
Muốn tự kiểm cụm của bạn có đang mắc kẹt ở bước hai không, chạy thử:
kafka-features.sh --bootstrap-server kf:9092 describe
Nếu SupportedMaxVersion cao hơn FinalizedVersionLevel, bạn đã nâng nhị phân mà chưa nâng giao thức. Có thể là cố ý — hoặc là một lần nâng cấp bỏ dở từ lâu mà không ai nhớ.
Phần sau đo Kafka trong Docker cho môi trường thật: thiếu một dòng cấu hình là mất 10.000 tin.