Menu
ESC

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

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

Đang tải...

Bài 40 — Integration Design — Synchronous Pattern

Technical BA Masterclass Bài 40/60

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

Hãy hình dung một buổi sáng thứ Hai, bạn ngồi vào bàn họp với team kỹ thuật để thiết kế tính năng "Đặt hàng và thanh toán" cho một sàn thương mại điện tử. Người dùng bấm nút "Thanh toán", hệ thống phải kiểm tra tồn kho, trừ điểm tích lũy, gọi cổng thanh toán VNPay, rồi tạo đơn hàng. Câu hỏi tưởng đơn giản nhưng quyết định cả kiến trúc: các bước này gọi nhau như thế nào? Gọi trực tiếp và chờ kết quả ngay (đồng bộ — synchronous), hay đẩy việc đi rồi xử lý sau (bất đồng bộ — asynchronous)?

Đây chính là lằn ranh giữa một hệ thống chạy mượt và một hệ thống sập dây chuyền lúc 9 giờ tối ngày sale. Là một Technical BA, bạn không cần tự code những lời gọi này, nhưng bạn phải viết được spec mô tả luồng tích hợp đồng bộ, hiểu được hệ quả của từng lựa chọn, và biết khi nào pattern này phù hợp — khi nào nó trở thành quả bom hẹn giờ.

Bài 40 này tập trung riêng vào Synchronous Integration Pattern — kiểu tích hợp mà bên gọi (caller) phải chờ bên được gọi (callee) trả về kết quả mới đi tiếp. Bài 41 sẽ nói về async và batch; còn ở đây, chúng ta sẽ mổ xẻ thật kỹ point-to-point, API Gateway, và các nguyên tắc thiết kế đồng bộ để bạn viết spec đúng ngay từ đầu.

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

Synchronous nghĩa là gì trong tích hợp hệ thống?

Tích hợp đồng bộ là khi Service A gọi Service B và dừng lại chờ B xử lý xong, trả kết quả về thì A mới chạy tiếp. Giống như bạn gọi điện hỏi đồng nghiệp một con số rồi cầm máy chờ họ tra cứu — bạn không làm gì khác được cho đến khi có câu trả lời.

Về mặt kỹ thuật, đồng bộ thường được hiện thực qua:

  • REST API (HTTP request/response) — phổ biến nhất, dễ debug, ai cũng đọc được.
  • gRPC — nhanh hơn, dùng Protocol Buffers, hợp cho giao tiếp nội bộ giữa các service.
Điểm mấu chốt của đồng bộ: caller bị block (chờ), và kết quả của caller phụ thuộc trực tiếp vào việc callee có sống và phản hồi kịp hay không. Đây vừa là điểm mạnh (đơn giản, dễ hiểu, có kết quả ngay), vừa là gót chân Achilles (một service chậm kéo cả chuỗi chậm theo).

Pattern 1 — Point-to-Point (gọi trực tiếp điểm-điểm)

Đây là cách đơn giản nhất: Service A biết địa chỉ của Service B và gọi thẳng qua REST hoặc gRPC. Service "Đặt hàng" gọi thẳng service "Tồn kho", rồi gọi thẳng service "Thanh toán".

Ưu điểm:

  • Cực kỳ đơn giản khi hệ thống còn nhỏ (dưới 5 service). Không cần thêm thành phần trung gian nào.
  • Độ trễ thấp — không qua trạm trung chuyển.
  • Dễ hiểu, dễ vẽ sequence diagram, dễ giải thích cho team.
Nhược điểm chí mạng — độ phức tạp N²: Khi số service tăng lên, số kết nối trực tiếp tăng theo cấp số nhân. Với công thức n(n-1)/2, nếu có 5 service tất cả gọi lẫn nhau thì có tới 10 kết nối; 10 service thì 45 kết nối; 20 service thì 190 kết nối. Mỗi kết nối là một chỗ phải cấu hình địa chỉ, xác thực, retry, timeout. Đến một lúc, sơ đồ kiến trúc trông như một mớ bòng bong dây điện — người ta gọi đây là kiến trúc "spaghetti" hay "death star".

Thêm nữa, point-to-point gắn chặt (tight coupling): mỗi service phải biết địa chỉ, định dạng dữ liệu, cơ chế xác thực của tất cả service nó gọi. Đổi một chỗ là phải sửa nhiều nơi.

Pattern 2 — API Gateway (cổng API)

Khi point-to-point bắt đầu rối, API Gateway xuất hiện như một "lễ tân" đứng trước toàn bộ hệ thống. Mọi request từ bên ngoài (web, mobile app, đối tác) đều đi qua một cổng duy nhất, rồi cổng đó định tuyến (route) đến đúng service phía sau.

