Kafka được mô tả là nhanh nhờ "zero-copy" và "page cache" — hai cụm từ hay được nhắc mà ít khi được đo. Bài này đo cả hai.
Broker 1,2 GB phục vụ 1.130 MB mỗi giây
Ghi 2 triệu tin × 500 byte — 973 MB trên đĩa — rồi đọc lại toàn bộ:
đọc lần 1 1.129,9 MB/giây 2.369.668 tin/giây
đọc lần 2 1.034,4 MB/giây 2.169.197 tin/giây
Trong khi đó:
bộ nhớ container 1,227 GiB
Cached (hệ điều hành) 5.630.792 kB
973 MB dữ liệu không nằm trong JVM. Nó nằm trong bộ đệm trang của hệ điều hành, và Kafka không quản lý nó chút nào.
Đây là quyết định thiết kế lớn nhất của Kafka và cũng là quyết định "lười" nhất: thay vì tự dựng một bộ nhớ đệm như cơ sở dữ liệu vẫn làm, nó ghi ra tệp và để nhân hệ điều hành lo. Nhân đã có sẵn thuật toán thay trang tốt, đã có sẵn đọc trước tuần tự, và quan trọng nhất — bộ nhớ đó không phải heap của JVM, nên không có lần dọn rác nào phải quét qua.
Xoá sạch bộ đệm rồi đọc lại
Câu hỏi thật sự: nếu bộ đệm trang biến mất thì Kafka chậm đi bao nhiêu?
sync; echo 3 > /proc/sys/vm/drop_caches
Cached: 5.630.792 kB -> 1.131.884 kB
Rồi đọc lại:
| Thông lượng | |
|---|---|
| đọc lạnh (vừa xoá bộ đệm) | 677,8 MB/giây |
| đọc lại ngay sau (đã nóng lại) | 1.088,7 MB/giây |
Chỉ chênh 1,6 lần.
Đây là kết quả tôi không chờ đợi. Trực giác về "cache miss" gợi ý con số 10 hay 100 lần — đó là tỉ lệ quen thuộc khi một cơ sở dữ liệu phải đi tìm trang ngẫu nhiên trên đĩa.
Kafka thì đọc tuần tự, và đọc tuần tự từ ổ thể rắn vốn đã rất nhanh. Nhân còn tự đọc trước, nên trong lúc consumer xử lý khối này thì khối sau đã đang được nạp.
Kết luận thực dụng: bộ đệm trang giúp thật, nhưng mất nó không phải thảm hoạ. Một broker vừa khởi động lại, hoặc một consumer đọc lại dữ liệu từ tuần trước, chỉ chậm đi khoảng một phần ba — không phải hai bậc độ lớn. Điều này khiến việc đọc lại lịch sử trên Kafka khả thi hơn nhiều người nghĩ.
(Con số này gắn với ổ thể rắn. Trên ổ quay, chênh lệch sẽ lớn hơn nhiều — nhưng Kafka đọc tuần tự nên vẫn dễ chịu hơn cơ sở dữ liệu đọc ngẫu nhiên.)
Bốn thứ Kafka không làm
Hiệu năng của Kafka đến từ những việc nó không làm, hơn là từ thuật toán khéo léo nào.
Không giữ bộ nhớ đệm riêng. Phần 2 đo được: ghi 500.000 tin chỉ đẩy bộ nhớ JVM từ 316 MB lên 577 MB, trong khi dữ liệu tăng 100 MB. Heap nhỏ nghĩa là không có lần dọn rác nào kéo dài, và độ trễ p99 không có gai.
Không sửa dữ liệu tại chỗ. Ghi luôn là nối vào cuối tệp. Không có cây phải cân bằng lại, không phân mảnh, không đọc-sửa-ghi. Đây là thao tác mà mọi loại ổ đĩa đều làm nhanh nhất.
Không sao chép dữ liệu qua vùng nhớ JVM khi gửi. Với dữ liệu không cần lọc, broker gọi sendfile để nhân đẩy thẳng trang từ bộ đệm ra socket. Byte không bao giờ đi vào heap. Đây chính là "zero-copy", và nó là lý do một broker heap nhỏ phục vụ được hơn một gigabyte mỗi giây.
Không giải nén ở broker. Phần 5 đo được lz4 nén 6,7 lần. Lô nén đó đi nguyên vẹn từ producer, qua đĩa broker, qua các bản sao, tới tận consumer. Broker không mở ra xem.
sendfile bị tắt lúc nào
Zero-copy chỉ hoạt động khi broker gửi nguyên xi những gì nó đã lưu. Ba trường hợp phá vỡ điều đó:
isolation.level=read_committed— broker phải lọc bản ghi của giao dịch bị huỷ (phần 6), nên phải đọc dữ liệu vào bộ nhớ.- Giao thức nén khác nhau giữa topic và producer — broker phải giải nén rồi nén lại. Giữ
compression.type=producerở topic để tránh (phần 5). - TLS — dữ liệu phải đi qua tầng mã hoá trong vùng người dùng.
sendfilekhông dùng được. Đây là cái giá thật của việc bật SSL trên Kafka, và nó lớn hơn chi phí mã hoá thuần tuý.
Không phải lý do để không bật TLS trên mạng công cộng. Nhưng nó giải thích vì sao cùng một cụm sau khi bật TLS lại cần nhiều CPU hơn hẳn dự đoán.
Ba hệ quả khi vận hành
Đừng cấp heap lớn cho Kafka. 4–6 GB là đủ cho hầu hết trường hợp. Phần RAM còn lại để hệ điều hành làm bộ đệm trang thì có ích hơn nhiều — và heap lớn còn kéo dài thời gian dọn rác, tạo ra chính những gai độ trễ mà bạn đang cố tránh.
Đừng chạy dịch vụ ngốn RAM khác trên cùng máy. Một tiến trình khác chiếm RAM sẽ đẩy bộ đệm trang của Kafka ra, và consumer đang bám sát leader mất đi chỗ đọc rẻ nhất. Kafka thích chạy một mình trên máy, không phải vì nó kiêu mà vì nó đang mượn RAM của cả hệ thống.
Đừng lo khi thấy "bộ nhớ dùng gần hết" trên máy chạy Kafka. Phần lớn là Cached, và hệ điều hành trả lại ngay khi có ai cần. Cảnh báo đặt ở MemFree thấp sẽ kêu suốt ngày trên một máy Kafka hoàn toàn khoẻ mạnh. Đặt ở MemAvailable thì đúng hơn.
Consumer chậm mới là thứ đắt
Có một hệ quả ít ai nói: consumer đọc dữ liệu cũ phá bộ đệm trang cho mọi người.
Consumer bám sát leader đọc từ RAM — dữ liệu vừa ghi vẫn còn nóng. Nhưng một consumer đang tụt lại vài giờ phải đọc từ đĩa, và mỗi trang nó nạp lên sẽ đẩy một trang nóng ra ngoài. Consumer khoẻ mạnh bỗng chậm đi vì một consumer khác đang bị lag.
Đây là lý do lag không chỉ là vấn đề của riêng consumer đang lag. Nếu bạn có một nhóm chuyên đọc lại lịch sử — dựng lại chỉ mục, chạy lại báo cáo — hãy cân nhắc cho nó đọc từ một bản sao khác, hoặc chạy vào giờ thấp điểm.
Thử ba mươi giây
Xem dữ liệu Kafka của bạn có đang nằm trong bộ đệm trang không:
# tổng dữ liệu Kafka trên đĩa
du -sh /var/lib/kafka/data
# bộ đệm trang của hệ điều hành
grep -E "^(Cached|MemAvailable)" /proc/meminfo
# heap thật sự của Kafka
jcmd $(pgrep -f kafka.Kafka) GC.heap_info
Nếu Cached lớn hơn nhiều so với heap Kafka, mọi thứ đang đúng như thiết kế. Nếu heap của bạn lớn hơn 8 GB, đó là chỗ đầu tiên nên cắt.
Phần sau đo flush.messages và fsync — và một phép đo sai của tôi suýt tạo ra một kết luận hoàn toàn bịa.