Lời khuyên phổ biến là "đừng dùng JSON trên Kafka, dùng Avro". Bài này đo cả ba định dạng trên cùng một tập dữ liệu, và con số quan trọng nhất chỉ xuất hiện sau khi bật nén.

Kích thước và tốc độ thô, rồi sau khi nén

Dữ liệu và cách đo

200.000 bản ghi đơn hàng, tám trường, tương tự dữ liệu thật:

{"orderId":"ORD-000000","customerId":"CUST-00000","status":"PENDING",
 "currency":"VND","amount":0,"warehouse":"HCM-01",
 "createdAt":"2026-08-30T10:01:07Z","channel":"WEB"}

Đo ba thứ cho mỗi định dạng: kích thước, thời gian mã hoá, thời gian giải mã.

Kết quả thô

Byte mỗi tin Mã hoá Giải mã
JSON 174 147 ms 190 ms
Avro 69 231 ms 63 ms
Protobuf 77 304 ms 82 ms

Hai điều bất ngờ.

Avro nhỏ hơn JSON 2,5 lần — đúng như quảng cáo. Nó bỏ hẳn tên trường (lược đồ nằm ở chỗ khác), dùng số nguyên biến đổi độ dài, không có dấu ngoặc hay dấu phẩy.

Nhưng mã hoá Avro chậm hơn JSON 1,6 lần. Điều này ngược với hình dung "định dạng nhị phân thì nhanh hơn". Jackson đã được tối ưu suốt mười lăm năm cho đúng một việc, và nó làm việc đó rất tốt.

Giải mã thì đảo ngược: Avro nhanh gấp 3 lần, Protobuf gấp 2,3 lần. Phân tích JSON phải quét từng ký tự, tìm dấu ngoặc kép, khớp tên trường; định dạng nhị phân thì đọc thẳng theo vị trí.

Với Kafka, giải mã thường xảy ra nhiều hơn mã hoá — một tin ghi một lần nhưng có thể được nhiều nhóm consumer đọc. Nên cán cân nghiêng về phía nhị phân, nhưng không nhiều như bảng thô gợi ý.

Nhưng Kafka nén dữ liệu

Đây là chỗ bảng trên gây hiểu nhầm. Phần 5 đã đo: lz4 giữ 99% thông lượng mà nhỏ đi gần bảy lần, nên gần như mọi cụm sản xuất đều bật nén.

Đo lại sau khi nén:

Thô Sau gzip Tỉ lệ nén
JSON 34.774.858 2.068.348 16,8×
Avro 13.798.133 1.496.243 9,2×
Protobuf 15.396.262 1.464.833 10,5×

Và tỉ lệ giữa các định dạng:

Trước nén Sau nén
Avro / JSON 0,40 0,72
Protobuf / JSON 0,44 0,71

Lợi thế 2,5 lần co xuống còn 1,4 lần.

Lý do rõ ràng khi nhìn vào tỉ lệ nén: JSON nén được 16,8 lần còn Avro chỉ 9,2 lần. JSON lặp lại tên trường ở mọi bản ghi — "orderId", "customerId", "status" xuất hiện 200.000 lần — và đó chính xác là thứ thuật toán nén giỏi nhất. Avro đã bỏ tên trường từ trước, nên còn ít thứ dư thừa để nén.

Nói cách khác: nén làm phần lớn công việc mà Avro được ca ngợi vì đã làm. Và bạn dùng nén rồi.

Vậy lập luận về kích thước còn lại gì

Vẫn còn 1,4 lần, và với cụm hàng terabyte thì 30% đĩa không phải con số nhỏ. Nhưng nó không còn là lý do quyết định.

Lý do thật để chọn Avro hay Protobuf nằm ở chỗ khác: ép buộc lược đồ. Phần 22 đã đo một tin không đọc nổi khiến consumer ném hơn tám triệu ngoại lệ và khoá cứng partition. Đó là chuyện xảy ra khi không có gì kiểm tra lược đồ trước khi dữ liệu vào topic.

Avro cộng với một cơ quan đăng ký lược đồ chuyển việc kiểm tra đó sang lúc triển khai: producer đăng ký lược đồ mới, registry so với lược đồ cũ theo quy tắc tương thích, và từ chối nếu phá vỡ. Sự cố xảy ra ở giai đoạn triển khai, nơi bạn có thể lùi lại, thay vì ở giai đoạn chạy, nơi partition đã kẹt.

JSON cũng có thể làm vậy với JSON Schema, nhưng hệ sinh thái quanh Kafka mỏng hơn nhiều.

Chọn thế nào

JSON — một đội, ít dịch vụ, và bạn cần đọc được dữ liệu bằng mắt khi gỡ lỗi. kafka-console-consumer in ra thứ người đọc hiểu ngay, không cần công cụ gì thêm. Kém 1,4 lần về đĩa sau nén, và đó thường là cái giá chấp nhận được để đổi lấy việc gỡ lỗi dễ.

Avro — nhiều đội đọc chung một topic, và bạn cần một cơ quan đăng ký ép tương thích. Lược đồ tách rời khỏi dữ liệu, hệ sinh thái quanh Kafka đầy đủ nhất, và đây là lựa chọn mặc định của phần lớn hệ thống lớn.

Protobuf — bạn đã dùng gRPC ở nơi khác và muốn một định dạng cho cả hai. Kích thước và tốc độ tương đương Avro; khác biệt chính là lược đồ được biên dịch thành mã thay vì đọc lúc chạy.

Điều không nên làm: đổi từ JSON sang Avro chỉ vì lý do kích thước. Phép đo ở trên cho thấy lợi ích thật là 30% đĩa, còn cái giá là thêm một dịch vụ phải vận hành và một bước sinh mã trong quy trình xây dựng. Nếu bạn không cần ép tương thích, hãy bật nén và giữ JSON.

Ghi chú về phép đo

Tôi dùng Avro GenericRecordProtobuf DynamicMessage — hai đường đi chậm nhất của cả hai thư viện, vì chúng tra tên trường lúc chạy. Với lớp được sinh sẵn từ lược đồ, con số mã hoá sẽ tốt hơn đáng kể.

Tôi chọn đường đi này vì nó không cần bước sinh mã, nên phép đo lặp lại được bằng một lệnh. Tỉ lệ kích thước — con số quan trọng nhất trong bài — không phụ thuộc vào đường đi nào, vì byte ra là như nhau.

Nếu bạn định chọn dựa trên tốc độ, hãy đo lại bằng lớp sinh sẵn trên dữ liệu của chính bạn.

Thử ba mươi giây

So kích thước trên chính dữ liệu của bạn, không cần thư viện nào:

# lấy 10.000 bản ghi thật
kafka-console-consumer.sh --bootstrap-server kf:9092 --topic topic-cua-ban \
  --from-beginning --max-messages 10000 --timeout-ms 30000 > /tmp/mau.json

# thô và sau khi nén
wc -c < /tmp/mau.json
gzip -c /tmp/mau.json | wc -c

Chia hai số. Nếu tỉ lệ nén của bạn trên 10 lần, phần lớn kích thước đang là tên trường lặp lại — và đó chính là phần mà nén đã xử lý hộ bạn rồi.

Phần sau đo hàng đợi tin chết: thử lại tại chỗ chậm hơn 25 lần khi tỉ lệ hỏng lên 50%.