Lần đầu vào một dự án microservices, thứ làm bạn choáng thường không phải là code. Đó là một buổi họp kiểu: “Order service publish event qua outbox, payment consumer phải idempotent, nếu fail thì saga compensate, nhớ gắn correlation ID để trace.” Câu nào cũng có tiếng Việt, mà nghe không hiểu gì.
Glossary thì có rất nhiều. Vấn đề là học từng định nghĩa riêng lẻ thì mau quên. Lý do là bạn không thấy thuật ngữ đó sinh ra để chữa bệnh gì.
Bài này làm ngược lại: đi theo một request đặt hàng, từ lúc user bấm “Đặt hàng” đến khi đơn được xác nhận. Tới chỗ nào có vấn đề, thuật ngữ tương ứng sẽ xuất hiện. Đây là bài mở đầu của series Microservices từ A–Z. Mỗi nhóm thuật ngữ bên dưới sẽ có bài riêng để đi sâu, kèm code .NET.
Hệ thống ví dụ
Một shop online nhỏ, tách thành 4 service:
- Order Service: tạo và quản lý đơn hàng
- Payment Service: trừ tiền
- Inventory Service: giữ hàng trong kho
- Notification Service: gửi email/SMS
flowchart LR
C["Client (Web/Mobile)"] --> GW["API Gateway"]
GW --> O["Order Service"]
O --> ODB[("Order DB")]
O -- "OrderCreated" --> MB{{"Message Broker"}}
MB --> P["Payment Service"]
MB --> I["Inventory Service"]
MB --> N["Notification Service"]
P --> PDB[("Payment DB")]
I --> IDB[("Inventory DB")]
Cả series dùng lại đúng hệ thống này.
Chặng 0: Vì sao lại tách ra?
Trước khi có 4 service, hệ thống thường bắt đầu là một khối.
| Thuật ngữ | Hiểu nhanh |
|---|---|
| Monolith | Một ứng dụng, một lần deploy, thường dùng một database. Không xấu. Hầu hết sản phẩm nên bắt đầu như vậy. |
| Big Ball of Mud | Monolith để lâu không có ranh giới: module nào cũng gọi module nào, sửa một chỗ vỡ ba chỗ. Đây mới là thứ đáng sợ. |
| Modular Monolith | Vẫn deploy một khối, nhưng chia module có ranh giới rõ (mỗi module có API và dữ liệu riêng). Đây là bước đệm tự nhiên trước khi tách service. |
| Microservices | Mỗi service deploy độc lập, sở hữu dữ liệu riêng, giao tiếp qua mạng. Đổi lại, bạn phải gánh độ phức tạp của hệ phân tán. |
| Conway’s Law | Kiến trúc hệ thống sẽ phản chiếu cấu trúc giao tiếp của tổ chức. Có 4 team mà ép vào 1 service, hoặc 1 team ôm 12 service, đều sẽ đau. |
WARNINGMicroservices là giải pháp cho vấn đề tổ chức và scale team, trước khi là vấn đề kỹ thuật. Nếu team bạn có 3 người, gần như chắc chắn bạn chưa cần nó. Bài P2 sẽ nói kỹ chuyện này.
Chặng 1: Tách thế nào cho đúng — DDD
Câu hỏi đầu tiên: ranh giới giữa Order và Payment nằm ở đâu? Tách sai thì sau này hai service phải gọi nhau liên tục, tức là bạn có một distributed monolith: tệ hơn cả monolith gốc.
| Thuật ngữ | Hiểu nhanh |
|---|---|
| Domain-Driven Design (DDD) | Thiết kế phần mềm xoay quanh nghiệp vụ, dùng ngôn ngữ của nghiệp vụ. |
| Ubiquitous Language | Dev và business dùng chung một bộ từ. Nếu business nói “đơn hàng” mà code viết Cart, sớm muộn sẽ có bug hiểu nhầm. |
| Bounded Context | Ranh giới mà trong đó một từ có đúng một nghĩa. “Product” trong Catalog (mô tả, hình ảnh) khác “Product” trong Inventory (số lượng, vị trí kho). Mỗi Bounded Context thường là ứng viên để thành một service. |
| Aggregate | Cụm object phải luôn nhất quán với nhau, thay đổi qua một cửa duy nhất (aggregate root). Order + OrderLine là một aggregate. |
| Entity / Value Object | Entity có định danh (OrderId). Value Object chỉ có giá trị (Money, Address): hai Money(100, "VND") là một. |
| Domain Event vs Integration Event | Domain Event là chuyện xảy ra bên trong một service. Integration Event là chuyện được công bố ra ngoài cho service khác. |
| Anti-Corruption Layer (ACL) | Lớp dịch giữa model của mình và model của hệ thống khác (legacy, bên thứ ba), để model “bẩn” không lan vào code của mình. |
| Strangler Fig | Tách dần từng phần khỏi monolith thay vì viết lại từ đầu. Request được chuyển dần sang service mới cho đến khi phần cũ không còn ai dùng. |
Chặng 2: Request đi vào — Giao tiếp & API
User bấm “Đặt hàng”. Request từ mobile app đi vào hệ thống.
| Thuật ngữ | Hiểu nhanh | .NET |
|---|---|---|
| API Gateway | Cửa ngõ duy nhất: routing, auth, rate limit. Client không cần biết có bao nhiêu service phía sau. | YARP |
| Backend for Frontend (BFF) | Mỗi loại client một gateway riêng: mobile cần payload gọn, web admin cần nhiều dữ liệu hơn. | YARP / Minimal API |
| Rate Limiting | Giới hạn số request trên một khoảng thời gian để bảo vệ hệ thống. | Microsoft.AspNetCore.RateLimiting |
| REST vs gRPC | REST (JSON/HTTP) cho client bên ngoài. gRPC (Protobuf/HTTP2) nhanh và có schema chặt, hợp cho service gọi nhau nội bộ. | ASP.NET Core, Grpc.AspNetCore |
| API Composition | Gateway hoặc một service gọi nhiều service rồi ghép kết quả, ví dụ màn “chi tiết đơn” cần Order + Payment + Shipping. | |
| JWT / OAuth 2.0 | Gateway xác thực user một lần. Service phía sau chỉ cần verify token. | Microsoft.AspNetCore.Authentication.JwtBearer |
Order Service nhận request, lưu đơn trạng thái Pending. Bây giờ nó cần báo cho Payment và Inventory.
Chặng 3: Báo cho service khác — Messaging
Order có thể gọi thẳng HTTP sang Payment. Nhưng nếu Payment đang chết, đơn hàng cũng chết theo. Cách phổ biến hơn là bắn event rồi đi tiếp.
| Thuật ngữ | Hiểu nhanh | .NET |
|---|---|---|
| Synchronous vs Asynchronous | Sync: gọi rồi chờ kết quả (HTTP/gRPC). Async: gửi message rồi làm việc khác, bên nhận xử lý khi nó sẵn sàng. | |
| Message Broker / Event Bus | Trung gian nhận và phân phối message: RabbitMQ, Kafka, Azure Service Bus. | MassTransit, Wolverine |
| Queue vs Topic | Queue là point-to-point: mỗi message có một người nhận. Topic là pub/sub: mọi subscriber đều nhận được. OrderCreated là topic, vì Payment, Inventory và Notification đều cần nghe. | |
| Dead Letter Queue (DLQ) | Message xử lý fail quá số lần retry sẽ bị đẩy vào đây, không làm tắc queue chính, chờ người xem. | Có sẵn trong MassTransit/Wolverine |
| Backpressure | Khi bên nhận xử lý không kịp, nó cần cách báo bên gửi chậm lại, thay vì để queue phình vô hạn. | |
| Webhook | Hệ thống bên ngoài (ví dụ cổng thanh toán) gọi ngược lại HTTP endpoint của mình khi có sự kiện. |
Chặng 4: Dữ liệu nằm rải rác — Nhất quán
Đây là chặng khó nhất, và cũng là chỗ phỏng vấn system design hay đào sâu nhất.
Mỗi service giữ database riêng (Database per Service). Vậy “tạo đơn + trừ tiền + giữ hàng” không còn gói gọn trong một transaction nữa. Chuyện gì xảy ra nếu trừ tiền xong mà kho báo hết hàng?
| Thuật ngữ | Hiểu nhanh |
|---|---|
| Database per Service | Service khác không được đọc thẳng DB của mình. Muốn dữ liệu thì hỏi qua API hoặc nghe event. |
| Distributed Transaction / 2PC | Transaction trải trên nhiều DB. Two-Phase Commit làm được, nhưng chậm, khóa tài nguyên, và hầu hết broker/cloud DB hiện đại không hỗ trợ. Thực tế ít ai dùng. |
| Eventual Consistency | Chấp nhận dữ liệu lệch nhau trong vài giây, miễn là cuối cùng sẽ khớp. Đơn hiện “Đang xử lý” trong lúc chờ Payment là ví dụ. |
| CAP Theorem | Khi mạng bị chia cắt (Partition), bạn phải chọn giữa Consistency và Availability. Microservices thường nghiêng về A + eventual consistency. |
| Saga | Chuỗi transaction local, mỗi bước có một compensating transaction để hoàn tác. Kho hết hàng thì hoàn tiền, rồi hủy đơn. |
| Choreography vs Orchestration | Choreography: các service tự nghe event của nhau rồi phản ứng, không ai điều phối. Orchestration: một orchestrator (saga state machine) ra lệnh từng bước. |
| Dual Write | Lỗi kinh điển: lưu DB xong rồi publish message. Nếu app crash ở giữa, DB có đơn mà không ai biết. |
| Transactional Outbox | Cách chữa Dual Write: ghi message vào bảng Outbox trong cùng transaction với dữ liệu, sau đó một background job đọc bảng này và publish. |
| Idempotent Consumer | Broker thường đảm bảo at-least-once, nên một message có thể đến hai lần. Consumer phải xử lý sao cho chạy hai lần cũng không trừ tiền hai lần. |
| CQRS | Tách model ghi (command) và model đọc (query). Phía đọc có thể là một Materialized View được dựng sẵn từ event, để khỏi phải join qua 4 service. |
| Event Sourcing | Không lưu trạng thái hiện tại, mà lưu chuỗi event đã xảy ra, trạng thái được tính lại từ đó. Mạnh nhưng đắt. Chỉ dùng khi thật sự cần audit hoặc time-travel. |
Outbox trong EF Core, ý tưởng cốt lõi chỉ có vậy:
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.OutboxMessages.Add(OutboxMessage.From(new OrderCreated(order.Id, order.Total)));
await db.SaveChangesAsync(ct); // đơn hàng và message commit cùng lúc
await tx.CommitAsync(ct);
// Background worker đọc OutboxMessages chưa gửi → publish lên broker → đánh dấu đã gửi
Và phía consumer, idempotent bằng cách nhớ message nào đã xử lý:
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var messageId = context.MessageId!.Value;
if (await db.ProcessedMessages.AnyAsync(m => m.Id == messageId))
return; // đã xử lý rồi, bỏ qua
await paymentService.ChargeAsync(context.Message.OrderId, context.Message.Total);
db.ProcessedMessages.Add(new ProcessedMessage(messageId));
await db.SaveChangesAsync();
}
TIPMassTransit và Wolverine đều có sẵn Outbox/Inbox. Trong dự án thật, bạn nên dùng sẵn thay vì tự viết. Các đoạn code trên chỉ để hiểu cơ chế. Đoạn consumer trên còn một lỗ: hai bản sao cùng message đến song song thì cả hai đều qua được bước
AnyAsync. Bản thật cần unique constraint trênProcessedMessages.Idvà ghi nó cùng transaction với nghiệp vụ. Bài về Idempotent Consumer sẽ đi vào chi tiết.
Chặng 5: Có service bị chậm — Độ tin cậy
Payment Service gọi sang cổng thanh toán bên ngoài, và cổng này thỉnh thoảng mất 30 giây mới trả lời. Nếu không làm gì, thread của Payment sẽ bị giữ hết, rồi Payment chết, rồi mọi thứ gọi Payment cũng chết theo. Đó là cascading failure.
| Thuật ngữ | Hiểu nhanh | .NET |
|---|---|---|
| Timeout | Không chờ vô hạn. Gọi ra ngoài lúc nào cũng phải có timeout. | Polly, HttpClient.Timeout |
| Retry | Thử lại lỗi tạm thời, có exponential backoff + jitter để không dội bom service đang yếu. Chỉ retry thao tác idempotent. | Polly |
| Circuit Breaker | Lỗi nhiều quá thì “ngắt cầu dao”: fail ngay trong một khoảng thời gian thay vì cứ gọi tiếp. Có 3 trạng thái: Closed → Open → Half-Open. | Polly |
| Bulkhead | Chia ngăn tài nguyên (connection pool, concurrency) để một dependency chậm không hút cạn tài nguyên của cả service. | Polly |
| Health Check | Endpoint cho orchestrator biết service còn sống (liveness) và sẵn sàng nhận traffic chưa (readiness). | AddHealthChecks() |
Từ .NET 8, package Microsoft.Extensions.Http.Resilience gói sẵn mấy thứ này cho HttpClient:
builder.Services.AddHttpClient<PaymentGatewayClient>()
.AddStandardResilienceHandler(); // timeout + retry + circuit breaker, dựng trên Polly v8
Chặng 6: Có bug trên production — Observability
Khách báo: “Tôi bị trừ tiền mà đơn vẫn Pending”. Request đó đã đi qua 4 service và 1 broker. Log nằm ở 4 chỗ khác nhau. Bạn tìm bằng cách nào?
| Thuật ngữ | Hiểu nhanh | .NET |
|---|---|---|
| Correlation ID | Một ID gắn vào request từ gateway và truyền qua mọi service/message, để gom log của cùng một luồng. | Activity.Current.TraceId |
| Centralized Logging | Log từ mọi service đổ về một chỗ (Seq, ELK, Loki), có thể search theo Correlation ID. | Serilog |
| Distributed Tracing | Vẽ lại toàn bộ đường đi của request thành các span, thấy được bước nào chậm hay lỗi. | OpenTelemetry |
| Metrics | Số liệu theo thời gian: request/giây, tỉ lệ lỗi, độ trễ p95/p99. | OpenTelemetry + Prometheus |
| SLI / SLO / SLA | SLI là thứ đo được (p99 latency). SLO là mục tiêu nội bộ (p99 < 300ms). SLA là cam kết với khách, có phạt nếu vi phạm. |
NOTEObservability không phải “có log là đủ”. Câu hỏi đúng là: khi có sự cố chưa từng gặp, bạn có trả lời được “chuyện gì đang xảy ra” chỉ bằng dữ liệu hệ thống đang phát ra không.
Chặng 7: Deploy và vận hành
Order Service vừa có bản mới. Làm sao deploy mà không làm gián đoạn 3 service còn lại?
| Thuật ngữ | Hiểu nhanh | .NET / hạ tầng |
|---|---|---|
| Containerization | Đóng gói service cùng runtime thành image chạy giống nhau ở mọi môi trường. | Docker, dotnet publish /t:PublishContainer |
| Container Orchestration | Tự động chạy, scale, restart container. | Kubernetes |
| Service Discovery / Registry | Service tìm địa chỉ của nhau bằng tên, không hardcode IP. | K8s DNS, .NET Aspire |
| Externalized Configuration | Config nằm ngoài code, đổi theo môi trường, không phải build lại. | IOptions<T>, Azure App Configuration |
| Sidecar / Service Mesh | Một container phụ chạy cạnh mỗi service lo mTLS, retry, metrics. Mesh là cả mạng lưới sidecar đó (Istio, Linkerd). | |
| Blue-Green / Canary | Blue-Green: chạy song song bản cũ và mới, rồi chuyển toàn bộ traffic. Canary: chuyển dần 5% → 25% → 100%, theo dõi metrics từng bước. | |
| Feature Toggle | Deploy code nhưng tắt tính năng, bật lên khi sẵn sàng. Tách chuyện deploy khỏi chuyện release. | Microsoft.FeatureManagement |
| Contract Testing | Test để đảm bảo Order vẫn gửi đúng format mà Payment mong đợi, không cần dựng cả hệ thống. | Pact |
| Backward Compatibility | Bản mới của event/API không được làm vỡ consumer cũ: chỉ thêm field, không đổi nghĩa hay xóa field. |
Mình đã viết về Feature Flags trong .NET ở một bài riêng, nếu bạn muốn đọc trước.
Nhìn lại cả hành trình của đơn hàng
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant O as Order Service
participant MB as Broker
participant P as Payment
participant I as Inventory
C->>GW: POST /orders (JWT)
GW->>O: route + Correlation ID
O->>O: Lưu Order + Outbox (1 transaction)
O-->>C: 202 Accepted (Pending)
O->>MB: OrderCreated (outbox worker)
MB->>P: OrderCreated
P->>P: Idempotent check → charge (retry, circuit breaker)
P->>MB: PaymentSucceeded
MB->>I: PaymentSucceeded
I-->>MB: InventoryFailed (hết hàng)
MB->>P: InventoryFailed → Refund (compensate)
MB->>O: InventoryFailed → Order Cancelled
Đọc lại buổi họp ở đầu bài: “Order service publish event qua outbox, payment consumer phải idempotent, nếu fail thì saga compensate, nhớ gắn correlation ID để trace.” Giờ mỗi cụm từ đều có một chỗ đứng trong hình trên.
Đọc tiếp trong series
Series này có 22 bài, chia theo đúng các chặng ở trên:
- Nền tảng: bài này, và P2: Khi nào không nên dùng microservices
- Phân rã: DDD & Bounded Context, Database per Service, Strangler Fig & ACL
- Giao tiếp: API Gateway & BFF với YARP, Messaging, Kafka vs RabbitMQ
- Nhất quán dữ liệu: Saga, Outbox, Idempotent Consumer, CQRS, Event Sourcing
- Độ tin cậy: Retry, Timeout, Circuit Breaker, Bulkhead với Polly
- Observability: Logging, Tracing, Health check với OpenTelemetry
- Triển khai: Service Discovery, Service Mesh, Canary, Contract Testing
NOTETham khảo khi viết series: microservices.vn (bộ thuật ngữ và pattern tiếng Việt rất đầy đủ), microservices.io của Chris Richardson, và Building Microservices (Sam Newman, 2nd edition).
Đăng ký nhận bản tin
Nhận thông báo khi có bài viết mới. Không spam, hủy bất cứ lúc nào.