tu-huynh
Tu Huynh a Software Engineer guy
Blog

Protocol Buffer 101

Weekly Grokking Research wrote

Problem

Trong một hệ thống phân tán, nãy sinh một nhu cầu các hệ thống cần liên lạc với nhau thông qua network, do đó chúng ta cần một định dạng data format chung để chuẩn hoá việc giao tiếp. Chúng ta có vẻ khá quen thuộc với các data format phổ biến như JSON, XML, … khi chúng ta muốn giao tiếp với một hệ thống khác, với cách format human-readable text.

Vì là human-readable text format nên việc encode JSON, XML tốn khá nhiều bandwidth khi giao tiếp. Với một application nhỏ lượng dữ liệu truyền đi nhỏ thì không ảnh hưởng nhiều nhưng khi đến hàng terabyte thì việc tối ưu encoding sẽ giảm được rất nhiều việc tiêu tốn tài nguyên.

Từ đó Protocol Buffer ra đời để giải quyết bài toán này. Protobuf cung cấp một interface description language (IDL) để chúng ta define cấu trúc của dữ liệu mà chúng ta muốn truyền tải.

// point.proto
message Point {
  required int32 x = 1;
  required int32 y = 2;
  optional string label = 3;
}

Trên là cấu trúc của Point với toạ độ x, y và label. Mỗi attribute của Point được define với validator(required, optional), kiểu dữ liệu(int32, string), tên (x, y, label) và field tag(1, 2, 3).

Sau khi define bằng IDL trên, ta sẽ có một số thư viện để generate ra function cho từng ngôn ngữ và được gọi như một RPC.

Giờ chúng ta đã biết được cách protobuf define data structure cho việc giao tiếp vậy cách mà protobuf tối ưu việc encoding để tối ưu như thế nào.

Encoding

Chúng ta sẽ so sánh cách encoding của JSON và protobuf để xem cách mà protobuf tối ưu việc encoding.

JSON trải qua một quá trình được nhiều người tối ưu và tìm cách encoding thành binary để tối ưu. Cách JSON được encode thành binary format bởi một thư viện là Message Pack:

{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

proto1

Chúng ta thấy rằng một attribute được encode bao gồm độ dài, data type, các tên các attribute được encode theo ASCII. Từ đó ta có được kết quả sau khi encoding là 66 byte dữ liệu

Vì protobuf chúng ta có một điều bắt buộc và chúng ta phải define proto file để nói lên data structure chúng ta muốn truyền tải, còn JSON việc encoding không bắt buộc ta làm điều đó. Chính việc define đó đã giúp protobuf tối ưu việc encoding. Hãy xem cách protobuf encode cấu trúc dữ liệu.

// person.proto
message Person {
    required string user_name       = 1;
    optional int64  favorite_number = 2;
    repeated string interests       = 3;
}

proto2

Bằng cách sử dụng field tag chúng ta thay thế việc encode trực tiếp attribute name. Do field tag đôc lập với attribute name nên việc attribute name làm tốn tài nguyên sẽ không còn nữa. Kết quả cho ta thấy encoded data giảm từ 66 byte xuống còn 33 byte.

Backward & forward compatibility

Có một khái niệm gọi là schema evolution, muốn nói là schema thay đổi thường xuyên trong quá trình phát triển phần mềm. Mà đối với một hệ thống phân tán, việc tồn tại cả code cũ và mới cùng lúc trong hệ thống là một thực tế. Vậy khi sử dụng protobuf làm cách nào để chúng ta đảm bảo việc code mới encode data của code cũ encode (backward compatibility) và code cũ encode data của code mới encode (forward compatibility).

  • Khi ta muốn thay đổi một attribute nanme việc này không ảnh hưởng đến việc encoding, vì chúng ta chỉ sử dụng field tag để encode, nên đảm bảo cả backward và forward compatibility. Nhưng vì vậy việc thay đổi field tag là một điều nên tránh vì nó sẽ làm code cũ không thể encode data được encode bởi code mới.
  • Khi chúng ta thêm một field mới, với một field tag mới, khi code cũ encode đến field tag đó vì nó ko tồn tại trong schema của nó nên nó sẽ skip attribute đó dựa trên chiều dài của attribute đó được mô tả trong data. Việc này đảm bảo cho ta tính chất forward compatibility. Còn khi code mới encode data từ code cũ, chúng cũng sẽ bỏ qua nếu field tag đó không tồn tại, nên đảm bảo backward compatibility, nhưng có một chú ý là không nên sử dụng required validation vì code mới chưa có field này, sẽ làm vi phạm trong việc validation.
  • Việc remove tương tự, nhưng phải đảm bảo không remove required field vì cũng sẽ làm fail validation ở code cũ.
  • Khi thay đổi data type, việc code cũ hay code mới encode data của nhau sẽ gặp nhiều vấn đề khi chúng không thể hiểu nhau hoặc có thế làm việc encode bị sai. Chúng ta nên cẩn thận trong việc này, có thể tạo một field mới với kiểu dữ liệu mới.

Usecase

Protobuf thường được dùng trong việc giao tiếp trong nội bộ (internal microservice) trong khi việc giao tiếp với hệ thống ngoài thường được sử dụng JSON, XML hay một số data format tương tự do việc nó là human-readable và khá phổ biến.

References

  1. Wikipedia: https://en.wikipedia.org/wiki/Protocol_Buffers
  2. Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems - Chương 4. Encoding