Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 28 — Microservices Communication Patterns

Technical BA Masterclass Bài 28/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn vừa được giao viết spec cho một hệ thống e-commerce mới của một công ty bán lẻ tại Việt Nam. Hệ thống được chia thành nhiều microservice: dịch vụ Đặt hàng (Order), dịch vụ Thanh toán (Payment), dịch vụ Kho (Inventory), dịch vụ Giao hàng (Shipping), dịch vụ Thông báo (Notification). Câu hỏi đầu tiên mà kiến trúc sư hỏi bạn không phải là "service này làm gì", mà là "các service này nói chuyện với nhau như thế nào". Đó chính là chủ đề của bài này: communication patterns — các mẫu giao tiếp giữa microservice.

Vì sao một Technical BA cần nắm chắc phần này? Bởi vì cách các service giao tiếp quyết định trực tiếp đến những thuộc tính mà nghiệp vụ quan tâm: thời gian phản hồi cho khách hàng, khả năng chịu lỗi khi một service chết, tính nhất quán của dữ liệu (khách trừ tiền nhưng đơn không tạo?), và độ phức tạp khi vận hành. Nếu bạn viết requirement "khi khách đặt hàng, hệ thống gửi email xác nhận" mà không hiểu giao tiếp đồng bộ hay bất đồng bộ, bạn có thể vô tình ép đội kỹ thuật làm cho luồng đặt hàng phụ thuộc vào email server — và khi mail server chậm, khách không đặt được hàng. Một quyết định giao tiếp sai làm hỏng cả trải nghiệm nghiệp vụ.

Bài này không yêu cầu bạn code. Mục tiêu là giúp bạn hiểu đủ sâu để tham gia thiết kế tích hợp, đặt đúng câu hỏi cho architect, và viết spec phản ánh đúng ràng buộc kỹ thuật.

Khái niệm cốt lõi

Có hai trục lớn để phân loại cách microservice giao tiếp: đồng bộ vs bất đồng bộ (theo thời gian), và giao tiếp điểm-điểm vs qua broker (theo cách kết nối). Hiểu hai trục này là hiểu 80% bức tranh.

Giao tiếp đồng bộ (Synchronous / Request-Response)

Trong mô hình đồng bộ, service gọi (caller) gửi một request và chờ cho đến khi nhận được response rồi mới đi tiếp. Đây giống hệt cuộc gọi điện thoại: bạn gọi, người kia phải nhấc máy và trả lời thì cuộc trò chuyện mới tiếp diễn.

Các công nghệ phổ biến cho giao tiếp đồng bộ:

  • REST over HTTP: phổ biến nhất, dễ hiểu, dễ debug. Service A gọi GET /products/123 của Service B và chờ JSON trả về.
  • gRPC: nhanh hơn REST nhờ dùng Protocol Buffers (nhị phân) thay vì JSON, hợp cho giao tiếp nội bộ giữa các service cần hiệu năng cao.
  • GraphQL: cho phép caller hỏi đúng dữ liệu mình cần trong một request, thường dùng ở tầng API gateway hướng ra ngoài.
Đặc trưng quan trọng nhất của giao tiếp đồng bộ là temporal coupling — ràng buộc thời gian. Caller và callee phải cùng "sống" và cùng sẵn sàng tại cùng thời điểm. Nếu Service B đang sập hoặc đang chậm, Service A bị kẹt theo. Đây là con dao hai lưỡi: đổi lại sự đơn giản và phản hồi tức thì, bạn chấp nhận rủi ro lan truyền lỗi.

Giao tiếp bất đồng bộ (Asynchronous / Event-driven)

Trong mô hình bất đồng bộ, service gửi đi một thông điệp (message hoặc event) rồi đi tiếp ngay, không chờ phản hồi. Một bên trung gian — thường là message broker như Kafka, RabbitMQ, hay AWS SQS/SNS — giữ thông điệp lại và chuyển cho service nhận khi nó sẵn sàng. Giống như gửi tin nhắn Zalo: bạn gửi xong làm việc khác, người nhận đọc và xử lý lúc nào tùy họ.

Có hai phong cách bất đồng bộ chính:

  • Message/Command: gửi một lệnh cụ thể tới một consumer cụ thể (ví dụ "hãy gửi email này"). Mang tính point-to-point.
  • Event/Publish-Subscribe: phát một sự kiện "đã xảy ra việc gì đó" (ví dụ OrderPlaced) mà không quan tâm ai nghe. Nhiều consumer cùng đăng ký và phản ứng độc lập.
