Hình dung bạn nhân bản cả một thư viện sang toà nhà dự phòng. Chép hết sách lên kệ thì dễ — cứ đủ tiền và đủ xe tải là xong. Nhưng có một thứ tinh tế hơn nhiều: dấu trang của từng người đọc, cái kẹp cho biết mỗi người đang đọc dở tới trang nào. Nếu chỉ sách sang mà dấu trang không sang, thì đến ngày mọi người chuyển qua thư viện mới, ai cũng phải đọc lại từ trang đầu — hoặc tệ hơn, nhảy thẳng tới trang cuối. Sao chép Kafka giữa hai vùng đúng y như vậy: sách là dữ liệu, dấu trang là offset của consumer, và phần chép dấu trang là phần âm thầm không chạy.
Sao chép Kafka giữa hai vùng là nền của mọi kế hoạch phục hồi thảm hoạ. Bài này dựng hai cụm thật, chạy MirrorMaker 2, và tìm ra rằng phần dễ thì chạy hoàn hảo còn phần khó thì im lặng không chạy.
Dựng
Hai cụm độc lập A và B, sao chép topic don từ A sang B:
clusters = A, B
A.bootstrap.servers = kA:9092
B.bootstrap.servers = kB:9092
A->B.enabled = true
A->B.topics = don
sync.group.offsets.enabled = true
connect-mirror-maker.sh mm2.props
Dữ liệu sang rất nhanh
| chép hết 200.000 tin đã có | 8,7 giây |
| độ trễ khi đang chạy | 1.467 / 498 / 1.457 / 1.436 ms |
Khoảng một giây rưỡi cho một tin mới (đã bao gồm khoảng 200–300 ms chạy công cụ). Trên mạng cục bộ thì đây gần như là mức sàn; giữa hai vùng thật, cộng thêm thời gian đi về của mạng.
Tên topic ở cụm đích mang tiền tố cụm nguồn:
cụm A: don -> cụm B: A.don
Tiền tố là cố ý và quan trọng: nó ngăn vòng lặp vô tận khi bạn sao chép hai chiều. Không có nó, don ở B sẽ được chép ngược về A thành don, rồi lại sang B, mãi mãi.
Đổi lại, mọi consumer ở cụm B phải biết tên có tiền tố. Đây là chỗ kế hoạch chuyển vùng hay bị hụt: ứng dụng đọc don ở A, và khi chuyển sang B nó phải đọc A.don — một thay đổi cấu hình mà ai đó phải nhớ, đúng vào lúc đang có sự cố.
Offset thì không sang
Đây là phần quan trọng nhất, và nó không hoạt động.
Nhóm gfail ở cụm A đã đọc tới:
p0 offset 16.200 p1 offset 16.900 p2 offset 16.900
Ở cụm B, sau khi bật sync.group.offsets.enabled=true và chờ:
topic A.checkpoints.internal:0:0 không có bản ghi nào
nhóm gfail: không tồn tại
Tôi thử thêm A->B.groups=.*, emit.checkpoints.enabled=true, sync.group.offsets.interval.seconds=5, khởi động lại MirrorMaker, và thử cả với một nhóm đang hoạt động thay vì nhóm đã dừng. Vẫn 0 bản ghi.
Trong thời gian tôi dành cho nó, tôi không tìm ra nguyên nhân, và ghi lại đúng như vậy thay vì đoán.
Có một dòng lỗi trong log nhưng nó thuộc chiều ngược lại (B->A, chiều tôi không bật):
ERROR [Worker clientId=B->A] Failed to reconfigure connector's tasks
(MirrorCheckpointConnector), retrying after backoff.
RetriableException: Timeout while loading consumer groups.
Điều đáng nói nhất không phải là nó hỏng, mà là cách nó hỏng: topic checkpoint được tạo ra đàng hoàng, rỗng, và im lặng. Không có gì trên bảng theo dõi cho biết việc dịch offset đang không chạy.
Bài học vận hành
Kế hoạch chuyển vùng dựa vào offset đã dịch chỉ đúng nếu offset thật sự được dịch. Hãy kiểm chứ đừng cấu hình rồi tin:
kafka-get-offsets.sh --bootstrap-server kB:9092 --topic A.checkpoints.internal
Ra 0 nghĩa là khi cụm A chết, mọi consumer khởi động ở cụm B sẽ không tìm thấy offset nào và rơi về auto.offset.reset:
earliest→ xử lý lại toàn bộ lịch sử còn giữ trên topiclatest→ bỏ qua mọi thứ chưa xử lý
Cả hai đều là hỏng, và cả hai đều xảy ra trong im lặng đúng vào lúc tệ nhất.
Câu tổng kết: sao chép dữ liệu là phần dễ. Chuyển vùng có trạng thái mới là phần khó.
Bảy topic nội bộ
MM2 tạo ra khá nhiều thứ ở cả hai cụm:
cụm B: A.don bản sao dữ liệu
A.checkpoints.internal offset đã dịch
heartbeats kiểm tra đường sống
mm2-configs.A.internal cấu hình Connect
mm2-offsets.A.internal tiến độ của chính MM2
mm2-status.A.internal trạng thái connector
cụm A: mm2-offset-syncs.B.internal ánh xạ offset A <-> B
Đáng biết vì hai lý do: chúng chiếm chỗ và cần chính sách lưu trữ, và khi dọn dẹp một thiết lập MM2, xoá connector không xoá chúng.
Ba kiến trúc, chọn cái nào
Chủ động – bị động. Ghi vào A, sao chép sang B, B chỉ dùng khi A chết. Đơn giản nhất và là lựa chọn đúng cho phần lớn trường hợp. Điểm yếu là chính bài toán offset ở trên.
Chủ động – chủ động. Ghi vào cả hai, sao chép hai chiều. Tiền tố topic ngăn vòng lặp, nhưng ứng dụng phải đọc cả don lẫn B.don để thấy đủ dữ liệu. Và không có thứ tự toàn cục giữa hai cụm — hai sự kiện cùng khoá ghi ở hai nơi sẽ không có quan hệ trước-sau nào.
Cụm trải vùng. Một cụm Kafka duy nhất với broker ở nhiều vùng, dùng rack awareness để rải bản sao. Không cần MM2, không có tiền tố, offset đúng tự nhiên. Đổi lại acks=all phải chờ qua liên vùng — phần 4 đo được p50 434 ms trên mạng cục bộ, và liên vùng thì cộng thêm 50–150 ms mỗi chiều.
Kiến trúc thứ ba là kiến trúc duy nhất giải quyết được bài toán offset một cách sạch sẽ, và nó chỉ khả thi khi độ trễ giữa các vùng đủ thấp.
Cùng một lằn ranh "dữ liệu vs vị trí đọc", đổi ngăn xếp
Khác biệt cốt lõi ở đây — chép được dữ liệu không có nghĩa là chép được vị trí đã xử lý tới đâu — không phải chuyện riêng của Kafka. Nó là một trong những cái bẫy lặp lại nhất của mọi bài toán sao chép và chuyển giao có trạng thái.
Trong thế giới cơ sở dữ liệu, đây chính là lý do failover khó hơn backup: sao chép các hàng (physical/logical replication) là phần dễ, nhưng một replica muốn lên làm primary cần đúng vị trí sao chép (LSN của PostgreSQL, GTID của MySQL) thì client mới biết tiếp tục từ đâu — cả một lớp công cụ failover tồn tại chỉ để lo đúng chuyện đó. Khôi phục từ bản sao lưu cũng vậy: chép hàng thì đủ, nhưng quên chép mốc cao nhất của sequence/auto-increment thì bản ghi mới hoặc đè lên hoặc nhảy số.
Trong thế giới Java, sao chép một collection thì dễ, nhưng sao chép vị trí của một iterator vào collection đó thì không — iterator không mang đi được. Mọi stream processor có checkpoint đều dựa trên cùng phân biệt này: dữ liệu thì bền, nhưng cái khiến khởi động lại đúng là offset đã commit, không phải bản thân dữ liệu.
Mẫu số chung: sao chép dữ liệu là phần dễ; sao chép vị trí đọc — con trỏ cho biết ai đã xử lý tới đâu — mới là phần khó, và là phần hay âm thầm không chạy; một cuộc chuyển giao chỉ liền mạch khi con trỏ đi cùng dữ liệu, bằng không bên tiếp nhận hoặc làm lại từ đầu hoặc nhảy qua phần chưa xử lý — và cả hai đều hỏng trong im lặng, đúng vào ngày tệ nhất. Nên câu hỏi đúng với bất kỳ bản sao hay kế hoạch dự phòng nào không phải "dữ liệu đã sang chưa" mà là "vào ngày chuyển giao thật, bên mới có biết bắt đầu từ đâu không, và tôi đã kiểm con số đó chứ không chỉ cấu hình rồi tin chưa".
Nếu bạn đang chạy MM2, kiểm ba con số này một lần còn hơn tin rằng mọi thứ ổn:
# dữ liệu có sang không
kafka-get-offsets.sh --bootstrap-server kB:9092 --topic A.ten-topic
# offset có được dịch không
kafka-get-offsets.sh --bootstrap-server kB:9092 --topic A.checkpoints.internal
# nhóm consumer có xuất hiện ở cụm đích không
kafka-consumer-groups.sh --bootstrap-server kB:9092 --list | grep ten-nhom
Con số đầu tiên tăng còn hai con số sau không đổi nghĩa là bạn có bản sao dữ liệu nhưng không có kế hoạch chuyển vùng — và khác biệt đó chỉ lộ ra vào ngày cụm chính chết.
Phần sau đo quy mô cụm: bao nhiêu broker là đủ, và dấu hiệu nào nói đã đến lúc thêm.