API Gateway gánh những trách nhiệm chung (cross-cutting concerns) mà nếu để mỗi service tự làm thì lặp đi lặp lại:

  • Định tuyến (routing): /orders đi đến Order Service, /payments đi đến Payment Service.
  • Xác thực & phân quyền: kiểm tra token một lần ở cổng, các service phía sau không phải lo.
  • Rate limiting: chặn client gọi quá nhiều (chi tiết ở Bài 17).
  • Tổng hợp dữ liệu (aggregation): một request từ mobile có thể được cổng gom kết quả từ 3 service rồi trả về một lượt.
  • Ghi log, giám sát tập trung.
Quan trọng: API Gateway không xóa bỏ bản chất đồng bộ — phía sau cổng, client vẫn chờ kết quả. Nhưng nó giải quyết bài toán N² cho luồng từ ngoài vào: client chỉ cần biết một địa chỉ (cổng), thay vì biết địa chỉ của từng service.

BFF — Backend for Frontend

Một biến thể đáng nhắc: BFF (Backend for Frontend). Thay vì một cổng chung cho mọi loại client, ta tạo cổng riêng cho từng loại: một BFF cho web, một BFF cho mobile, một BFF cho đối tác. Lý do: app mobile cần dữ liệu gọn nhẹ (tiết kiệm pin, 3G yếu ở vùng quê), còn web có thể tải nhiều hơn. BFF cho phép tối ưu payload theo từng client mà không làm phình to một cổng chung.

Những thuộc tính spec bắt buộc phải nêu cho luồng đồng bộ

Là Technical BA, khi viết spec tích hợp đồng bộ, bạn phải mô tả rõ:

  • Timeout: chờ tối đa bao lâu thì bỏ cuộc? (ví dụ 3 giây).
  • Retry: thử lại mấy lần, cách nhau bao lâu? Có dùng exponential backoff không?
  • Circuit breaker: khi callee hỏng liên tục, có "ngắt cầu dao" để ngừng gọi tạm thời, tránh kéo sập cả hệ thống không?
  • Fallback: nếu gọi thất bại thì trả về gì cho người dùng?
  • Idempotency: nếu retry, có gây ra hậu quả kép (trừ tiền hai lần) không? (chi tiết ở Bài 29).

Tình huống thực tế

Ví dụ 1 — Tiki và bài toán point-to-point ngày Sale 11/11

Giả định một sàn TMĐT lớn ở Việt Nam — tạm gọi là sàn X, mô hình tương tự Tiki. Ban đầu hệ thống chỉ có 4 service: Catalog, Cart, Order, Payment. Tất cả gọi trực tiếp nhau theo point-to-point qua REST. Mọi thứ chạy ngon trong 2 năm đầu.

Đến ngày Sale 11/11, lưu lượng tăng gấp 15 lần. Order Service gọi đồng bộ sang Payment Service. Payment Service lại gọi đồng bộ cổng VNPay bên ngoài. VNPay bị quá tải, phản hồi chậm từ 200ms lên 8 giây. Vì là đồng bộ, mỗi request "Đặt hàng" phải ôm máy chờ 8 giây. Các connection của Order Service không được giải phóng, pool kết nối cạn kiệt, Order Service treo. Cart Service gọi Order cũng treo theo. Toàn bộ chuỗi sập domino chỉ vì một mắt xích cuối cùng (VNPay) chậm.

Diễn giải: đây là hiện tượng "cascading failure" — lỗi lan truyền điển hình của tích hợp đồng bộ không có cơ chế bảo vệ. Vấn đề không phải point-to-point sai, mà là spec thiếu timeout (đáng lẽ chỉ chờ 3 giây) và thiếu circuit breaker (đáng lẽ sau 20 lần lỗi liên tiếp thì ngắt, trả về thông báo "Thanh toán đang bận, vui lòng thử lại").

Bài học: Technical BA viết spec luồng đồng bộ phải luôn đặt câu hỏi: "Nếu service phía sau chậm hoặc chết thì chuyện gì xảy ra?". Mỗi lời gọi đồng bộ là một lời hứa rằng caller sẽ chờ — và bạn phải giới hạn lời hứa đó bằng timeout.

Ví dụ 2 — Ngân hàng số đưa API Gateway vào để dẹp "spaghetti"

Một ngân hàng số tại Đông Nam Á (tương tự mô hình TNEX hay Timo) phát triển nhanh từ 6 lên 18 microservice: Tài khoản, Thẻ, Chuyển khoản, Thông báo, KYC, Lịch sử giao dịch... Ban đầu mọi service gọi point-to-point. App mobile phải tự biết địa chỉ của 7 service khác nhau, tự xử lý token cho từng cái. Mỗi lần đội bảo mật đổi cơ chế xác thực, cả app mobile lẫn 18 service đều phải sửa — một cơn ác mộng vận hành.

