Có một nghịch lý ở Kafka: nó ghi mọi message xuống đĩa để đảm bảo độ bền, nhưng vẫn ingest hàng triệu message mỗi giây — nhanh hơn nhiều hệ chỉ giữ dữ liệu trong RAM. Bí quyết không nằm ở phần cứng đắt tiền, mà ở một lựa chọn cấu trúc dữ liệu: log chỉ-thêm-vào-cuối. Bài này không chỉ giải thích — ta chạy thật Kafka 3.8 (KRaft, không cần Zookeeper) và đo bằng chính công cụ perf của Kafka.

Bài toán: đĩa "chậm" — làm sao ghi bền mà vẫn nhanh?

Trực giác thông thường: chạm đĩa là chậm nên hệ nhanh phải tránh đĩa. Nhưng "đĩa chậm" chỉ đúng với ghi ngẫu nhiên. Ghi tuần tự (nối vào cuối) nhanh hàng bậc độ lớn. Kafka đặt cược toàn bộ thiết kế vào sự thật này: mỗi partition là một log chỉ-thêm — message mới luôn ghi vào cuối, mỗi cái nhận một offset tăng dần, không bao giờ sửa/xoá giữa chừng. Consumer tự nhớ offset đã đọc, nên đọc cũng là quét tuần tự.

Tạo topic (chia thành nhiều partition = nhiều log song song):

kafka-topics.sh --create --topic events \
    --partitions 6 --replication-factor 1 \
    --bootstrap-server localhost:9092

Ảnh chụp đoạn mã nền tối minh hoạ Kafka thật 3.8 KRaft log chỉ thêm ingest hàng triệu message mỗi giây append-only log partition page cache zero-copy batching, một tạo topic bằng tạo log chia thành nhiều partition mỗi partition là 1 log chỉ thêm kafka-topics.sh create topic events partitions 6 replication-factor 1 bootstrap-server localhost 9092 mỗi partition offset tăng dần ghi luôn ở cuối tuần tự 6 partition 6 log song song, hai producer perf test ghi 5 triệu message gộp lô batching kafka-producer-perf-test.sh topic events num-records 5000000 record-size 100 throughput -1 producer-props acks 1 batch.size 65536 linger.ms 5 gộp nhiều record thành 1 request ít syscall ghi tuần tự lô lớn, ba consumer perf test đọc 5 triệu message quét tuần tự cộng zero-copy sendfile kafka-consumer-perf-test.sh topic events messages 5000000 bootstrap-server localhost 9092 consumer tự giữ offset đọc là quét tuần tự từ offset broker sendfile từ page cache thẳng ra socket zero-copy không copy qua user-space

Hình 1: Chạy Kafka thật — tạo topic 6 partition (mỗi partition là log chỉ-thêm), producer perf test với batching (batch.size/linger.ms), consumer perf test đọc tuần tự từ offset với zero-copy sendfile().

Đo THẬT bằng công cụ perf của Kafka

Ta chạy một broker Kafka 3.8 (KRaft, 1 broker, 6 partition) trong container và dùng kafka-producer-perf-test.sh / kafka-consumer-perf-test.sh — công cụ benchmark chính thức của Kafka.

Producer — ghi 5 triệu message 100 byte:

5.000.000 records sent
1.253.132 records/sec  (119,51 MB/sec)
avg latency 17,19 ms | p50 1 ms | p95 25 ms | p99 85 ms | p99.9 1441 ms

Hơn 1,25 triệu message/giây trên một broker, chỉ với ghi tuần tự vào log + gộp lô. Đây chính là điều lý giải "hàng triệu message/giây" của Kafka — không phép màu, chỉ là append-only + batching.

Consumer — đọc 5 triệu message:

5.000.000 messages consumed
1.139.211 msg/sec  (108,64 MB/sec)   fetch: 7.812.500 nMsg/sec

Đọc đạt 1,14 triệu message/giây. Consumer chỉ quét tuần tự từ offset của mình; broker dùng sendfile() bơm byte thẳng từ page cache ra socket (zero-copy), không copy qua user-space.

