38 điểm bởi GN⁺ 2025-12-05 | 10 bình luận | Chia sẻ qua WhatsApp
  • 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ặttự độ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

  • đị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

  • Protobuf định nghĩa rõ ràng cấu trúc dữ liệu thông qua file .proto
    • Mỗi trường có kiểu nghiêm ngặt, định danh sốtên cố định
  • Ví dụ:
    message User {
      int32 id = 1;
      string name = 2;
      string email = 3;
      bool isActive = 4;
    }
    
  • Lệnh protoc hỗ trợ tự động sinh mã cho nhiều ngôn ngữ như Dart, TypeScript, Kotlin, Swift, C#, Go, Rust
  • Với mã được tạo ra, có thể tuần tự hóa (writeToBuffer) và giải tuần tự hóa (fromBuffer)không cần tự parse hay kiểm tra thủ công
  • Kết quả là vừa tiết kiệm thời gian vừa nâng cao khả năng bảo trì

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 độngcả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 httpuser.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

 
GN⁺ 2025-12-05
Ý 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. Trong proto3, 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 JSON

    • Blog đó có khá nhiều ngộ nhận tương tự. Ví dụ, trong bài phản đối dùng SVG, tác giả không tính đến ưu điểm là định dạng vector có thể scale tự do
    • Cốt lõi của vấn đề chỉ là sự khác biệt về ngôn ngữ và cách triển khai client/server. Tôi đang dùng framework Gooey ở phía client, tận dụng khái niệm Marshalling của Go. Nếu vượt qua được các giới hạn của Go thì có thể dùng rất type-safe. Tuy nhiên, điều quan trọng là phải chặn trường private bằng json:"-". Bạn có thể xem dự án của tôi tại Gooey
    • Bài này đang nhầm lẫn giữa định dạng tuần tự hóa và khái niệm hợp đồng (contract)
    • Trong các hệ thống mạng, vấn đề lệch dữ liệu (skew) luôn tồn tại bất kể cách mã hóa là gì. Tuy vậy, Protobuf cung cấp đối tượng kiểu tĩnh sau khi giải mã. JSON cũng có thể được kiểm tra tính hợp lệ, nhưng hầu hết mọi người không làm vậy. Kết quả là các đối tượng JSON bị biến đổi qua lại và không ai còn chắc chắn về cấu trúc nội bộ của chúng
    • Có lẽ tác giả bài gốc chỉ muốn nói rằng trong Protobuf, các trường bị thiếu sẽ được khởi tạo bằng giá trị mặc định. Điều này khác với khái niệm trường “required”
  • JSON đượ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

    • Đúng vậy. Phần lớn kiến trúc sư sẽ không cân nhắc proto nếu không có nhu cầu rõ ràng như gRPC. JSON sẽ không bị thay thế cho đến khi có một lựa chọn cho phép debug trực tiếp bằng console.log()
    • Khả năng debug cũng là điểm mạnh của JSON. Chỉ cần mở ra là đọc được. Trong khi đó, Protobuf cần đến tooling
    • Đúng thế. Nhưng mọi người thường chọn không bỏ thêm 15 phút ở giai đoạn thiết kế, để rồi sau đó mất 3 tháng lần lại vấn đề
    • JSON có thể sẽ không biến mất hoàn toàn như COBOL, nhưng với dự án mới thì chẳng có lý do gì phải dùng nó
  • 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ủa protobuf2. 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ểu message

    • Dù là JSON hay Protobuf, chỉ an toàn khi kiểm tra tương thích phiên bản được ép buộc trong pipeline CI
    • Không có hệ thống kiểu nào đi qua mạng mà không bị phá vỡ
  • Bà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

    • Tôi cũng đã thử nhiều định dạng nhị phân, nhưng cuối cùng JSON đã gzip vẫn vượt trội về hiệu quả
    • Điểm yếu của JSON là tốc độ tuần tự hóa/giải tuần tự hóa. Còn lại thì có thể giải quyết dần dần
    • JSON/HTML dùng Brotli hoặc zstd theo kiểu streaming cũng đáng cân nhắc. Có thể tận dụng cửa sổ nén trong khi giữ kết nối mở
    • Tham khảo liên quan: Bài viết so sánh hiệu năng Protobuf của Auth0
    • Sự kết hợp giữa JSON và mod_deflate tạo ra khác biệt cảm nhận rất lớn
  • 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

    • Tuy nhiên, schema nghiêm ngặt của Protobuf đôi khi lại là ưu điểm. Phần lớn API không công khai JSON schema. Tôi đã từng xác thực bằng ajv hoặc superstruct, nhưng với Protobuf thì không cần
    • Sẽ thật tốt nếu trình duyệt hỗ trợ trực tiếp API CBOR. Phần triển khai nội bộ đã có sẵn nên chắc cũng không quá khó
  • 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

    • ASN.1 có quá nhiều tính năng. Nếu hỗ trợ tất cả thì thư viện sẽ phức tạp quá mức, còn nếu chỉ hỗ trợ một phần thì lại không còn là ASN.1 chuẩn nữa
    • Tôi thích ASN.1 DER. Tôi đã tự triển khai encoder/decoder DER bằng C và phát hành FOSS. Tôi còn tạo một bản mở rộng “ASN.1X” để bao trùm hoàn toàn mô hình dữ liệu của JSON
    • Nhưng trong các hệ thống như SNMP, sự linh hoạt quá mức của ASN.1 lại là vấn đề. Mỗi nhà sản xuất mở rộng theo kiểu riêng của họ
    • Ngay trong nội bộ Google, việc tuần tự hóa/giải tuần tự hóa Protobuf cũng tiêu tốn nhiều CPU
    • ASN.1 bị thiết kế quá tay (overengineered) nên khó hỗ trợ. Những tính năng như kế thừa là không cần thiết
  • 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

    • Tính dễ đọc và dễ debug của JSON không thể bị xem nhẹ. Hầu hết các team chọn JSON vì hiệu quả ngắn hạn
    • Tôi tò mò không biết đã gặp những vấn đề gì. Theo kinh nghiệm của tôi, so với sự bất tiện của Protobuf thì rủi ro hỏng dữ liệu của JSON còn lớn hơn. Protobuf sẽ bị bắt bằng lỗi biên dịch, còn JSON thì nổ trong production
  • 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

    • Nhưng trên thực tế, zero-copy đôi khi còn chậm hơn. Việc sao chép trong cache gần như miễn phí, còn xử lý trực tiếp cấu trúc động lại tạo ra overhead. Trong hầu hết trường hợp, chỉ cần sao chép một lần (one-copy) là đủ
    • Đây chủ yếu là luận điểm từ marketing của Cap’n Proto; trên thực tế khác biệt hiệu năng là rất nhỏ. Cả hai định dạng đều cần chuyển đổi giữa kiểu native và nhị phân. Tùy payload mà hiệu năng sẽ tương đương
    • Đây có thể không phải vấn đề của định dạng mà là vấn đề ở phần triển khai thư viện
  • Trong một dự án NodeJS, tôi đã định nghĩa toàn bộ API bằng .proto và 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

    • Với mục đích này, Twirp sẽ phù hợp. Nó xử lý Protobuf hoặc JSON trên HTTP thuần túy
    • ConnectRPC cũng cung cấp cách tiếp cận tương tự. Tuy vậy, phạm vi hỗ trợ vẫn còn chưa rõ ràng
  • Đị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

    • Bên trong Google đã có sẵn một hệ sinh thái Protobuf nhúng schema như vậy. Có thể tham khảo Riegeli
    • Avro hoặc Yardl cũng cung cấp cách tiếp cận tương tự
    • Nhưng trong dịch vụ web, đôi khi lại là trường hợp schema 200KB còn message chỉ 1KB. Khi đó sẽ kém hiệu quả
    • Avro vẫn là một lựa chọn thay thế tốt
 
tested 2025-12-09
 
onixboox 2025-12-08

https://msgpack.org/ cái này thì sao?

 
cosine20 2025-12-08

MessagePack cũng rất ổn.

 
savvykang 2025-12-06

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.

 
vipeen 2025-12-06

"Việc gỡ lỗi thì khó hơn"

Loại

 
jjw9512151 2025-12-05

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.

 
ifmkl 2025-12-05

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

 
click 2025-12-06

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.

 
bakyeono 2025-12-05
  • Định dạng nhị phân tôi mơ ước là loại dựa trên schema nhưng vẫn bao gồm schema bên 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 schema 1KB vào message 2GB không phải là gánh nặng lớn
  • Nhưng trong dịch vụ web thì ngược lại, thường có trường hợp schema 200KB còn message chỉ 1KB. Khi đó sẽ kém hiệu quả

=> 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.