cleanup.policy=compact biến topic Kafka thành thứ gần với một bảng khoá–giá trị. Bài này đo xem nó thật sự để lại cái gì — và đáp án không phải cái người ta hay nói.
Nó dùng để làm gì
Giữ tin theo thời gian phù hợp với dòng sự kiện: "đơn hàng #123 đã tạo", "đơn hàng #123 đã giao". Sự kiện cũ hết ý nghĩa thì bỏ.
Nhưng có loại dữ liệu khác: trạng thái hiện tại. "Số dư tài khoản A là 500.000", "cấu hình của dịch vụ B là X". Ở đây tin cũ không hết ý nghĩa theo thời gian — nó hết ý nghĩa khi có tin mới cùng khoá.
cleanup.policy=compact giữ lại giá trị mới nhất của mỗi khoá và bỏ những giá trị cũ hơn. Kết quả là một topic mà đọc từ đầu tới cuối cho ra ảnh chụp trạng thái hiện tại — nền tảng của Kafka Streams state store, Kafka Connect, và chính topic nội bộ __consumer_offsets.
30.000 tin, 1.000 khoá, còn lại 2.908
Ghi 30.000 tin chỉ dùng 1.000 khoá — mỗi khoá 30 phiên bản — rồi đợi nén chạy:
tin đọc được: 2.908
khoá phân biệt: 1.000
Không phải 1.000.
Nhìn kỹ một khoá:
K500 v27500
K500 v28500
K500 v29500
Ba tin của cùng một khoá vẫn còn.
Nén bảo đảm giá trị cuối cùng của mỗi khoá còn lại. Nó không bảo đảm mỗi khoá chỉ còn một tin.
Lý do: Kafka chia log thành hai phần. Phần đầu (head) là khu vực mới ghi, gồm khúc đang mở và những khúc chưa được bộ dọn chạm tới; phần đuôi (tail) là khu vực đã nén. Bộ dọn chỉ làm việc trên phần đuôi, và phần đầu luôn còn nguyên mọi bản trùng.
Hệ quả thực dụng và bắt buộc phải nhớ: consumer đọc topic nén phải chịu được đọc trùng. Đọc từ đầu tới cuối và ghi đè vào một Map là đúng — vì tin đến sau luôn mới hơn. Đếm số dòng để suy ra số khoá là sai.
Trên đĩa
00000000000000000000.log 0 byte
00000000000000005885.log 0 byte
00000000000000011610.log 0 byte
00000000000000017106.log 0 byte
00000000000000022596.log 17.948 byte
00000000000000028092.log 34.216 byte <- khúc đang mở, không nén
620 KB xuống 100 KB. Bốn khúc đầu thành tệp 0 byte — mọi tin trong đó đều đã có phiên bản mới hơn ở khúc sau.
Và chi tiết quan trọng: offset vẫn là 0 đến 30.000. Nén không đánh số lại. Nó bỏ bản ghi và để lại khoảng trống trong dãy offset.
Nghĩa là trên topic nén, offset cuối − offset đầu không phải số tin. Mọi phép tính lag dựa trên hiệu số offset đều sai — cùng vấn đề với topic giao dịch ở phần 6, nhưng nguyên nhân khác.
Xoá một khoá: bia mộ
Gửi tin có khoá nhưng giá trị null:
K1 (rỗng)
Đó là "bia mộ". Nén thấy nó thì bỏ mọi giá trị cũ hơn của khoá đó, rồi sau một khoảng thời gian bỏ luôn chính bia mộ.
Khoảng thời gian đó là delete.retention.ms, mặc định 86.400.000 ms — một ngày. Và nó dẫn tới một cái bẫy nghiêm trọng:
Consumer offline lâu hơn delete.retention.ms sẽ không bao giờ thấy lệnh xoá. Khi nó quay lại, bia mộ đã bị dọn, và bản sao cục bộ của nó giữ lại một khoá đáng lẽ đã bị xoá — vĩnh viễn, cho tới khi có ai đó dựng lại từ đầu.
Với dữ liệu phải xoá theo yêu cầu pháp lý, đây là chỗ nguy hiểm thật: bạn tưởng đã xoá, mà một bản sao ở đâu đó vẫn giữ. Nếu consumer của bạn có thể offline lâu, hãy nâng delete.retention.ms lên dài hơn thời gian offline tối đa bạn chấp nhận.
Ba điều kiện để nén thật sự chạy
Đây là nguồn của phần lớn câu hỏi "tôi bật compact rồi mà không thấy gì đổi".
cleanup.policy=compact — mặc định là delete. Đặt được compact,delete để dùng cả hai: nén theo khoá và vẫn xoá theo retention.ms. Hữu ích khi bạn muốn giữ trạng thái hiện tại nhưng không muốn giữ mãi mãi những khoá không còn ai cập nhật.
min.cleanable.dirty.ratio — mặc định 0,5. Bộ dọn chỉ bắt tay vào một partition khi phần chưa nén chiếm quá nửa. Với topic ít ghi, tỉ lệ đó có thể mất hàng tuần mới đạt. Trong phép đo này tôi đặt 0,01 để nó chạy trong vòng vài phút.
min.compaction.lag.ms — mặc định 0, nhưng nếu ai đó đặt nó thì tin mới hơn khoảng đó không bị đụng tới.
Và một điều kiện âm thầm hơn cả ba cái trên: tin không có khoá thì không bao giờ bị nén. Nó không có gì để so trùng. Trên một topic đặt compact, những tin thiếu khoá tích lại mãi mãi, và vì retention.ms không áp cho chính sách compact, chúng cũng không hết hạn. Đây là kiểu rò rỉ đĩa rất khó lần ra, vì mọi cấu hình nhìn đều đúng.
Chọn khoá cho topic nén
Với topic nén, khoá là khoá chính. Ba luật:
Khoá phải là danh tính, không phải phân nhóm. khach_hang_id đúng, khu_vuc sai — nén theo khu_vuc sẽ để lại đúng một bản ghi cho mỗi khu vực và xoá sạch dữ liệu của mọi khách hàng trừ người cuối cùng.
Số khoá phân biệt quyết định kích thước cuối cùng. Topic nén hội tụ về khoảng số khoá × cỡ trung bình mỗi tin. Nếu số khoá tăng không giới hạn — ví dụ khoá là mã phiên — thì topic cũng tăng không giới hạn dù có nén.
Phải xoá bằng bia mộ, không xoá bằng cách ngừng ghi. Khoá không được cập nhật nữa sẽ nằm lại mãi. Với cleanup.policy=compact,delete thì retention.ms mới dọn được chúng.
Thử ba mươi giây
Kiểm topic nén của bạn có đang nén thật không:
kafka-configs.sh --bootstrap-server kf:9092 --describe \
--entity-type topics --entity-name ten-topic | tr ',' '\n' | grep -E "cleanup|dirty|compaction"
# so số tin đọc được với số khoá phân biệt
kafka-console-consumer.sh --bootstrap-server kf:9092 --topic ten-topic \
--from-beginning --timeout-ms 30000 --property print.key=true 2>/dev/null > /tmp/c.txt
wc -l < /tmp/c.txt
cut -f1 /tmp/c.txt | sort -u | wc -l
Tỉ lệ gần 1 là nén đang chạy tốt. Tỉ lệ 30:1 như phép đo ban đầu của tôi nghĩa là bộ dọn chưa chạm tới — và min.cleanable.dirty.ratio là chỗ đầu tiên nên nhìn.
Phần sau đo bộ đệm trang: vì sao broker 290 MB bộ nhớ phục vụ được hàng trăm MB mỗi giây.