Họ đưa vào API Gateway (dùng Kong). Sau khi triển khai: app mobile chỉ gọi một địa chỉ api.bank.vn. Cổng lo việc kiểm tra JWT, rate limit 100 request/phút/người dùng, định tuyến đến đúng service. Khi cần đổi cơ chế xác thực, chỉ sửa ở cổng. Số kết nối mà client phải biết giảm từ "biết hết 18 service" xuống còn "biết 1 cổng".

Diễn giải: API Gateway không làm hệ thống nhanh hơn (thực ra thêm một chặng nhảy nên trễ hơn vài ms), nhưng nó cắt giảm độ phức tạp coupling khủng khiếp và tập trung các mối lo chung về một chỗ.

Bài học: Khi bạn thấy spec có nhiều client cùng gọi nhiều service, và các yêu cầu xác thực/rate-limit/logging lặp lại khắp nơi — đó là tín hiệu nên đề xuất API Gateway. BA nên nêu rõ trong spec những trách nhiệm nào thuộc về cổng, những gì thuộc về service.

Ví dụ 3 — Khi đồng bộ là lựa chọn ĐÚNG: tra cứu hạn mức trước khi cho vay

Không phải lúc nào async cũng tốt hơn. Một công ty fintech cho vay tiêu dùng có luồng: khách hàng bấm "Vay 10 triệu", hệ thống phải gọi sang Credit Bureau (CIC — Trung tâm Thông tin Tín dụng) để kiểm tra điểm tín dụng ngay lập tức trước khi quyết định duyệt hay từ chối.

Ở đây, đồng bộ là lựa chọn duy nhất hợp lý: kết quả quyết định trực tiếp bước tiếp theo và người dùng đang chờ màn hình. Bạn không thể nói "chúng tôi sẽ kiểm tra sau rồi báo bạn" — khách hàng cần biết được duyệt hay không ngay. Spec đặt timeout 5 giây với CIC; nếu CIC không phản hồi kịp, fallback sang trạng thái "Đang xét duyệt thủ công" thay vì để màn hình treo vô tận.

Bài học: Đồng bộ phù hợp khi (1) caller cần kết quả ngay để đi tiếp, (2) người dùng đang chờ real-time, (3) thao tác đọc dữ liệu là chính. BA giỏi là người phân biệt được "việc này bắt buộc đồng bộ" với "việc này có thể đẩy async cho nhẹ hệ thống".

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

Đây là quy trình giúp Technical BA thiết kế và viết spec cho một luồng tích hợp đồng bộ:

Bước 1 — Liệt kê các service tham gia và thứ tự gọi. Vẽ ra: ai gọi ai, theo trình tự nào. Một sequence diagram đơn giản (xem Bài 35) là đủ. Ví dụ: Order → Inventory → Payment → VNPay.

Bước 2 — Xác định việc nào BẮT BUỘC đồng bộ. Với mỗi lời gọi, hỏi: "Caller có cần kết quả ngay để đi tiếp không?". Nếu có (kiểm tra tồn kho, duyệt thanh toán) thì đồng bộ. Nếu không (gửi email xác nhận, ghi log analytics) thì để dành cho async ở Bài 41.

Bước 3 — Chọn cơ chế: point-to-point hay qua Gateway. Nếu là luồng nội bộ giữa ít service, point-to-point hoặc gRPC. Nếu là luồng từ client/đối tác bên ngoài vào, đi qua API Gateway.

Bước 4 — Định nghĩa contract của mỗi lời gọi. Endpoint, method, request body, response body, mã lỗi. Đây là phần giao với OpenAPI/Swagger (Bài 11).

Bước 5 — Đặt các thông số chống chịu lỗi (resilience). Với mỗi lời gọi đồng bộ, ghi rõ trong spec: timeout (bao nhiêu giây), số lần retry, circuit breaker (ngưỡng kích hoạt), fallback (làm gì khi thất bại).

Bước 6 — Kiểm tra idempotency cho các thao tác ghi. Nếu có retry trên thao tác trừ tiền/tạo đơn, phải đảm bảo không bị thực hiện hai lần — thường dùng idempotency key.

Bước 7 — Mô tả luồng lỗi (error flow), không chỉ luồng thành công. Spec tốt mô tả cả "đường hạnh phúc" lẫn "đường đau khổ": khi Payment chết, người dùng thấy gì, đơn hàng ở trạng thái nào.

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

