- JSON, tiêu chuẩn đã trở thành mặc định cho web API, dễ đọc và linh hoạt nhưng có những giới hạn về hiệu năng và độ ổn định
- Protobuf (Protocol Buffers) bảo đảm rõ ràng cấu trúc dữ liệu thông qua định nghĩa kiểu nghiêm ngặt và tự động sinh mã
- Nhờ dùng tuần tự hóa nhị phân, Protobuf có thể giảm kích thước dữ liệu hơn khoảng 3 lần và cải thiện tốc độ truyền tải so với JSON
- Server và client cùng chia sẻ một schema
.proto, nên không cần lo sai lệch kiểu dữ liệu hay phải kiểm tra thủ công
- Dù việc debug khó hơn, Protobuf phù hợp hơn với API hiện đại ở các khía cạnh hiệu năng, khả năng bảo trì và hiệu quả phát triển
Tính phổ biến và giới hạn của JSON
- JSON là định dạng văn bản dễ đọc với con người, nên chỉ với
console.log() cũng có thể kiểm tra dữ liệu
- Nhờ khả năng tích hợp hoàn hảo với web, nó được chấp nhận rộng rãi trong JavaScript và các framework backend
- Nó mang lại tính linh hoạt với việc tự do thêm/xóa trường và thay đổi kiểu, nhưng vì vậy cũng có thể dẫn đến lệch cấu trúc hoặc phát sinh lỗi
- Hệ sinh thái công cụ phong phú giúp có thể xử lý dễ dàng chỉ với trình soạn thảo văn bản hay
curl
- Tuy vậy, dù có những ưu điểm này, vẫn tồn tại các lựa chọn tốt hơn về hiệu năng và độ an toàn kiểu dữ liệu
Tổng quan về Protobuf
- Là định dạng tuần tự hóa nhị phân do Google phát triển năm 2001 và công bố năm 2008
- Được sử dụng rộng rãi trong hệ thống nội bộ và giao tiếp giữa các microservice
- Nhiều người thường hiểu lầm rằng phải dùng cùng gRPC, nhưng Protobuf vẫn có thể được dùng độc lập trong HTTP API
- Ban đầu, tính khó quan sát của định dạng nhị phân khiến nó kém thân thiện hơn, nhưng nó có thế mạnh lớn về hiệu quả và độ ổn định
Hệ thống kiểu mạnh và sinh mã tự động
Hiệu quả của tuần tự hóa nhị phân
- Protobuf được tuần tự hóa dưới dạng dữ liệu nhị phân thay vì văn bản, nên rất gọn và nhanh
- So sánh kích thước của cùng một dữ liệu (đối tượng
User):
- JSON: 86 byte (68 byte nếu bỏ khoảng trắng)
- Protobuf: 30 byte
- Nguyên nhân tạo nên hiệu quả:
- Dùng mã hóa varint cho số
- Dùng thẻ số thay cho khóa văn bản
- Loại bỏ khoảng trắng và cú pháp không cần thiết
- Tối ưu hóa trường tùy chọn
- Kết quả là giúp giảm băng thông, tăng tốc độ phản hồi, tiết kiệm dữ liệu di động và cải thiện trải nghiệm người dùng
Ví dụ API Protobuf dựa trên Dart
- Dùng package
shelf để dựng một HTTP server đơn giản và trả về đối tượng User dưới dạng Protobuf
- Điểm chính trong mã server:
- Tạo đối tượng
User() rồi tuần tự hóa bằng writeToBuffer()
- Chỉ định
'content-type': 'application/protobuf' trong header phản hồi
- Client dùng package
http và user.pb.dart để giải mã trực tiếp dữ liệu Protobuf
- Vì server và client cùng chia sẻ một schema
.proto, nên không xảy ra sai lệch cấu trúc dữ liệu
- Cách làm tương tự cũng áp dụng giống hệt trong Go, Rust, Kotlin, Swift, C#, TypeScript
Những ưu điểm còn lại của JSON
- Với Protobuf, khó diễn giải ý nghĩa nếu không có schema
- Do chỉ hiển thị định danh số thay vì tên trường, nên con người khó đọc hơn
- So sánh ví dụ:
- JSON:
{ "id": 42, "name": "Alice" }
- Protobuf:
1: 42, 2: "Alice"
- Vì vậy, với Protobuf:
- Cần công cụ giải mã chuyên dụng
- Bắt buộc quản lý schema và version
- Dù vậy, lợi ích về hiệu năng và hiệu quả vẫn lớn hơn rất nhiều
Kết luận
- Protobuf là công nghệ tuần tự hóa trưởng thành và hiệu năng cao, hoàn toàn có thể dùng cho API công khai
- Nó vẫn hoạt động độc lập trong HTTP API thông thường mà không cần gRPC
- Là công cụ giúp cải thiện đồng thời hiệu năng, độ vững chắc, giảm lỗi và hiệu quả phát triển
- Với các dự án thế hệ tiếp theo, việc áp dụng Protobuf là hoàn toàn đáng cân nhắc
10 bình luận
Ý kiến Hacker News
JSON thường dẫn tới việc gửi dữ liệu mơ hồ hoặc không được đảm bảo. Nhiều vấn đề có thể phát sinh như thiếu trường, sai kiểu, gõ nhầm khóa, cấu trúc không được tài liệu hóa. Nhưng đã có bài viết cho rằng Protobuf khiến những điều này trở nên bất khả thi bằng cách định nghĩa rõ cấu trúc message bằng tệp
.proto. Tuy nhiên, đó là sự hiểu sai về triết lý của Protobuf. Trongproto3, trường required hoàn toàn không được hỗ trợ. Tài liệu chính thức (Protobuf Best Practices) cũng nêu rõ rằng “required fields are considered harmful and were removed”. Rốt cuộc, client Protobuf cũng phải được viết theo hướng phòng thủ giống như API JSONjson:"-". Bạn có thể xem dự án của tôi tại GooeyJSON được nén vẫn hoàn toàn dùng tốt, và chi phí giao tiếp ban đầu thấp. Dĩ nhiên sẽ có vấn đề nếu thiếu trường hoặc kiểu bị thay đổi, nhưng những người cố thiết kế cấu trúc được gõ kiểu hoàn hảo và lập quy trình đồng bộ phiên bản hầu hết đều thất bại. Cuối cùng, phương án có chi phí cho con người thấp hơn sẽ thắng. Vì vậy JSON sẽ không biến mất cho đến khi có một lựa chọn thay thế với chi phí giao tiếp giữa con người còn thấp hơn
console.log()Protobuf không hoàn hảo. Khi server và client được triển khai ở các thời điểm khác nhau và phiên bản spec không khớp, tính an toàn sẽ bị phá vỡ. Có thể giảm nhẹ bằng cách không tái sử dụng ID, sao chép unknown-field, v.v., nhưng hệ thống phân tán về bản chất vốn đã phức tạp. Dù vậy,
protobuf3đã giải quyết nhiều vấn đề củaprotobuf2. Trước đây không thể phân biệt giữa giá trị mặc định được thiết lập và trường bị thiếu, còn giờ có thể xử lý bằng cách dùng kiểumessageBài viết gọi nó là “siêu hiệu quả”, nhưng lại không nhắc đến gzip. Phần lớn dữ liệu văn bản vốn đã được truyền đi với nén tự động. Vì vậy, Protobuf nên được so sánh với JSON đã gzip
Việc ủng hộ giao thức tốt hơn là điều tích cực, nhưng khó có thể nói Protobuf thay thế JSON cả về hiệu quả lẫn khả năng sử dụng. Protobuf bỏ lỡ những lĩnh vực mà JSON làm tốt vì schema quá nghiêm ngặt. Thậm chí CBOR còn phù hợp hơn để thay thế JSON. CBOR linh hoạt như JSON nhưng có cách mã hóa gọn hơn
ASN.1 từ năm 1984 đã làm được những gì Protobuf đang làm, thậm chí còn linh hoạt hơn. Nếu dùng mã hóa DER thì cũng không tệ đến vậy. Có thể xem ví dụ ASN.1 DER. Protobuf quá phức tạp so với những gì nó đạt được
Tôi đã xây cả một hệ thống production bằng Protobuf, và việc quản lý nó thực sự rất đau đớn. Về mặt kỹ thuật thì nghe có vẻ hay, nhưng trong thực tế JSON đơn giản hơn nhiều
Protobuf rất tuyệt, nhưng đáng tiếc là không hỗ trợ zero-copy. Những định dạng như Cap’n Proto có thể loại bỏ nút thắt tuần tự hóa/giải tuần tự hóa
Trong một dự án NodeJS, tôi đã định nghĩa toàn bộ API bằng
.protovà xây một server trả về proto hoặc JSON tùy theo Content-Type. Nó có cấu trúc hơn nhiều so với Swagger. Tuy nhiên tôi vẫn thấy tiếc vì Google không cung cấp tính năng này dưới dạng thư viện chính thức. gRPC lại bất tiện vì phụ thuộc vào HTTP/2. Nhân tiện, tôi nghĩ Text proto là ngôn ngữ cấu hình tĩnh tốt nhấtĐịnh dạng nhị phân trong mơ của tôi là loại dựa trên schema nhưng cũng nhúng schema vào trong message. Làm vậy thì có thể đọc ngay bằng plugin vim. Khi xử lý hàng triệu đối tượng, việc gắn thêm 1KB schema vào message 2GB không phải gánh nặng lớn
TypeSpec
https://typespec.io/
https://msgpack.org/ cái này thì sao?
MessagePack cũng rất ổn.
Tôi cho rằng việc khẳng định một định dạng là đã trưởng thành trong khi thậm chí còn không có bộ giải mã chính thức phục vụ gỡ lỗi là mâu thuẫn.
"Việc gỡ lỗi thì khó hơn"
Loại
Giống như mọi công cụ khác, không có gì là vạn năng, nhưng tôi nghĩ Protobuf cũng là một công cụ đủ tốt.
Đặc biệt, đã có lúc tôi phải bắn dữ liệu dung lượng lớn với tần suất cao (20 lần/giây) tới nhiều ngôn ngữ phía khách hàng trong môi trường nhúng, và khi đó tôi đã xử lý gọn gàng bằng nanopb.
Làm chặt chẽ như vậy thì có phải cuối cùng sẽ quay sang dùng XML không ạ haha
Nếu lược đồ cũng được định nghĩa bằng DTD và phía parser có cache thì cũng sẽ có hiệu quả là lược đồ chỉ cần được truyền một lần.
=> Dù sao thì schema chẳng phải cũng nhất định phải được truyền đi ít nhất một lần sao? Ngay cả với JSON, không phải là không có schema, mà nó được chứa ngầm trong dữ liệu, nên có vẻ cũng không phải là không truyền schema. Ngược lại, vì schema bị truyền lặp lại ở từng mục một nên còn kém hiệu quả hơn. “Dạng dựa trên schema nhưng vẫn bao gồm schema trong message” có vẻ khá ổn đấy.