Chương 4 — Encoding and Evolution
🎯 Mục tiêu chương: Hiểu cách dữ liệu được chuyển từ cấu trúc in-memory thành chuỗi byte (encoding) và các format phổ biến (JSON/XML, Thrift, Protocol Buffers, Avro) xử lý schema evolution ra sao. Nắm khái niệm backward/forward compatibility và cách chúng áp dụng cho ba kiểu dataflow: qua database, qua service (REST/RPC), và qua message passing — nền tảng để làm rolling upgrade không downtime.
Vì sao encoding và evolution quan trọng
Ứng dụng luôn thay đổi: thêm feature, hiểu rõ hơn yêu cầu, business đổi. Thay đổi feature thường kéo theo thay đổi dữ liệu (thêm field, thêm loại record, trình bày khác). Chương 1 gọi đây là evolvability.
- Relational DB: tại mỗi thời điểm chỉ có một schema (đổi bằng migration
ALTER). - Schema-on-read (schemaless): DB chứa lẫn dữ liệu format cũ và mới.
Nhưng code không đổi ngay lập tức được:
- Server-side: dùng rolling upgrade (staged rollout) — deploy bản mới lên vài node một lúc, kiểm tra ổn rồi mới lan dần → không downtime, deploy thường xuyên hơn.
- Client-side: phụ thuộc người dùng, họ có thể không cập nhật app rất lâu.
→ Code cũ, code mới, dữ liệu cũ, dữ liệu mới cùng tồn tại. Cần tương thích hai chiều:
| Khái niệm | Định nghĩa | Độ khó |
|---|---|---|
| Backward compatibility | Code mới đọc được dữ liệu do code cũ ghi | Thường dễ: người viết code mới biết format cũ, có thể xử lý tường minh |
| Forward compatibility | Code cũ đọc được dữ liệu do code mới ghi | Khó hơn: code cũ phải biết bỏ qua những gì phiên bản mới thêm vào |
Formats for Encoding Data
Chương trình làm việc với dữ liệu ở (ít nhất) hai dạng:
- In-memory: object, struct, list, array, hash table, tree... tối ưu cho CPU truy cập, dùng pointer.
- Chuỗi byte tự chứa (self-contained) khi ghi file hoặc gửi qua mạng (ví dụ JSON) — pointer vô nghĩa với process khác.
Chuyển in-memory → byte gọi là encoding (serialization, marshalling); ngược lại là decoding (parsing, deserialization, unmarshalling). Sách dùng từ encoding vì serialization trùng nghĩa với serializable isolation trong transaction (Chương 7). Encoding cũng không liên quan tới encryption.
Language-Specific Formats
Ví dụ: Java java.io.Serializable, Ruby Marshal, Python pickle, thư viện bên thứ ba như Kryo (Java). Tiện vì lưu/khôi phục object với rất ít code, nhưng có vấn đề sâu:
- Gắn chặt với một ngôn ngữ: đọc từ ngôn ngữ khác rất khó → bị khoá vào ngôn ngữ hiện tại lâu dài và khó tích hợp với tổ chức khác.
- Bảo mật: để khôi phục đúng kiểu object, decoder phải có khả năng khởi tạo class tuỳ ý → attacker gửi byte sequence độc hại có thể dẫn tới remote code execution.
- Versioning bị xem nhẹ: forward/backward compatibility thường bị bỏ qua.
- Hiệu năng bị xem nhẹ: Java serialization nổi tiếng chậm và encoding phình to.
→ Chỉ dùng cho mục đích rất tạm thời.
JSON, XML, and Binary Variants
JSON, XML (và CSV) là các format chuẩn, đa ngôn ngữ, textual nên người đọc được. XML bị chê dài dòng và phức tạp; JSON phổ biến nhờ trình duyệt hỗ trợ sẵn (subset của JavaScript) và đơn giản hơn XML. Các vấn đề tinh tế:
- Mơ hồ về số: XML và CSV không phân biệt số với chuỗi toàn chữ số (trừ khi có schema ngoài). JSON phân biệt string/number nhưng không phân biệt integer và float, không quy định precision. Số nguyên lớn hơn 2^53 không biểu diễn chính xác bằng IEEE 754 double → sai khi parse trong JavaScript. Ví dụ Twitter: tweet ID là số 64-bit, nên API trả ID hai lần — một lần dạng number, một lần dạng string (
idvàid_str). - Không hỗ trợ binary string: phải encode binary bằng Base64 — hacky và tăng kích thước 33%.
- Schema là tuỳ chọn: XML Schema và JSON Schema mạnh nhưng phức tạp. Nhiều tool JSON không dùng schema → phải hardcode logic encode/decode (ví dụ biết field nào là Base64).
- CSV không có schema: ứng dụng tự định nghĩa ý nghĩa cột; thêm cột phải xử lý thủ công; escaping (giá trị chứa dấu phẩy, xuống dòng) có spec nhưng không phải parser nào cũng làm đúng.
Dù vậy chúng vẫn "đủ tốt" và sẽ còn phổ biến, nhất là làm data interchange format giữa các tổ chức — ở đó việc thống nhất được bất cứ thứ gì khó hơn nhiều so với chuyện đẹp hay hiệu quả.
Binary encoding
Dữ liệu dùng nội bộ thì có thể chọn format gọn hơn, parse nhanh hơn. Dataset nhỏ thì lợi ích không đáng kể, nhưng ở mức terabyte thì format ảnh hưởng lớn.
- Có hàng loạt binary encoding cho JSON (MessagePack, BSON, BJSON, UBJSON, BISON, Smile) và XML (WBXML, Fast Infoset), nhưng không cái nào phổ biến như bản text.
- Chúng giữ nguyên data model JSON/XML (có thể thêm kiểu như int vs float, binary string), và vì không có schema nên vẫn phải nhúng tên field vào dữ liệu.
Record ví dụ xuyên suốt chương:
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
Với MessagePack:
- Byte đầu
0x83: object (4 bit cao =0x80) có 3 field (4 bit thấp =0x03). Hơn 15 field thì dùng type indicator khác và số field chiếm 2 hoặc 4 byte. - Byte
0xa8: string (4 bit cao0xa0) dài 8 byte (0x08), tiếp theo là 8 byte ASCIIuserName— nhờ length prefix nên không cần ký tự kết thúc hay escaping. 0xa6+ 6 byteMartin, v.v.
Kết quả: 66 byte, so với 81 byte của JSON text (bỏ whitespace). Giảm ít như vậy liệu có đáng đánh đổi khả năng đọc bằng mắt? Các format có schema dưới đây làm được 32 byte.
Thrift and Protocol Buffers
Apache Thrift (gốc từ Facebook) và Protocol Buffers/protobuf (gốc từ Google) cùng nguyên lý, open source khoảng 2007–08. Cả hai bắt buộc có schema, định nghĩa bằng IDL:
// Thrift IDL
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
// Protocol Buffers
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}
Mỗi bên có code generation tool sinh class từ schema cho nhiều ngôn ngữ; ứng dụng gọi code sinh ra để encode/decode.
Thrift có hai binary format chính (thực ra ba, thêm DenseProtocol chỉ có ở C++, cộng hai format dạng JSON):
| Format | Kích thước record ví dụ | Cách làm gọn |
|---|---|---|
| JSON text (bỏ whitespace) | 81 byte | Không có — lưu cả tên field |
| MessagePack | 66 byte | Binary nhưng vẫn lưu tên field |
| Thrift BinaryProtocol | 59 byte | Thay tên field bằng field tag (1, 2, 3) + type annotation |
| Thrift CompactProtocol | 34 byte | Gộp field type + tag vào 1 byte, dùng variable-length integer |
| Protocol Buffers | 33 byte | Tương tự CompactProtocol, bit packing hơi khác |
| Avro | 32 byte | Không có tag, không có type — chỉ nối value theo thứ tự schema |
Điểm khác cốt lõi so với MessagePack: dữ liệu encode không chứa tên field mà chứa field tag — con số trong schema, như alias ngắn gọn cho field.
Variable-length integer (varint): thay vì 8 byte cho số 1337, chỉ dùng 2 byte; bit cao nhất của mỗi byte báo còn byte tiếp theo hay không. Số từ –64 đến 63 chiếm 1 byte, –8192 đến 8191 chiếm 2 byte, số lớn hơn thì nhiều byte hơn.
Minh hoạ (khái niệm) một field trong protobuf:
field 1 (user_name, string): [tag=1 | type=length-delimited] [len=6] 'M' 'a' 'r' 't' 'i' 'n'
field 2 (favorite_number): [tag=2 | type=varint] [0xb9 0x0a] ← 1337 dạng varint 2 byte
field 3 (interests, repeated):[tag=3 | type=length-delimited] [len=11] "daydreaming"
[tag=3 | type=length-delimited] [len=7] "hacking"
required/optional không ảnh hưởng cách encode — không có gì trong binary cho biết field là required. required chỉ bật runtime check báo lỗi nếu field chưa set, giúp bắt bug.
Field tags and schema evolution
Record encode chỉ là chuỗi nối các field đã encode, mỗi field nhận diện bằng tag number + datatype; field không set thì bỏ qua. Do đó tag là thứ quyết định ý nghĩa dữ liệu:
- Đổi tên field: OK — dữ liệu encode không bao giờ chứa tên.
- Đổi tag của field: KHÔNG — làm mọi dữ liệu cũ vô nghĩa.
- Thêm field mới với tag mới:
- Forward compat: code cũ gặp tag lạ thì bỏ qua; type annotation cho parser biết cần skip bao nhiêu byte.
- Backward compat: code mới đọc dữ liệu cũ vẫn ổn vì tag cũ giữ nguyên nghĩa. Nhưng field mới không được là
required(dữ liệu cũ không có field đó → check fail). Mọi field thêm sau lần deploy đầu phải optional hoặc có default.
- Xoá field: như thêm field nhưng đảo chiều compat. Chỉ xoá được field optional (không bao giờ xoá required), và không bao giờ dùng lại tag number đó (còn dữ liệu cũ đâu đó mang tag này; code mới phải bỏ qua nó).
Ví dụ tiến hoá an toàn:
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
optional string email = 4; // thêm mới: optional, tag mới
// tag 5 từng là "nickname" đã xoá → reserved, không tái sử dụng
}
Datatypes and schema evolution
- Đổi datatype có thể được nhưng rủi ro mất precision/bị cắt. Ví dụ int32 → int64: code mới đọc dữ liệu cũ ổn (điền 0 vào bit thiếu); nhưng code cũ đọc dữ liệu mới vẫn dùng biến 32-bit → giá trị không vừa sẽ bị truncate.
- Protobuf không có kiểu list/array mà có marker
repeated: cùng tag xuất hiện nhiều lần. Hệ quả hay: đổioptional(một giá trị) thànhrepeated(nhiều giá trị) là an toàn — code mới đọc dữ liệu cũ thấy list 0 hoặc 1 phần tử; code cũ đọc dữ liệu mới chỉ thấy phần tử cuối cùng. - Thrift có kiểu list riêng tham số hoá theo kiểu phần tử → không tiến hoá single → multi như protobuf được, nhưng hỗ trợ nested list.
Avro
Apache Avro bắt đầu năm 2009, là subproject của Hadoop, vì Thrift không hợp với use case của Hadoop. Cũng dùng schema, có hai ngôn ngữ schema: Avro IDL (cho người viết) và dạng JSON (cho máy đọc):
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
{
"type": "record", "name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favoriteNumber", "type": ["null", "long"], "default": null},
{"name": "interests", "type": {"type": "array", "items": "string"}}
]
}
- Không có tag number trong schema. Encode chỉ 32 byte — gọn nhất.
- Byte sequence không có gì nhận diện field hay kiểu: chỉ là các value nối nhau. String = length prefix + UTF-8; integer dùng varint như Thrift CompactProtocol.
- Decode bằng cách đi qua các field theo thứ tự trong schema và dùng schema để biết kiểu → chỉ decode đúng nếu dùng đúng schema lúc ghi. Vậy Avro tiến hoá schema thế nào?
The writer's schema and the reader's schema
- Writer's schema: schema mà ứng dụng dùng khi encode (ví dụ compile sẵn trong app).
- Reader's schema: schema mà code đọc đang kỳ vọng (ví dụ code generate lúc build).
- Ý tưởng then chốt: hai schema không cần giống nhau, chỉ cần tương thích. Khi decode, thư viện Avro đặt hai schema cạnh nhau và dịch dữ liệu từ writer's schema sang reader's schema (schema resolution, quy định rõ trong Avro spec):
- Field khác thứ tự: không sao — ghép theo tên field.
- Field có trong writer nhưng không có trong reader: bỏ qua.
- Field reader cần nhưng writer không có: điền default value khai báo trong reader's schema.
Ví dụ resolution:
Writer's schema (v1): userName, favoriteNumber, interests
Reader's schema (v2): userName, interests, photoUrl (default: null)
→ Đọc: userName ← khớp tên; interests ← khớp tên (khác thứ tự vẫn OK)
favoriteNumber ← writer có, reader không → bỏ qua
photoUrl ← writer không có → điền default null
Schema evolution rules
- Forward compat: schema mới là writer, schema cũ là reader. Backward compat: schema mới là reader, schema cũ là writer.
- Chỉ được thêm hoặc xoá field có default value (ví dụ
favoriteNumberdefaultnull):- Thêm field không default → reader mới không đọc được dữ liệu writer cũ → phá backward compat.
- Xoá field không default → reader cũ không đọc được dữ liệu writer mới → phá forward compat.
nullkhông phải default hợp lệ cho mọi kiểu: muốn cho phép null phải dùng union type, ví dụunion { null, long, string } field;.nullchỉ làm default được nếu là một nhánh của union (chính xác: default phải cùng kiểu với nhánh đầu tiên). Dài dòng hơn nhưng giúp tránh bug nhờ tường minh cái gì được null.- Vì vậy Avro không có
optional/requirednhư protobuf/Thrift, mà dùng union + default. - Đổi datatype: được nếu Avro convert được kiểu.
- Đổi tên field: reader's schema khai báo alias để khớp tên cũ → backward compatible nhưng không forward compatible. Tương tự thêm nhánh vào union cũng chỉ backward compatible.
But what is the writer's schema?
Không thể nhúng cả schema vào mỗi record (schema có khi lớn hơn dữ liệu, mất hết lợi ích nén). Tuỳ ngữ cảnh:
| Ngữ cảnh | Cách reader biết writer's schema |
|---|---|
| File lớn chứa hàng triệu record cùng schema (Hadoop) | Ghi writer's schema một lần ở đầu file — Avro object container file |
| Database với record ghi riêng lẻ ở các thời điểm khác nhau | Mỗi record có version number ở đầu; DB lưu danh sách schema theo version; reader tra version → lấy schema. Ví dụ Espresso (LinkedIn) |
| Gửi record qua kết nối mạng hai chiều | Thương lượng schema version khi thiết lập kết nối, dùng suốt connection — Avro RPC protocol |
Dù thế nào thì một database các schema version (schema registry) cũng rất đáng có: vừa là tài liệu, vừa cho phép kiểm tra compatibility trước khi deploy. Version có thể là số tăng dần hoặc hash của schema.
Dynamically generated schemas
Ưu điểm lớn của việc không có tag number: Avro thân thiện với schema sinh động (dynamically generated).
- Ví dụ: dump nội dung một relational DB ra file binary. Với Avro, sinh schema tự động từ relational schema: mỗi bảng → một record schema, mỗi cột → một field, tên cột = tên field.
- DB schema đổi (thêm một cột, bỏ một cột) → chỉ cần sinh lại Avro schema và export; không cần quan tâm tới thay đổi. Reader vẫn ghép được vì field nhận diện bằng tên.
- Với Thrift/protobuf, tag thường phải gán thủ công; mỗi lần DB schema đổi, admin phải cập nhật mapping cột → tag, và cẩn thận không dùng lại tag cũ. Schema sinh động vốn không phải mục tiêu thiết kế của chúng.
Code generation and dynamically typed languages
- Thrift/protobuf dựa vào code generation — hữu ích cho ngôn ngữ static type (Java, C++, C#): cấu trúc in-memory hiệu quả, type check, autocomplete trong IDE.
- Với ngôn ngữ dynamic (JavaScript, Ruby, Python) thì code generation ít ý nghĩa, thậm chí bị ghét vì thêm bước compile; với schema sinh động nó còn là vật cản.
- Avro có code generation tuỳ chọn, dùng tốt không cần nó. Object container file nhúng writer's schema nên self-describing: mở bằng thư viện Avro và xem như file JSON. Rất hợp với ngôn ngữ xử lý dữ liệu dynamic như Apache Pig: mở file Avro, phân tích, ghi output Avro mà không phải nghĩ tới schema.
The Merits of Schemas
- Schema của protobuf/Thrift/Avro đơn giản hơn nhiều so với XML Schema/JSON Schema (vốn hỗ trợ validation chi tiết như regex, khoảng giá trị) → dễ implement, hỗ trợ nhiều ngôn ngữ.
- Ý tưởng không mới: ASN.1 (chuẩn hoá 1984) dùng tag number để tiến hoá schema; encoding DER của nó vẫn dùng cho chứng chỉ SSL (X.509). Nhưng ASN.1 quá phức tạp, tài liệu kém → không nên dùng cho ứng dụng mới.
- Nhiều hệ thống có binary encoding độc quyền, ví dụ network protocol của relational DB; vendor cung cấp driver (ODBC/JDBC) để decode.
Ưu điểm của binary encoding có schema:
- Gọn hơn nhiều so với các "binary JSON" vì bỏ được tên field.
- Schema là tài liệu luôn đúng: vì bắt buộc cần để decode nên không thể lệch với thực tế (khác tài liệu viết tay).
- Schema registry cho phép kiểm tra forward/backward compat trước khi deploy.
- Code generation cho ngôn ngữ static type → type check lúc compile.
Tóm lại, schema evolution cho sự linh hoạt như schemaless/schema-on-read JSON, đồng thời đảm bảo tốt hơn về dữ liệu và tooling tốt hơn.
| Tiêu chí | Language-specific | JSON/XML/CSV | Thrift / Protobuf | Avro |
|---|---|---|---|---|
| Đa ngôn ngữ | Không | Có | Có (code gen) | Có |
| Schema | Ngầm theo class | Tuỳ chọn, phức tạp | Bắt buộc, IDL | Bắt buộc, IDL hoặc JSON |
| Nhận diện field | — | Tên field trong dữ liệu | Tag number trong dữ liệu | Tên field trong writer's schema |
| Kích thước | Thường phình to | Lớn nhất | Nhỏ (33–34 byte) | Nhỏ nhất (32 byte) |
| Người đọc được | Không | Có | Không | Không |
| Compat | Thường không có | Tuỳ cách dùng | Rõ ràng qua tag rules | Rõ ràng qua schema resolution + default |
| Schema sinh động | — | — | Khó (gán tag thủ công) | Rất tốt |
Modes of Dataflow
Compatibility là quan hệ giữa process encode và process decode. Ba cách dữ liệu chảy giữa các process:
Dataflow Through Databases
- Process ghi encode, process đọc decode. Nếu chỉ một process thì đọc là phiên bản tương lai của chính nó → ghi DB giống "gửi message cho chính mình trong tương lai" → backward compat hiển nhiên cần.
- Thường nhiều process (nhiều service, hoặc nhiều instance cùng service) truy cập cùng lúc; trong rolling upgrade, một số chạy code mới, một số còn code cũ → dữ liệu do code mới ghi có thể bị code cũ đọc → cần cả forward compat.
- Cạm bẫy mất field lạ: code mới thêm field và ghi vào DB; code cũ (không biết field) đọc record, sửa, rồi ghi lại. Mong muốn: code cũ giữ nguyên field lạ. Format encoding hỗ trợ việc này, nhưng nếu ứng dụng decode vào model object rồi re-encode thì field lạ có thể bị rơi mất trong quá trình chuyển đổi. Không khó giải quyết, chỉ cần ý thức được.
Different values written at different times
- Trong một DB có giá trị ghi 5 ms trước và giá trị ghi 5 năm trước. Deploy code mới mất vài phút, nhưng dữ liệu 5 năm tuổi vẫn nằm đó với encoding gốc — "data outlives code".
- Migration (rewrite dữ liệu sang schema mới) có thể làm nhưng đắt với dataset lớn, nên DB tránh. Hầu hết RDBMS cho phép thay đổi đơn giản như thêm cột default
nullmà không rewrite: khi đọc row cũ, DB tự điền null cho cột thiếu (ngoại lệ: MySQL thường rewrite cả bảng dù không cần). Espresso của LinkedIn dùng Avro để lưu, tận dụng luật schema evolution của Avro. - Nhờ schema evolution, cả DB trông như encode bằng một schema duy nhất dù bên dưới là nhiều phiên bản lịch sử.
Archival storage
- Snapshot DB để backup hoặc load vào data warehouse: thường encode bằng schema mới nhất cho nhất quán, dù nguồn là hỗn hợp nhiều version.
- Dump ghi một lần rồi immutable → hợp với Avro object container file, hoặc format columnar cho analytics như Parquet.
Dataflow Through Services: REST and RPC
- Mô hình phổ biến: client và server; server expose API qua mạng, gọi là service.
- Web: browser gửi
GET/POST, dùng chuẩn chung HTTP, URL, SSL/TLS, HTML. Client khác: app mobile/desktop, JavaScript app dùng XMLHttpRequest (Ajax) — response thường là JSON cho code xử lý, API là đặc thù ứng dụng. - Server có thể là client của service khác (app server → DB). Chia ứng dụng lớn thành service nhỏ theo chức năng: service-oriented architecture (SOA), gần đây là microservices.
- Service giống DB ở chỗ cho submit/query dữ liệu, nhưng chỉ cho input/output định trước bởi business logic → encapsulation, kiểm soát chi tiết client được làm gì.
- Mục tiêu then chốt của microservices: mỗi service do một team sở hữu, deploy và tiến hoá độc lập không cần phối hợp → phải chấp nhận server và client cũ/mới chạy cùng lúc → encoding phải tương thích qua các version API.
Web services
Khi dùng HTTP làm giao thức nền gọi là web service, dùng trong nhiều bối cảnh:
- App trên thiết bị người dùng gọi service qua internet công cộng.
- Service gọi service khác cùng tổ chức, thường cùng datacenter (middleware).
- Service gọi service của tổ chức khác qua internet (public API, xử lý thẻ tín dụng, OAuth).
| Tiêu chí | REST | SOAP |
|---|---|---|
| Bản chất | Triết lý thiết kế dựa trên nguyên tắc HTTP, không phải protocol | Protocol dựa trên XML |
| Quan hệ với HTTP | Tận dụng HTTP: URL định danh resource, cache control, auth, content negotiation | Muốn độc lập với HTTP, tránh dùng hầu hết tính năng HTTP |
| Mô tả API | OpenAPI/Swagger (tuỳ chọn) | WSDL (XML, không dành cho người đọc) |
| Tooling | Đơn giản, ít code generation | Phụ thuộc nặng vào tool, code gen, IDE; hệ chuẩn WS-* phức tạp |
| Interoperability | Tốt, mọi ngôn ngữ | Hay lỗi giữa các vendor; khó với ngôn ngữ không được hỗ trợ |
| Xu hướng | Phổ biến, gắn với microservices, tích hợp giữa tổ chức | Còn ở enterprise lớn, không được ưa ở công ty nhỏ |
Lưu ý: SOAP không phải điều kiện của SOA — SOAP là công nghệ cụ thể, SOA là cách tiếp cận kiến trúc. Ngay trong phe REST cũng tranh cãi, ví dụ HATEOAS.
The problems with remote procedure calls (RPCs)
Lịch sử công nghệ gọi API qua mạng nhiều hype nhưng nhiều vấn đề: EJB và Java RMI (chỉ Java), DCOM (chỉ Microsoft), CORBA (quá phức tạp, không có backward/forward compat). Tất cả dựa trên RPC (từ những năm 1970): làm cho gọi service từ xa trông như gọi hàm local — gọi là location transparency. Ý tưởng này sai về căn bản vì network request khác local call:
- Không dự đoán được: request/response có thể mất, máy xa chậm hoặc chết — ngoài tầm kiểm soát → phải tính tới retry.
- Thêm một kết cục: timeout — không có response thì không biết request đã tới hay chưa.
- Retry có thể thực thi nhiều lần: có khi request tới nơi, chỉ response bị mất → cần idempotence/deduplication trong protocol.
- Latency biến động dữ dội: dưới 1 ms lúc tốt, nhiều giây khi mạng nghẽn/service quá tải; local call thì gần như cố định.
- Không truyền được pointer: mọi tham số phải encode thành byte — ổn với số/chuỗi, rắc rối với object lớn.
- Khác ngôn ngữ: framework phải dịch kiểu dữ liệu giữa các ngôn ngữ, ví dụ vấn đề số > 2^53 của JavaScript.
→ Đừng cố làm remote service giống local object. Một phần sức hút của REST là không che giấu việc nó là network protocol.
Current directions for RPC
- RPC không biến mất. Framework dựa trên các encoding của chương: Thrift và Avro có sẵn RPC; gRPC dùng Protocol Buffers; Finagle dùng Thrift; Rest.li dùng JSON over HTTP.
- Thế hệ mới thừa nhận remote call khác local call: Finagle, Rest.li dùng futures/promises cho hành động async có thể fail, dễ gọi song song nhiều service rồi gộp kết quả; gRPC hỗ trợ streams (chuỗi request/response theo thời gian). Một số có service discovery (tìm IP/port của service).
- RPC binary tuỳ biến hiệu năng tốt hơn JSON over REST. Nhưng REST có lợi thế lớn: dễ thử nghiệm, debug (browser,
curl, không cần code gen), mọi ngôn ngữ đều hỗ trợ, hệ sinh thái tool khổng lồ (cache, load balancer, proxy, firewall, monitoring, testing). - Kết luận: REST thống trị public API; RPC chủ yếu cho giao tiếp giữa service cùng tổ chức, cùng datacenter.
Data encoding and evolution for RPC
- Giả định đơn giản hoá hợp lý: server được update trước, client sau → chỉ cần backward compat cho request (server mới đọc request của client cũ) và forward compat cho response (client cũ đọc response của server mới).
- Compat của RPC thừa hưởng từ encoding: Thrift, gRPC (protobuf), Avro RPC theo luật của format tương ứng; SOAP dùng XML schema — tiến hoá được nhưng có cạm bẫy tinh tế; REST thường JSON không schema chính thức — thêm optional request parameter và thêm field vào response thường được coi là thay đổi tương thích.
- Khó khăn: RPC hay dùng xuyên tổ chức, provider không ép được client nâng cấp → phải giữ compat rất lâu, có khi vô thời hạn; thay đổi phá compat thì thường phải chạy song song nhiều version API.
- Chưa có chuẩn chung về API versioning. Với REST: version trong URL (
/v1/...) hoặc trong HTTPAcceptheader. Với service dùng API key: lưu version client chọn ở phía server, cho đổi qua giao diện quản trị riêng.
Message-Passing Dataflow
Asynchronous message passing nằm giữa RPC và database: giống RPC vì message tới process khác với latency thấp; giống DB vì message không đi thẳng mà qua trung gian message broker (message queue, message-oriented middleware) lưu tạm.
Lợi ích so với RPC trực tiếp:
- Làm buffer khi bên nhận không sẵn sàng hoặc quá tải → tăng reliability.
- Tự redeliver message cho process đã crash → không mất message.
- Bên gửi không cần biết IP/port bên nhận (hữu ích trên cloud, VM đến rồi đi).
- Gửi một message cho nhiều người nhận.
- Tách rời logic sender và recipient (sender publish, không quan tâm ai consume).
Khác RPC: thường một chiều — sender không chờ reply (reply nếu có thì qua kênh riêng). Pattern asynchronous: gửi xong là quên.
Message brokers
- Trước đây thống trị bởi phần mềm thương mại: TIBCO, IBM WebSphere, webMethods. Nay open source phổ biến: RabbitMQ, ActiveMQ, HornetQ, NATS, Apache Kafka (so sánh chi tiết ở Chương 11).
- Cách dùng chung: process gửi message tới queue hoặc topic có tên; broker đảm bảo giao tới một hoặc nhiều consumer/subscriber. Nhiều producer và consumer trên cùng topic.
- Topic chỉ một chiều, nhưng consumer có thể publish sang topic khác (chuỗi xử lý) hoặc tới reply queue mà sender gốc đọc (request/response kiểu RPC).
- Broker không áp data model: message chỉ là byte + metadata → dùng encoding nào cũng được. Encoding backward + forward compatible → đổi và deploy publisher/consumer độc lập, theo thứ tự bất kỳ.
- Consumer republish sang topic khác thì cũng phải giữ field lạ, giống cạm bẫy ở phần database.
Distributed actor frameworks
- Actor model: mô hình concurrency trong một process. Thay vì thao tác thread (race condition, lock, deadlock), logic đóng gói trong actor — mỗi actor đại diện một client/entity, có state local không chia sẻ, giao tiếp bằng message bất đồng bộ. Message không đảm bảo tới nơi. Mỗi actor xử lý một message một lúc → không lo thread.
- Distributed actor framework: dùng cùng mô hình để scale qua nhiều node; message giữa node khác nhau được encode, gửi qua mạng, decode trong suốt.
- Location transparency hợp với actor hơn RPC vì actor model vốn đã giả định message có thể mất ngay cả trong một process → ít "lệch pha" giữa local và remote.
- Về bản chất là tích hợp message broker + actor model. Rolling upgrade vẫn phải lo forward/backward compat vì node mới và cũ gửi message cho nhau.
| Framework | Encoding mặc định | Rolling upgrade |
|---|---|---|
| Akka | Java built-in serialization (không có compat) | Được nếu thay bằng Protocol Buffers |
| Orleans | Format tuỳ biến riêng, không hỗ trợ rolling upgrade | Phải dựng cluster mới, chuyển traffic, tắt cluster cũ; hoặc dùng serialization plug-in |
| Erlang OTP | Record schema khó thay đổi | Có thể nhưng phải lên kế hoạch kỹ; kiểu maps (Erlang R17, 2014) có thể giúp |
| Dataflow | Ai encode / decode | Compat cần có | Lưu ý |
|---|---|---|---|
| Database | Process ghi encode, process đọc decode (có thể nhiều năm sau) | Backward (bắt buộc) + forward (khi rolling upgrade) | Data outlives code; giữ field lạ khi update |
| REST / RPC | Client encode request, server decode + encode response, client decode | Backward cho request, forward cho response (giả định server update trước) | API public phải giữ compat rất lâu, versioning |
| Message passing | Sender encode, recipient decode qua broker | Cả hai chiều để deploy theo thứ tự bất kỳ | Async, một chiều; giữ field lạ khi republish |
Summary
- Chi tiết encoding không chỉ ảnh hưởng hiệu năng mà còn ảnh hưởng kiến trúc và khả năng deploy. Rolling upgrade giúp release không downtime, deploy nhỏ và thường xuyên, phát hiện/rollback bản lỗi trước khi ảnh hưởng nhiều user → tốt cho evolvability.
- Vì các node chạy version khác nhau, mọi dữ liệu lưu chuyển phải có backward compat (code mới đọc dữ liệu cũ) và forward compat (code cũ đọc dữ liệu mới).
- Language-specific encoding: gói gọn trong một ngôn ngữ, thường không có compat. JSON/XML/CSV: phổ biến, compat tuỳ cách dùng, mơ hồ về kiểu số và binary. Thrift/Protobuf/Avro: gọn, hiệu quả, compat rõ ràng, schema làm tài liệu và code gen, nhưng phải decode mới đọc được.
- Ba mode dataflow: database, RPC/REST, message passing (broker hoặc actor). Cẩn thận một chút thì compat và rolling upgrade hoàn toàn làm được.
⚠️ Hiểu lầm & cạm bẫy thường gặp
- Dùng Java Serializable / Python pickle để lưu trữ lâu dài hoặc nhận dữ liệu từ ngoài — khoá ngôn ngữ, không compat, và có thể dẫn tới remote code execution.
- Tin rằng JSON giữ chính xác số nguyên 64-bit — JavaScript mất chính xác với số > 2^53; hãy trả ID dạng string (như Twitter).
- Đổi tag number hoặc tái sử dụng tag đã xoá trong protobuf/Thrift — làm hỏng dữ liệu cũ âm thầm. Hãy đánh dấu tag đã xoá là reserved.
- Thêm field
requiredmới — phá backward compat vì dữ liệu cũ không có field đó. - Nghĩ
required/optionalảnh hưởng binary encoding — không; chỉ là runtime check. - Đổi int32 → int64 và nghĩ an toàn hai chiều — code cũ đọc giá trị lớn sẽ bị truncate.
- Avro: thêm/xoá field không có default — phá backward hoặc forward compat. Và
nullkhông tự động là default hợp lệ, phải dùng union vớinulllà nhánh đầu. - Đổi tên field trong Avro bằng alias chỉ backward compatible, không forward.
- Mất field lạ khi decode vào model object rồi ghi lại — code cũ xoá mất dữ liệu do code mới thêm. Áp dụng cho cả DB lẫn consumer republish message.
- Coi RPC như local function call — quên timeout, retry gây thực thi trùng (cần idempotency key), latency biến động.
- Nghĩ migration DB là miễn phí — dữ liệu cũ sống rất lâu; rewrite dataset lớn rất đắt.
- Akka với serialization mặc định không rolling upgrade được — phải đổi sang protobuf hoặc tương tự.
💼 Áp dụng thực tế & phỏng vấn
- Zero-downtime deployment: khi được hỏi "làm sao thêm một field/đổi schema mà không downtime", trả lời theo bước: (1) thêm field optional/có default, (2) deploy reader hiểu field mới, (3) deploy writer ghi field mới, (4) backfill nếu cần, (5) chỉ xoá field cũ khi không còn reader nào dùng. Đây là pattern expand–contract (parallel change).
- Schema registry với Kafka: Confluent Schema Registry lưu các version Avro/Protobuf schema, message chỉ mang schema ID; registry kiểm tra compat (BACKWARD/FORWARD/FULL) trước khi producer đăng ký schema mới — đúng ý tưởng "database of schema versions" của chương.
- Chọn giao thức giữa service: public API → REST + JSON (dễ debug, tooling phong phú, versioning qua URL/header). Nội bộ, hiệu năng cao, nhiều ngôn ngữ → gRPC + protobuf (streaming, code gen, deadline). Pipeline dữ liệu/Hadoop/data lake → Avro hoặc Parquet.
- Idempotency trong API thanh toán/đặt hàng: vì retry qua mạng có thể thực thi hai lần, dùng idempotency key (như Stripe). Đây là câu hỏi hay gặp trong phỏng vấn.
- Async vs sync: dùng message queue (Kafka, RabbitMQ, SQS) để buffer spike, decouple service, fan-out tới nhiều consumer; đổi lại không có response ngay và phải xử lý redelivery/duplicate.
- Mobile client: client cũ có thể sống nhiều năm → API phải forward-compatible trong response (client cũ bỏ qua field lạ) và server phải chấp nhận request kiểu cũ; cân nhắc giữ nhiều version API song song.
- Con số đáng nhớ: record ví dụ JSON 81 byte → MessagePack 66 → Thrift Binary 59 → Thrift Compact 34 → Protobuf 33 → Avro 32; Base64 tăng 33%; số > 2^53 mất chính xác trong JS.