Ảnh chụp bảng kết quả chạy thật trên Kafka 3.8 KRaft 1 broker 6 partition output thật, producer kafka-producer-perf-test 5 triệu message 100 byte 5000000 records sent 1253132 records mỗi giây 119,51 MB mỗi giây avg latency 17,19 ms p50 1 ms p95 25 ms p99 85 ms p99.9 1441 ms ghi tuần tự vào log cộng gộp lô hơn 1,2 triệu message mỗi giây trên 1 broker, consumer kafka-consumer-perf-test đọc 5 triệu message 5000000 messages consumed 1139211 msg mỗi giây 108,64 MB mỗi giây fetch 7812500 nMsg mỗi giây đọc bằng quét tuần tự từ offset broker sendfile từ page cache ra socket zero-copy, vì sao nhanh thế nguồn tài liệu paper Kafka append-only mỗi partition là log chỉ thêm ghi luôn ở cuối tuần tự đĩa yêu batching producer gộp record thành lô batch.size linger.ms ít syscall page cache dựa vào cache OS thay vì heap JVM đọc gần đây khỏi chạm đĩa zero-copy sendfile bơm từ page cache ra socket bỏ copy qua user-space partition 6 partition 6 log song song trên nhiều broker nhân throughput offset consumer tự giữ offset broker không theo dõi từng consumer

Hình 2: Chạy thật trên Kafka 3.8 — producer 1.253.132 records/giây (119,51 MB/s, p50 1ms), consumer 1.139.211 msg/giây (108,64 MB/s). Cùng bốn trụ cột tốc độ: append-only, batching, page cache, zero-copy, partition, offset.

Bốn trụ cột đằng sau con số

Append-only là nền, ba kỹ thuật còn lại khuếch đại (theo tài liệu Kafka):

  1. Batching. Producer gộp nhiều record thành một lô (batch.size, linger.ms) trước khi gửi — ít request, ít syscall, và ghi tuần tự lô lớn xuống log. Đây là lý do throughput đạt triệu/giây thay vì bị chặn bởi chi phí mỗi-message.
  2. Page cache thay vì heap. Kafka để OS đệm dữ liệu trong page cache (RAM do kernel quản), flush theo lô. Message vừa ghi còn nóng trong cache nên consumer đọc gần như không chạm đĩa. Broker Kafka do đó dùng heap JVM nhỏ, nhường RAM cho page cache.
  3. Zero-copy bằng sendfile(). Khi gửi cho consumer, broker bơm byte thẳng từ page cache ra socket, bỏ hai lần copy và hai lần chuyển ngữ cảnh mỗi lô.
  4. Partition để song song. Một topic chia nhiều partition trên nhiều broker; ghi/đọc chạy song song. Throughput một broker (đã đo) nhân lên quy mô cụm.

Đánh đổi cần cân nhắc

Append-only tuyệt cho ingest, nhưng không cho cập nhật/xoá điểm. Không thể "sửa message thứ 5". Muốn biểu diễn cập nhật, Kafka dùng log compaction (giữ bản mới nhất mỗi khoá) — cơ chế riêng, không phải sửa tại chỗ. Bài toán CRUD nhiều cập nhật ngẫu nhiên thì log không phải cấu trúc đúng.

Con số là throughput với acks=1, page cache. 1,25 triệu/giây ở trên dùng acks=1 (chỉ chờ leader ghi) và dựa page cache. acks=all (chờ mọi replica) hoặc ép flush đĩa mỗi message sẽ giảm throughput đổi lấy độ bền cao hơn. Độ bền thật của Kafka đến từ replication sang broker khác, không phải fsync từng cái — một đánh đổi có chủ đích giữa độ trễ và độ bền.

"Đĩa chậm" là huyền thoại có điều kiện. Bài học cốt lõi: nhanh/chậm phụ thuộc mẫu truy cập, không phải "RAM vs đĩa" tuyệt đối. Thiết kế để truy cập tuần tự thì đĩa nhanh bất ngờ; thiết kế bừa thì cả SSD cũng chậm.

Ba ý mang về

  1. Log chỉ-thêm-vào-cuối biến mọi ghi thành tuần tự — và ghi tuần tự nhanh hàng bậc độ lớn: đo thật bằng công cụ perf của Kafka, producer đạt 1,25 triệu records/giây (119,51 MB/s) trên một broker.
  2. Batching, page cache, zero-copy khuếch đại thêm: consumer đọc 1,14 triệu message/giây bằng quét tuần tự từ offset + sendfile() zero-copy; partition cho phép nhân throughput song song trên cụm.
  3. Chọn cấu trúc theo mẫu truy cập, không theo huyền thoại: append-only đánh đổi khả năng cập nhật-điểm để lấy ingest cực nhanh, và dựa vào replication (không phải fsync từng cái) cho độ bền — acks là knob cân giữa độ trễ và độ bền.

Nguồn

Phần sau ta chuyển sang một bài toán rất khác: Uber ghép tài xế với khách theo vị trí — dựng thật chỉ mục không gian trên Redis GEO và đo trực tiếp.