Hình dung bạn đặt một trạm kiểm tra an ninh ở cổng một nhà kho. Với hàng đi vào, ai cũng đã phải dừng lại làm thủ tục nhập kho từ trước, nên trạm này cộng thêm chẳng bao nhiêu. Nhưng với hàng đi ra, trước giờ vốn có một làn tốc hành: thủ kho chuyển thẳng kiện hàng từ kệ lên xe tải mà không cần mở ra. Trạm kiểm tra mới buộc mở từng kiện để soi — và cái làn tốc hành đó biến mất hoàn toàn. Cùng một trạm kiểm tra, nhưng nó rẻ ở chiều vào và đắt ở chiều ra, chỉ vì chiều ra mới có thứ để mất. TLS của Kafka đúng là cái trạm đó, và tôi sẽ đo cả hai chiều.

Phần 19 dự đoán rằng bật TLS sẽ tắt sendfile và làm việc đọc đắt hơn hẳn việc ghi. Bài này dựng một broker có cả hai cổng và đo xem dự đoán đó đúng bao nhiêu.

Dựng broker có cả hai cổng

Một broker, hai listener — PLAINTEXT ở 9092 và SSL ở 9094 — nên mọi phép so sánh chạy trên cùng phần cứng, cùng dữ liệu, cùng thời điểm.

KAFKA_LISTENERS=PLAINTEXT://:9092,SSL://:9094,CONTROLLER://:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL
KAFKA_SSL_KEYSTORE_FILENAME=server.keystore.jks
KAFKA_SSL_KEYSTORE_CREDENTIALS=keystore_creds
KAFKA_SSL_KEY_CREDENTIALS=key_creds

Ảnh apache/kafka đọc mật khẩu từ tệp trong /etc/kafka/secrets, không đọc từ biến môi trường. Đặt KAFKA_SSL_KEYSTORE_PASSWORD trực tiếp làm container chết với thông báo line 18: !1: unbound variable — một thông báo không nói gì về nguyên nhân, và tôi mất một lúc mới lần ra.

Ghi: chậm 20%

300.000 tin × 500 byte, ba vòng:

Vòng 1 Vòng 2 Vòng 3
PLAINTEXT 197,58 241,23 279,94 MB/giây
SSL 201,20 190,23 223,87 MB/giây

Lấy vòng đã nóng máy: 279,94 so với 223,87 — SSL chậm 20%.

Đây là chi phí mã hoá thuần tuý. Không nhỏ, nhưng cũng không phải thứ khiến ai đó từ bỏ TLS.

Đọc: chậm 2,2 lần

900.000 tin, ba vòng:

Vòng 1 Vòng 2 Vòng 3
PLAINTEXT 644,7 993,9 1052,4 MB/giây
SSL 491,0 470,6 483,3 MB/giây

1052,4 so với 483,3 — SSL chậm 2,2 lần.

Nhưng con số quan trọng hơn nằm ở hình dạng của hai hàng.

Hàng PLAINTEXT tăng dần: 644 → 993 → 1052. Đó là bộ đệm trang nóng lên (phần 19).

Hàng SSL gần như phẳng: 491 → 470 → 483. Nó không hưởng lợi gì từ việc dữ liệu đã nằm sẵn trong bộ đệm trang.

Vì sao đọc tốn hơn ghi nhiều đến vậy

Phần 19 đã giải thích cơ chế, và phép đo này xác nhận nó.

Không có TLS, broker gọi sendfile: nhân hệ điều hành đẩy thẳng trang từ bộ đệm ra socket. Byte không bao giờ đi vào vùng nhớ của JVM. Đó là lý do một broker heap nhỏ phục vụ được hơn một gigabyte mỗi giây.

Có TLS, mỗi byte phải vào vùng người dùng để mã hoá. Zero-copy biến mất hoàn toàn. Và cùng với nó, phần lớn lợi ích của bộ đệm trang cũng biến mất — vì nút thắt chuyển từ "lấy dữ liệu ra khỏi đĩa" sang "chạy AES trên từng byte". Đó chính là cột SSL phẳng ở trên.