Lỗi 1 — Chuỗi đồng bộ quá dài. Order chờ Inventory, Inventory chờ Pricing, Pricing chờ Tax... Mỗi mắt xích cộng dồn độ trễ và cộng dồn xác suất lỗi. Nếu mỗi service có 99.9% uptime, chuỗi 5 service nối tiếp chỉ còn ~99.5% — và độ trễ là tổng tất cả. Mẹo: giữ chuỗi đồng bộ càng ngắn càng tốt; cân nhắc gọi song song (parallel) các bước độc lập thay vì nối tiếp.

Lỗi 2 — Quên đặt timeout. Đây là lỗi phổ biến nhất và nguy hiểm nhất. Không có timeout = chờ vô hạn = cạn pool kết nối = sập hệ thống (đúng như ví dụ Sale 11/11). Mẹo: mọi lời gọi đồng bộ trong spec PHẢI có timeout. Không có ngoại lệ.

Lỗi 3 — Coi API Gateway là "viên đạn bạc". Gateway giải quyết coupling nhưng nếu nhồi quá nhiều logic nghiệp vụ vào cổng, nó biến thành một "monolith trá hình" và là điểm chết duy nhất (single point of failure). Mẹo: cổng chỉ nên làm việc chung (routing, auth, rate limit), không nên chứa logic nghiệp vụ.

Lỗi 4 — Dùng đồng bộ cho việc đáng lẽ là async. Gửi email, đẩy notification, ghi báo cáo — không cần caller chờ. Nếu bắt người dùng chờ những việc này, trải nghiệm chậm vô lý. Mẹo: hỏi "người dùng có cần kết quả của bước này ngay không?" — nếu không, đừng đồng bộ.

Lỗi 5 — Bỏ qua idempotency khi có retry. Retry một lời gọi "trừ tiền" mà không có khóa idempotency = trừ tiền khách hai lần = khiếu nại và mất uy tín. Mẹo: mọi thao tác ghi có retry đều cần idempotency key.

Mẹo vàng cho BA: Trong spec, luôn vẽ kèm bảng nhỏ cho mỗi tích hợp đồng bộ với 5 cột: Endpoint | Timeout | Retry | Circuit Breaker | Fallback. Đội dev sẽ biết ơn bạn, và bạn sẽ không bao giờ quên những thứ then chốt.

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

Bài 1 — Phân loại đồng bộ/bất đồng bộ. Cho luồng "Đặt vé máy bay": (a) kiểm tra ghế trống, (b) giữ chỗ tạm, (c) gọi cổng thanh toán, (d) gửi email vé điện tử, (e) cập nhật điểm tích lũy thành viên. Với mỗi bước, xác định nên đồng bộ hay bất đồng bộ và giải thích ngắn gọn lý do.

Bài 2 — Tính độ phức tạp. Một hệ thống có 8 microservice gọi point-to-point lẫn nhau. Tính số kết nối tối đa theo công thức n(n-1)/2. Sau đó lập luận: ở quy mô này nên giữ point-to-point hay đưa vào API Gateway? Vì sao?

Bài 3 — Viết spec resilience. Cho lời gọi đồng bộ từ Order Service đến cổng thanh toán MoMo. Hãy điền bảng 5 cột: Timeout, Retry (số lần + khoảng cách), Circuit Breaker (ngưỡng), Fallback (hành vi khi lỗi), và nêu có cần idempotency key không, vì sao.

Bài 4 — Phân tích sự cố. Đọc lại ví dụ Sale 11/11. Viết một đoạn 5–7 dòng đề xuất 3 thay đổi cụ thể trong spec để sự cố không tái diễn, sắp xếp theo thứ tự ưu tiên.

Tóm tắt

Tích hợp đồng bộ (synchronous) là kiểu giao tiếp mà caller chờ callee trả kết quả mới đi tiếp — đơn giản, trực quan, có kết quả ngay, nhưng dễ sập dây chuyền nếu một mắt xích chậm. Hai pattern cốt lõi bạn cần nắm: point-to-point (gọi trực tiếp, gọn cho hệ thống nhỏ dưới 5 service nhưng vướng độ phức tạp N² khi mở rộng) và API Gateway (một cổng đứng trước, gánh routing/auth/rate-limit, cắt giảm coupling cho luồng từ ngoài vào, có biến thể BFF cho từng loại client).

Là Technical BA, giá trị của bạn nằm ở chỗ: không chỉ vẽ luồng thành công, mà mô tả đầy đủ timeout, retry, circuit breaker, fallback và idempotency cho mỗi lời gọi đồng bộ. Hãy nhớ ba câu hỏi trụ cột: "Việc này có bắt buộc đồng bộ không?", "Nếu service phía sau chậm/chết thì sao?", và "Retry có gây hậu quả kép không?". Trả lời được ba câu đó, spec của bạn đã vững hơn phần lớn tài liệu ngoài kia. Bài 41 tiếp theo sẽ mở sang thế giới async và batch — nơi caller không còn phải đứng chờ.