Lợi ích lớn nhất của bất đồng bộ là loose coupling và khả năng chịu lỗi: nếu service Notification chết, các event vẫn nằm trong broker, và khi nó sống lại sẽ xử lý nốt — đơn hàng vẫn được tạo bình thường. Đổi lại, bạn mất tính tức thời (eventual consistency — dữ liệu nhất quán "sau cùng" chứ không ngay lập tức) và tăng độ phức tạp vận hành.

Orchestration vs Choreography

Khi một quy trình nghiệp vụ trải qua nhiều service, có hai cách điều phối:

  • Orchestration (điều phối tập trung): có một "nhạc trưởng" — thường là một Orchestrator service — gọi lần lượt từng service và quyết định bước tiếp theo. Dễ nhìn toàn cảnh luồng, dễ debug, nhưng nhạc trưởng dễ thành điểm nghẽn và điểm chết.
  • Choreography (vũ đạo phân tán): không ai chỉ huy; mỗi service tự lắng nghe event và tự phản ứng. Linh hoạt, không có điểm chết trung tâm, nhưng luồng nghiệp vụ "ẩn" trong các event, khó theo dõi toàn cục.
Đây là khái niệm cực kỳ quan trọng với BA, vì nó quyết định cách bạn mô tả một end-to-end flow trong tài liệu.

Saga — quản lý transaction xuyên service

Trong monolith, bạn có một database transaction: hoặc tất cả thành công, hoặc rollback hết. Trong microservice, mỗi service có database riêng, không có transaction chung. Saga pattern giải quyết bằng chuỗi các giao dịch cục bộ, mỗi bước có một compensating action (hành động bù trừ) để hoàn tác khi bước sau thất bại. Ví dụ: nếu đã trừ kho nhưng thanh toán thất bại, saga sẽ phát lệnh "hoàn kho". BA cần biết khái niệm này để định nghĩa rõ "khi bước X hỏng thì bù trừ những gì".

Tình huống thực tế

Tình huống 1 — Tiki và luồng đặt hàng giờ flash sale

Giả định một sàn TMĐT lớn như Tiki trong đợt flash sale 12.12. Lúc đầu, đội kỹ thuật thiết kế luồng đặt hàng hoàn toàn đồng bộ: Order service gọi tuần tự Inventory (kiểm kho) → Payment (trừ tiền) → Shipping (tạo vận đơn) → Notification (gửi email), tất cả trong một request HTTP, khách phải chờ cả chuỗi xong mới thấy "đặt hàng thành công".

Vấn đề bùng nổ khi traffic tăng gấp 20 lần lúc 0h. Notification service phụ thuộc vào một nhà cung cấp email bên thứ ba bị quá tải, response chậm lên 8 giây. Vì luồng đồng bộ, mỗi đơn hàng bị treo 8 giây ở bước cuối, kéo theo connection pool của Order service cạn kiệt, và toàn bộ chức năng đặt hàng sụp đổ — chỉ vì cái email.

Đội đã tái thiết kế: tách bước Notification (và cả Shipping) ra bất đồng bộ. Order service chỉ làm đồng bộ hai việc cốt lõi cần phản hồi ngay cho khách — kiểm kho và thanh toán — rồi phát event OrderPlaced lên Kafka và trả về "thành công" cho khách trong dưới 1 giây. Notification và Shipping lắng nghe event đó và xử lý sau. Kết quả: dù email vẫn chậm, khách vẫn đặt hàng mượt, email tới trễ vài giây cũng không sao.

Bài học: hãy phân biệt việc nào bắt buộc đồng bộ (khách cần biết kết quả ngay: hết hàng? thanh toán ok?) và việc nào có thể bất đồng bộ (email, vận đơn, cập nhật điểm thưởng). Một BA giỏi sẽ chỉ ra ranh giới này ngay từ khi viết requirement, thay vì để đội nhận ra khi production sập.

Tình huống 2 — Ngân hàng số và bài toán nhất quán dữ liệu

Một ngân hàng số tại Việt Nam (kiểu Timo hay Cake) xây tính năng chuyển tiền liên kết với ví điện tử đối tác. Luồng gồm: Account service trừ tiền tài khoản → Partner service gọi API ví đối tác cộng tiền. Hai service, hai database riêng.

Đội ban đầu dùng giao tiếp đồng bộ thuần: trừ tiền xong gọi sang đối tác. Nhưng API đối tác đôi khi timeout sau khi đã cộng tiền thành công bên họ. Account service không nhận được phản hồi, tưởng thất bại, hoàn lại tiền cho khách — thế là khách nhận tiền hai lần. Đây chính là bài toán nhất quán phân tán.

Giải pháp là áp dụng Saga theo kiểu orchestration: một Transfer Orchestrator điều phối, kết hợp với giao tiếp idempotent (mỗi lệnh cộng tiền có một transaction-id duy nhất, gọi lại nhiều lần cũng chỉ cộng một lần) và compensating transaction (nếu xác nhận đối tác thất bại thật sự, mới phát lệnh hoàn tiền). Vì là tiền bạc, họ chọn orchestration để có một nơi nhìn rõ trạng thái từng giao dịch, phục vụ đối soát và xử lý khiếu nại.