Phía ghi thì không có sendfile để mất: dữ liệu đến từ mạng và phải đi qua JVM dù thế nào. Nên nó chỉ tốn chi phí mã hoá thuần: 20%.

Kết luận thực dụng cho việc lập kế hoạch dung lượng: bật TLS thì phải tính lại phía đọc, không phải phía ghi. Nếu cụm của bạn có nhiều nhóm consumer đọc cùng một dữ liệu, đó là chỗ chi phí nhân lên.

Bắt tay không phải chỗ đáng lo

PLAINTEXT   701 ms cho mỗi lệnh CLI
SSL         704 ms

Chênh lệch nằm dưới nhiễu, và cả hai con số đều là thời gian khởi động JVM (phần 2), không phải thời gian bắt tay.

Bắt tay TLS chỉ xảy ra một lần mỗi kết nối, và client Kafka giữ kết nối rất lâu. Với ứng dụng chạy dài, nó không đáng kể.

Nó chỉ đáng kể với ứng dụng tạo producer mới cho từng yêu cầu — nhưng đó là mẫu sai vì nhiều lý do khác nữa (phần 2).

Bốn giao thức

Mã hoá Xác thực
PLAINTEXT không không
SSL có bằng chứng chỉ client (mTLS), nếu bật
SASL_PLAINTEXT không có
SASL_SSL có có

SASL_PLAINTEXT là cái bẫy. Tên nghe như có bảo mật, nhưng nó chỉ xác thực — thông tin xác thực đi trần trên dây. Với SASL/PLAIN thì đó là mật khẩu ở dạng rõ; với SCRAM thì là dữ liệu thách thức, khá hơn nhưng vẫn không có mã hoá cho dữ liệu.

Nó chỉ hợp lý khi có một lớp mã hoá khác bên dưới — ví dụ lưới dịch vụ đã lo mTLS. Trong mọi trường hợp khác, dùng SASL_SSL.

Chọn cơ chế SASL

SASL/SCRAM-SHA-512 — lựa chọn mặc định hợp lý. Thông tin xác thực lưu trong chính Kafka (trước đây là ZooKeeper), thêm bớt user bằng kafka-configs.sh, không cần hạ tầng ngoài.

SASL/GSSAPI (Kerberos) — khi tổ chức đã có Kerberos. Mạnh, và phức tạp tương xứng.

SASL/OAUTHBEARER — khi đã có một nhà cung cấp danh tính. Token có hạn, thu hồi tập trung được — ưu điểm vận hành thật so với mật khẩu tĩnh.

SASL/PLAIN — mật khẩu nằm trong tệp cấu hình của broker, đổi mật khẩu phải khởi động lại. Chỉ dùng cho môi trường thử nghiệm.

mTLS thay vì SASL — danh tính lấy từ chứng chỉ client. Không có mật khẩu nào để rò rỉ, nhưng bạn phải vận hành một hạ tầng chứng chỉ, kể cả việc gia hạn. Chứng chỉ hết hạn lúc 3 giờ sáng là một loại sự cố rất khó chịu.

Phân quyền là bước riêng

Xác thực trả lời "bạn là ai". Phân quyền trả lời "bạn được làm gì" — và Kafka không tự bật nó.

kafka-acls.sh --bootstrap-server kf:9094 --command-config /tmp/ssl.props \
  --add --allow-principal User:doi-phan-tich \
  --operation Read --topic don-hang --group '*'

Không bật authorizer.class.name thì mọi user đã xác thực đều làm được mọi thứ — kể cả xoá topic của người khác. Xác thực mà không phân quyền chỉ ghi lại được ai đã gây ra sự cố, không ngăn được nó.

