Dataflow — DB, REST/RPC, Message-Passing: Ba cách dữ liệu di chuyển giữa các hệ thống (DDIA)

Mở đầu
Ảnh: panumas nikhomkhai — Pexels
Ở phần trước của Chương 4, chúng ta đã biết cách encode dữ liệu — từ JSON/XML cho tới Thrift, Protobuf, Avro — và cách các schema evolution strategies (backward/forward compatibility) giúp hệ thống thay đổi mà không hỏng. Nhưng encode xong rồi thì dữ liệu đi đâu?
Câu trả lời nằm ở phần 4.2: Dataflow. Dữ liệu encode xong sẽ di chuyển qua ba con đường chính: database (lưu xuống disk rồi đọc lại), REST/RPC (gửi qua network request-response), và message-passing (gửi bất đồng bộ qua message broker). Mỗi con đường có đặc thù riêng về timing, về ai encode và ai decode, và về cách schema evolution ảnh hưởng.
Dataflow qua Database — Khi dữ liệu sống lâu hơn code
Ảnh: Brett Sayles — Pexels
Cách đơn giản nhất để dữ liệu di chuyển từ process này sang process khác là qua database: process A ghi dữ liệu, process B đọc dữ liệu đó sau này. Nhưng vì database là persistent storage, dữ liệu có thể tồn tại rất lâu — lâu hơn nhiều so với code đã ghi ra nó.
Đây chính là lúc backward compatibility trở nên cực kỳ quan trọng: code mới phải đọc được dữ liệu do code cũ ghi. Tưởng tượng bạn deploy phiên bản mới của ứng dụng, và database có hàng triệu row được ghi bởi version cũ — nếu code mới không đọc được, bạn có một cuộc khủng hoảng.
Một chi tiết thú vị mà Martin Kleppmann nhấn mạnh: trong database, dataflow thường là một chiều và asynchronous. Process A ghi dữ liệu (encode), rồi sau đó không biết ai đọc. Không có handshake giữa writer và reader. Điều này khác hoàn toàn với REST/RPC (sẽ nói ở phần sau), nơi hai bên giao tiếp đồng bộ với nhau.
Có hai loại dataflow trong database cần phân biệt:
- Same process reads what it wrote — một service vừa ghi vừa đọc. Đơn giản nhất: bạn biết format dữ liệu vì chính tay mình ghi.
- Different process reads — service A ghi, service B đọc (hoặc cùng service nhưng sau update). Lúc này schema evolution mới thực sự là vấn đề.
Khi bạn dump database để backup hoặc migrate, dữ liệu cũng thay đổi format. Bạn export ra SQL dump hay Avro file — đây cũng là một dạng dataflow, nơi dữ liệu được encode lại theo format mới. Avro đặc biệt hữu ích cho việc này vì nó self-describing và dễ đọc bởi bất kỳ ngôn ngữ nào.
Dataflow qua REST/RPC — Khi services nói chuyện với nhau
Ảnh: Markus Spiske — Pexels
Khác với database (nơi dữ liệu "nằm im" chờ đọc), REST và RPC là giao tiếp synchronous — client gửi request, server xử lý, trả về response. Đây là xương sống của kiến trúc microservices: hàng tá services nói chuyện với nhau qua HTTP hoặc gRPC.
REST — Representational State Transfer
REST không phải là một protocol mà là một phong cách kiến trúc. Dùng HTTP verbs (GET, POST, PUT, DELETE), resources định danh bằng URL, stateless communication. REST phổ biến đến mức hầu như backend engineer nào cũng quen — nhưng ít ai để ý rằng REST không quy định format dữ liệu. Bạn có thể dùng JSON, XML, hoặc thậm chí Protobuf nếu muốn.
Điểm mạnh của REST là tính đơn giản và dễ hiểu. Request và response có thể đọc được bởi con người (nếu dùng JSON). Công cụ như OpenAPI/Swagger giúp document API một cách chuẩn hoá.
RPC — Remote Procedure Call
RPC cố gắng làm cho một call đến remote service trông giống như một function call local. Thư viện RPC tự động marshal arguments thành request, gửi qua network, unmarshal response, và trả về kết quả. Nghe có vẻ tiện lợi — nhưng đây cũng là nguồn gốc của nhiều vấn đề.
Vấn đề cốt lõi: một local function call và một remote call không giống nhau. Network không đáng tin cậy — request có thể mất, response có thể mất, hoặc server có thể crash. Bạn không biết chắc rằng request đã được thực thi hay chưa (nên gọi lại? không gọi lại?). Latency thay đổi thất thường. Memory pointers không thể truyền qua network. Tất cả những khác biệt này làm cho RPC trở nên "location transparency" — một illusion nguy hiểm.
Các RPC framework hiện đại như gRPC (dùng Protobuf) giải quyết nhiều vấn đề này bằng cách:
- Dùng binary encoding (Protobuf) — nhỏ gọn, có typed schema
- Hỗ trợ streaming (server-stream, client-stream, bidirectional)
- Có deadline/timeout mặc định
- Phân biệt rõ ràng giữa lỗi network và lỗi ứng dụng
Schema evolution cho REST/RPC
Khi bạn thay đổi API của mình, backward và forward compatibility là vấn đề sống còn. Một số kỹ thuật phổ biến:
- URL-based versioning —
/v1/users,/v2/users. Rõ ràng nhưng nhân bản code. - Header-based versioning —
Accept: application/vnd.myapp.v1+json. Sạch hơn, ít nhân bản. - Add-only expansion — chỉ thêm field mới, không xoá hay thay đổi field cũ. Đây là cách an toàn nhất nếu bạn kiểm soát được cả client và server.
Với gRPC và Protobuf, bạn có số field tag làm định danh — không phụ thuộc vào tên field. Nên việc thêm field mới là backward-compatible. Nhưng Protobuf không có optional field mặc định (trước version 3), nên việc phân biệt "field có giá trị default" và "field không được set" là không dễ.
Dataflow qua Message-Passing — Khi các service không cần nói chuyện trực tiếp
REST và RPC là synchronous: client gửi request và phải chờ response. Nhưng nhiều tình huống, bạn không muốn (hoặc không cần) chờ. Ví dụ: khi user đặt hàng, bạn không cần gửi email ngay lập tức trong cùng request — bạn chỉ cần đẩy một message "đã có đơn hàng mới" vào queue, và một background worker sẽ xử lý sau.
Đây chính là lúc message-passing phát huy sức mạnh.
Message Broker — Trung gian đáng tin cậy
Message broker (RabbitMQ, Kafka, ActiveMQ, Amazon SQS) nhận message từ producer, lưu tạm, và chuyển đến consumer(s). Lợi ích:
- Buffering — nếu consumer quá tải, message nằm chờ trong broker. Không làm chậm producer.
- Fault isolation — consumer crash không ảnh hưởng đến producer. Message vẫn an toàn trong broker.
- Decoupling — producer không cần biết consumer là ai, ở đâu, bao nhiêu cái.
- Multiple consumers — một message có thể được gửi đến nhiều consumer (fan-out) hoặc chia đều (work queue).
Martin Kleppmann đặt message-passing vào vị trí trung gian giữa database và RPC: nó asynchronous như database, nhưng lại có định hướng message như RPC. Producer encode message, broker lưu, consumer decode và xử lý.
Actor Model — Distributed Actors
Một dạng đặc biệt của message-passing là actor model, phổ biến trong các framework như Akka (JVM), Orleans (.NET), và Erlang/Elixir OTP. Trong actor model, mỗi actor là một đơn vị tính toán độc lập, có trạng thái riêng, và giao tiếp với nhau hoàn toàn qua message. Không có shared memory, không có lock.
Vấn đề với distributed actors: bạn không biết actor kia đang chạy trên machine nào. Nếu machine crash, actor mất — trừ khi bạn có cơ chế persistence. Framework như Orleans giải quyết bằng "virtual actors" — actor luôn tồn tại về mặt logic, chỉ được khởi tạo thực sự khi cần.
Từ góc nhìn encoding và schema evolution, message-passing đặt ra câu hỏi: ai encode, ai decode? Trong Kafka, producer encode message, consumer decode. Nếu producer và consumer được deploy ở thời điểm khác nhau, bạn cần compatibility giữa các version. Với schema registry (như Confluent Schema Registry cho Avro), bạn có thể đảm bảo schema luôn tương thích — không ai push schema mới vi phạm backward/forward compatibility.
Key Takeaways
- Database dataflow — dữ liệu tồn tại lâu hơn code. Backward compatibility là bắt buộc. Forward compatibility cũng quan trọng nếu nhiều service cùng đọc DB.
- REST/RPC dataflow — synchronous, request-response. REST dùng HTTP semantics, RPC giấu đi complexity của network (không nên!). gRPC + Protobuf là sự kết hợp mạnh mẽ cho microservices.
- Message-passing dataflow — asynchronous qua broker. Producer và consumer hoàn toàn độc lập. Message broker là điểm trung gian tin cậy giữa các service.
- Mỗi mode dataflow có yêu cầu khác nhau về schema evolution. Cùng một binary encoding (Protobuf/Avro) nhưng cách áp dụng cho từng mode khác nhau hoàn toàn.
- Avro đặc biệt hữu ích cho database dump vì tính self-describing (schema đi kèm dữ liệu). Protobuf và Thrift phù hợp hơn cho RPC (code generation, typed contracts).
Kết
Chương 4.2 của DDIA mở ra một góc nhìn thú vị: dữ liệu encode xong không chỉ nằm im — nó di chuyển. Cách nó di chuyển (qua DB, qua REST/RPC, qua message broker) quyết định rất nhiều đến chiến lược schema evolution bạn cần áp dụng.
Ở phần tiếp theo, chúng ta sẽ bước sang Chương 5 — một trong những chương quan trọng nhất của DDIA: Replication. Làm sao để nhân bản dữ liệu qua nhiều node? Leader vs leaderless replication? Replication lag và cách xử lý? Hẹn gặp lại bạn ở bài sau!