Bài học: với nghiệp vụ tài chính, eventual consistencyidempotency không phải tùy chọn kỹ thuật mà là yêu cầu nghiệp vụ. BA phải hỏi: "Nếu bước này gọi lại lần hai thì sao? Nếu bước sau hỏng, ta hoàn tác bằng cách nào?" Những câu hỏi này thuộc về bạn, không chỉ về architect.

Tình huống 3 — Grab và event-driven cho hệ sinh thái đa dịch vụ

Grab vận hành nhiều mảng: gọi xe, giao đồ ăn (GrabFood), ví (GrabPay), điểm thưởng (GrabRewards). Khi một chuyến xe hoàn thành, rất nhiều mảng cần phản ứng: cộng điểm thưởng, cập nhật lịch sử chuyến, tính hoa hồng tài xế, ghi nhận cho hệ thống chống gian lận, cập nhật báo cáo doanh thu.

Nếu dùng đồng bộ, Ride service sẽ phải gọi lần lượt 5–6 service mỗi khi chuyến xong — vừa chậm, vừa coupling chặt, và mỗi khi thêm một mảng mới (ví dụ thêm tính năng carbon offset) lại phải sửa code Ride service. Thay vào đó, Grab dùng choreography qua event: Ride service chỉ phát một event TripCompleted lên Kafka. Các mảng còn lại tự đăng ký (subscribe) và phản ứng độc lập. Muốn thêm tính năng mới, chỉ cần thêm một consumer mới lắng nghe TripCompleted, không động chạm Ride service.

Bài học: khi nhiều bên cần phản ứng với cùng một sự việc và danh sách bên đó hay thay đổi, publish-subscribe choreography là lựa chọn giúp hệ thống mở rộng mà không phải sửa service gốc. BA nên nhận diện các "sự kiện nghiệp vụ" (business event) như đơn hoàn tất, chuyến kết thúc, thanh toán thành công — đây chính là ứng viên cho event-driven.

Hướng dẫn từng bước

Khi bạn cần thiết kế hoặc viết spec cho giao tiếp giữa các service, hãy đi theo các bước sau:

  • Liệt kê các tương tác (interaction) trong luồng nghiệp vụ. Vẽ ra: service nào cần gì từ service nào, tại bước nào. Mỗi mũi tên là một quyết định giao tiếp.
  • Với mỗi tương tác, hỏi: caller có cần kết quả ngay để đi tiếp không? Nếu (ví dụ kiểm kho trước khi xác nhận đơn) → nghiêng về đồng bộ. Nếu không (ví dụ gửi email, cập nhật báo cáo) → nghiêng về bất đồng bộ.
  • Đánh giá mức độ chịu lỗi mong muốn. Hỏi: "Nếu service nhận đang chết, luồng chính có được phép tiếp tục không?" Nếu phải tiếp tục → bất đồng bộ qua broker để tách phụ thuộc.
  • Xác định số lượng người nghe. Một người nhận duy nhất → message/command (point-to-point). Nhiều bên cùng phản ứng và hay thay đổi → event publish-subscribe.
  • Chọn cách điều phối luồng đa bước. Cần nhìn rõ toàn cảnh, dễ đối soát (tài chính) → orchestration. Cần linh hoạt, mở rộng tự do → choreography.
  • Xử lý transaction phân tán. Nếu một quy trình thay đổi dữ liệu ở nhiều service, định nghĩa saga: các bước và compensating action tương ứng cho mỗi bước.
  • Đặc tả các thuộc tính an toàn. Ghi rõ yêu cầu idempotency (gọi lại không gây tác dụng kép), retry (thử lại bao nhiêu lần), timeout, và dead-letter (thông điệp lỗi không xử lý được thì đi đâu).
  • Tài liệu hóa. Vẽ sequence diagram cho luồng đồng bộ, và event-flow diagram cho luồng bất đồng bộ. Ghi rõ contract của mỗi event (tên, schema, ý nghĩa).

Lỗi thường gặp & mẹo

Lỗi 1 — Đồng bộ hóa mọi thứ. Đây là sai lầm phổ biến nhất của hệ mới chuyển từ monolith. Mọi service gọi nhau bằng REST đồng bộ, tạo thành "distributed monolith" — phân tán nhưng coupling chặt còn hơn monolith, một service chết kéo sập cả chuỗi. Mẹo: mặc định hỏi "có thể tách bất đồng bộ không" cho mọi bước không cần phản hồi tức thì.