Và một chi tiết vận hành: bật phân quyền trên cụm đang chạy sẽ chặn hết mọi thứ trừ những gì đã có ACL. Cách chuyển an toàn là bật allow.everyone.if.no.acl.found=true trước, thêm đủ ACL, rồi mới tắt cờ đó.

Nếu chỉ làm được một việc

Với cụm nằm trong mạng riêng và mọi client thuộc cùng một tổ chức, thứ tự ưu tiên thực dụng:

  1. Đảm bảo cụm không lộ ra Internet. Nhóm bảo mật mạng và tường lửa làm việc này rẻ hơn mọi thứ bên dưới.
  2. Bật SASL_SSL với SCRAM. Trả 20% ghi và 2,2 lần đọc.
  3. Bật ACL. Không tốn hiệu năng, và là thứ ngăn một dịch vụ đọc dữ liệu nó không nên đọc.

Bước 3 rẻ nhất và hay bị bỏ qua nhất.

Cùng một lằn ranh "tính năng chạm-từng-byte phá đường tắt zero-copy", đổi ngăn xếp

Điều khiến phía đọc đắt hơn phía ghi nhiều đến vậy không phải đặc sản Kafka. Nó là hệ quả tất yếu của một định luật chung: bất cứ tính năng nào cần chạm vào từng byte đều không thể sống chung với đường tắt zero-copy.

Trong thế giới Java, chính cơ chế đó hiện ra ở FileChannel.transferTo() — ánh xạ thẳng xuống sendfile, đẩy tệp ra socket mà không qua heap. Khoảnh khắc bạn cần biến đổi byte (mã hoá, nén, kiểm tra), bạn mất transferTo và buộc phải chép dữ liệu qua heap để xử lý. Netty cũng vậy: FileRegion zero-copy của nó bị vô hiệu ngay khi bạn gắn SSL handler vào pipeline — đúng một lý do với Kafka.

Trong thế giới cơ sở dữ liệu, cùng cái bẫy đội lốt khác: một tính năng buộc đánh giá từng dòng sẽ phá đường tắt quét hàng loạt hay phục vụ trang trực tiếp. Một hàm bọc quanh cột trong mệnh đề WHERE khiến predicate mất tính sargable và index — đường tắt — không dùng được nữa; Row-Level Security bật lên buộc chạy hàm kiểm tra trên mỗi dòng, biến một lần quét rẻ thành đắt. Cùng một tính năng, chi phí nhỏ ở nơi vốn đã quét tuần tự, nặng ở nơi nó phá một đường tắt.

Mẫu số chung: một tính năng cắt ngang cần chạm vào từng byte — mã hoá, nén, kiểm tra, biến đổi — về bản chất không đội trời chung với các đường tắt zero-copy; nên chi phí của nó rất nhỏ ở nơi vốn không có đường tắt nào để mất (phía ghi đằng nào cũng qua JVM), và rất nặng ở nơi nó phá huỷ một đường tắt mà chỉ đường đó được hưởng (phía đọc vốn dùng sendfile). Nên câu hỏi đúng khi cân nhắc bật một tính năng như vậy không phải "nó làm chậm trung bình bao nhiêu phần trăm" mà là "nó có buộc dữ liệu phải đi qua vùng người dùng không, và đường nào trong hệ của tôi đang dựa vào zero-copy — vì đúng đường đó là chỗ tôi phải tính lại dung lượng, không phải con số trung bình".

Muốn tự kiểm cụm của bạn đang mở tới đâu, chạy thử:

# giao thức của từng listener
kafka-configs.sh --bootstrap-server kf:9092 --describe --entity-type brokers --entity-name 1 \
  | tr ',' '\n' | grep -iE "listener|security.protocol"

# có bật phân quyền không
kafka-acls.sh --bootstrap-server kf:9092 --list 2>&1 | head -3

Nếu lệnh thứ hai báo không có authorizer, mọi client kết nối được đều có toàn quyền — kể cả xoá topic.

Phần sau đo quy mô cụm: bao nhiêu broker, bao nhiêu partition, và khi nào cần thêm.