Lỗi 2 — Bỏ qua idempotency. Trong mạng phân tán, retry là điều chắc chắn xảy ra. Nếu một lệnh "trừ tiền" được gửi lại do timeout mà service không idempotent, khách bị trừ hai lần. Mẹo: mọi thao tác thay đổi dữ liệu qua mạng nên có khóa định danh (idempotency key) — và BA phải viết yêu cầu này vào spec, đừng để mặc định.

Lỗi 3 — Lạm dụng choreography đến mức không ai hiểu luồng. Khi mọi thứ là event tự do, không ai nắm được "đặt hàng rồi thì điều gì xảy ra tiếp". Debug thành ác mộng. Mẹo: dùng orchestration cho các luồng quan trọng cần đối soát; dùng choreography cho các phản ứng phụ, độc lập.

Lỗi 4 — Quên xử lý event thất bại. Event được phát đi nhưng consumer xử lý lỗi — rồi sao? Nếu không có dead-letter queue và cơ chế retry, dữ liệu âm thầm mất. Mẹo: luôn hỏi "nếu consumer xử lý event này thất bại thì sao?" trong mọi luồng bất đồng bộ.

Lỗi 5 — Nhầm eventual consistency là bug. Khi tách bất đồng bộ, dữ liệu giữa các service sẽ lệch nhau trong vài giây. Người dùng (và đôi khi cả PM) tưởng là lỗi. Mẹo: BA cần làm rõ với nghiệp vụ về độ trễ chấp nhận được, và thiết kế UI phản ánh trạng thái "đang xử lý" thay vì giả vờ mọi thứ tức thì.

Mẹo tổng quát: một câu hỏi vàng để hỏi architect là "Cái này đồng bộ hay bất đồng bộ, và vì sao?". Câu trả lời sẽ lộ ra phần lớn rủi ro nghiệp vụ tiềm ẩn của hệ thống.

Bài tập thực hành

Bài 1 — Phân loại tương tác. Cho luồng đăng ký tài khoản: tạo user → kiểm tra email trùng → gửi email xác minh → tạo ví điểm thưởng mặc định → ghi log audit. Với mỗi bước, hãy quyết định nên đồng bộ hay bất đồng bộ, và viết một câu giải thích lý do dựa trên câu hỏi "caller có cần kết quả ngay không".

Bài 2 — Thiết kế saga. Một đơn hàng đi qua: trừ kho → trừ tiền → tạo vận đơn. Hãy liệt kê compensating action cho từng bước, để khi bước "tạo vận đơn" thất bại, hệ thống có thể hoàn tác đúng và sạch.

Bài 3 — Phản biện thiết kế. Một đội đề xuất: "Mọi service gọi nhau bằng REST đồng bộ cho đơn giản và dễ debug." Hãy viết 3 câu hỏi phản biện mà bạn — với vai trò Technical BA — sẽ đặt ra để bộc lộ rủi ro của thiết kế này, kèm gợi ý cải tiến.

Bài 4 — Chọn orchestration hay choreography. Cho hai tình huống: (a) quy trình hoàn tiền cần đối soát chặt với kế toán; (b) khi đơn hoàn tất, năm bộ phận khác nhau cần phản ứng và danh sách này sẽ còn mở rộng. Với mỗi tình huống, chọn mẫu điều phối phù hợp và giải thích.

Tóm tắt

Giao tiếp giữa microservice xoay quanh hai lựa chọn nền tảng. Đồng bộ (REST, gRPC, GraphQL) cho phản hồi tức thì nhưng tạo ràng buộc thời gian — service gọi và service nhận phải cùng sẵn sàng, lỗi dễ lan truyền. Bất đồng bộ (qua message broker như Kafka, RabbitMQ) cho loose coupling và khả năng chịu lỗi cao, đổi lại là eventual consistency và độ phức tạp vận hành.

Với luồng đa bước, bạn chọn giữa orchestration (nhạc trưởng tập trung, dễ đối soát) và choreography (event tự do, dễ mở rộng), và dùng saga với compensating action để quản lý transaction xuyên service. Hai thuộc tính sống còn cần đặc tả là idempotency và xử lý thất bại (retry, timeout, dead-letter).

Vai trò của bạn — Technical BA — không phải là chọn công nghệ, mà là chỉ ra đúng đâu cần đồng bộ, đâu nên bất đồng bộ, dựa trên yêu cầu nghiệp vụ về tốc độ, độ tin cậy và tính nhất quán. Khi bạn đặt được câu hỏi "Cái này đồng bộ hay bất đồng bộ, và nếu nó hỏng thì điều gì xảy ra với khách hàng?", bạn đã đứng đúng vị trí cầu nối giữa nghiệp vụ và kỹ thuật.