Giao diện
Synchronous Communication — Giao tiếp đồng bộ
Thứ Sáu, 2 giờ sáng. PagerDuty réo liên hồi. Payment Service đang xử lý mỗi request trong 30 giây thay vì 200ms như bình thường. Order Service — caller chính — đặt timeout ở 5 giây nhưng cấu hình retry 3 lần. Kết quả: mỗi đơn hàng tạo ra 4 request (1 gốc + 3 retry), tổng cộng 12x load amplification đổ lên Payment Service vốn đã kiệt sức. Cascade failure lan sang Inventory, Notification, rồi cả hệ thống sập trong vòng 8 phút.
Sự cố này không bắt nguồn từ bug logic hay memory leak. Nó đến từ hai quyết định thiết kế tưởng chừng vô hại: chọn sai timeout và retry mà không có circuit breaker. Giao tiếp đồng bộ giữa các service là con dao hai lưỡi — nhanh, dễ hiểu, dễ debug, nhưng một khi failure xảy ra, nó lan truyền theo đúng chiều mà request đi. Chọn đúng protocol (REST, gRPC, GraphQL) mới chỉ là nửa bài toán. Nửa còn lại nằm ở resilience patterns: circuit breaker, timeout strategy, idempotency — những thứ quyết định hệ thống sống hay chết lúc 2 giờ sáng.
Bức tranh tư duy
Hãy hình dung ba cách giao tiếp quen thuộc trong đời thường.
REST giống như gọi điện thoại. Bạn quay số, đợi người kia nhấc máy, nói chuyện bằng ngôn ngữ tự nhiên (JSON/text), rồi cúp máy. Đơn giản, ai cũng hiểu, nhưng mỗi cuộc gọi chiếm trọn một đường dây (HTTP/1.1 connection).
gRPC giống như bộ đàm quân sự với mã hiệu đã thống nhất trước. Hai bên dùng codebook chung (Protocol Buffers), truyền tín hiệu nhị phân nhanh gọn, và một tần số có thể xử lý nhiều cuộc hội thoại đan xen (HTTP/2 multiplexing). Hiệu quả cao, nhưng cần thiết bị chuyên dụng — không phải ai cũng có bộ đàm.
GraphQL giống như gọi món từ thực đơn tuỳ chỉnh. Thay vì nhận combo cố định (REST endpoint trả full object), bạn chọn chính xác những gì mình cần. Không thừa, không thiếu. Nhưng nhà bếp (server) cần logic phức tạp hơn để xử lý mọi tổ hợp món.
Khi phép so sánh bị phá vỡ: HTTP/2 multiplexing cho phép hàng trăm request song song trên cùng một TCP connection — không có tương đương nào trong thế giới điện thoại. Và streaming hai chiều của gRPC (bidirectional streaming) thì hoàn toàn vượt qua mọi phép ẩn dụ analog. Đừng ép analogy quá xa — nó chỉ là điểm khởi đầu.
Cốt lõi kỹ thuật
REST — Nền tảng của web hiện đại
REST (Representational State Transfer) xây trên các ràng buộc kiến trúc: stateless, resource-oriented, uniform interface. Mỗi resource có URI riêng, thao tác qua HTTP verbs (GET, POST, PUT, DELETE, PATCH), và trạng thái chuyển qua hypermedia links.
Richardson Maturity Model phân REST thành 4 cấp:
| Level | Đặc điểm | Ví dụ |
|---|---|---|
| 0 | Single URI, single verb | POST /api cho mọi thứ |
| 1 | Multiple URIs (resources) | /users, /orders |
| 2 | HTTP verbs đúng ngữ nghĩa | GET /users/42, DELETE /orders/99 |
| 3 | HATEOAS — hypermedia controls | Response chứa links tới actions tiếp theo |
Phần lớn production API dừng ở Level 2. Level 3 (HATEOAS) lý thuyết đẹp nhưng hiếm khi được implement đầy đủ vì client thường hardcode endpoint.
REST tỏa sáng khi: public-facing API, CRUD operations, hệ thống cần browser support rộng, team muốn onboard nhanh.
gRPC — Tốc độ cho internal services
gRPC sử dụng Protocol Buffers (protobuf) làm Interface Definition Language (IDL) và serialization format. Chạy trên HTTP/2, hỗ trợ 4 dạng communication:
protobuf
// order_service.proto
syntax = "proto3";
service OrderService {
// Unary — 1 request, 1 response
rpc GetOrder(GetOrderRequest) returns (Order);
// Server streaming — 1 request, stream responses
rpc WatchOrderStatus(GetOrderRequest) returns (stream OrderStatus);
// Client streaming — stream requests, 1 response
rpc BatchCreateOrders(stream CreateOrderRequest) returns (BatchResult);
// Bidirectional streaming — stream cả hai chiều
rpc OrderChat(stream OrderMessage) returns (stream OrderMessage);
}
message GetOrderRequest {
string order_id = 1;
}
message Order {
string order_id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
int64 created_at = 5;
}Từ file .proto, gRPC generate code cho cả client lẫn server ở hầu hết ngôn ngữ phổ biến (Go, Java, Python, C#, Rust, TypeScript). Type safety được đảm bảo ở compile time — không còn chuyện client gửi sai field name mà runtime mới phát hiện.
gRPC tỏa sáng khi: service-to-service communication, low-latency requirement, polyglot microservices, streaming data.
GraphQL — Linh hoạt cho client-facing API
GraphQL giải quyết hai vấn đề kinh điển của REST: over-fetching (lấy thừa data) và under-fetching (thiếu data, phải gọi thêm endpoint).
graphql
# Schema definition
type Order {
id: ID!
customer: Customer!
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
}
type Query {
order(id: ID!): Order
orders(customerId: ID!, limit: Int = 10): [Order!]!
}
# Client query — chỉ lấy đúng những gì cần
query GetOrderSummary($orderId: ID!) {
order(id: $orderId) {
id
status
totalAmount
customer {
name
}
}
}N+1 Query Problem: Resolver cho customer bên trong Order có thể trigger 1 query cho mỗi order. Với 50 orders = 1 query orders + 50 query customers = 51 queries. Giải pháp: DataLoader — batch và cache các query trong cùng một execution cycle.
GraphQL tỏa sáng khi: mobile clients cần tối ưu bandwidth, nhiều loại client (web, mobile, IoT) cần data khác nhau từ cùng backend, API aggregation layer.
Service Mesh — Hạ tầng cho resilience
Service mesh tách logic networking (retry, timeout, circuit breaker, mTLS, load balancing) ra khỏi application code bằng sidecar pattern. Mỗi service instance có một proxy (thường là Envoy) chạy song song, intercept mọi inbound/outbound traffic.
Istio là service mesh phổ biến nhất trên Kubernetes. Nó cung cấp: traffic management (canary deployments, A/B testing), observability (distributed tracing, metrics), security (mTLS giữa mọi service). Trade-off: thêm latency (~1-2ms mỗi hop qua proxy) và operational complexity đáng kể.
Circuit Breaker Pattern
Circuit breaker hoạt động như cầu dao điện — ngắt mạch khi phát hiện lỗi liên tục để ngăn cascade failure.
Ba trạng thái:
Closed — Trạng thái bình thường. Mọi request đi qua. Circuit breaker đếm số failure trong sliding window (ví dụ: 10 failures trong 60 giây).
Open — Khi failure vượt threshold, circuit "mở". Mọi request bị reject ngay lập tức với fallback response (cached data, default value, error message). Không có request nào đến downstream service — giảm tải cho nó phục hồi.
Half-Open — Sau một khoảng thời gian (reset timeout), circuit breaker cho phép một số request thử nghiệm đi qua. Nếu thành công → quay về Closed. Nếu thất bại → quay về Open.
Timeout & Retry Strategy
Timeout không phải một con số magic. Nó cần dựa trên P99 latency của downstream service cộng buffer hợp lý. Đặt quá ngắn → false timeout. Đặt quá dài → thread pool cạn kiệt.
Exponential backoff với jitter là retry strategy chuẩn:
delay = min(base * 2^attempt + random_jitter, max_delay)
Attempt 1: 100ms + jitter
Attempt 2: 200ms + jitter
Attempt 3: 400ms + jitter
Attempt 4: 800ms + jitter (cap tại max_delay)Jitter là yếu tố sống còn. Không có jitter, khi service phục hồi, tất cả client retry đồng thời → thundering herd → service sập lại. Jitter phân tán retry theo thời gian.
Retry budget: Giới hạn tổng số retry trong một khoảng thời gian ở mức hệ thống (ví dụ: tối đa 20% request là retry). Ngăn chặn retry storm ngay cả khi từng client config hợp lý.
Idempotency — Retry an toàn
Một operation là idempotent nếu thực hiện nhiều lần cho cùng kết quả như thực hiện một lần.
| HTTP Method | Idempotent? | Safe? | Ghi chú |
|---|---|---|---|
| GET | ✅ | ✅ | Đọc data, không side effect |
| PUT | ✅ | ❌ | Thay thế toàn bộ resource |
| DELETE | ✅ | ❌ | Xoá lần 2 → vẫn 404/200 |
| POST | ❌ | ❌ | Mỗi lần tạo resource mới |
| PATCH | ❌* | ❌ | Tuỳ implementation |
POST cần xử lý đặc biệt khi retry. Giải pháp: Idempotency Key — client gửi kèm unique key (UUID) trong header. Server lưu key + response vào cache. Nếu nhận lại cùng key → trả response đã lưu thay vì xử lý lại.
POST /payments HTTP/1.1
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 100000,
"currency": "VND",
"recipient": "user_42"
}Stripe, PayPal, và hầu hết payment gateway đều implement pattern này. Không có nó, retry = duplicate payment = mất tiền thật.
Bảng so sánh tổng hợp
| Tiêu chí | REST | gRPC | GraphQL |
|---|---|---|---|
| Serialization | JSON (text) | Protobuf (binary) | JSON (text) |
| Transport | HTTP/1.1 hoặc HTTP/2 | HTTP/2 (bắt buộc) | HTTP/1.1 hoặc HTTP/2 |
| Browser support | ✅ Native | ⚠️ Cần gRPC-Web proxy | ✅ Native |
| Streaming | Hạn chế (SSE, WebSocket riêng) | ✅ 4 dạng streaming native | ⚠️ Subscriptions (WebSocket) |
| Code generation | Optional (OpenAPI) | ✅ Built-in từ .proto | Optional (codegen tools) |
| Learning curve | Thấp | Trung bình–Cao | Trung bình |
| Use case chính | Public API, CRUD | Service-to-service, streaming | Client-facing, aggregation |
| Payload size | Lớn (JSON verbose) | Nhỏ (binary compact) | Tuỳ query (linh hoạt) |
Thực chiến
Scenario 1: Kiến trúc protocol cho microservices
Một hệ thống e-commerce gồm 5 service: API Gateway, Order, Payment, Inventory, Notification. Câu hỏi đặt ra: dùng protocol gì ở đâu?
Quyết định thiết kế:
API Gateway expose REST (cho backward compatibility) và GraphQL (cho mobile client cần flexible queries) ra ngoài. Browser và third-party dễ dàng tích hợp, không cần thư viện đặc biệt.
Giữa các internal service dùng gRPC: payload nhỏ hơn 5-10x so với JSON, type safety từ protobuf, streaming cho real-time order status. Team dùng nhiều ngôn ngữ (Go cho Order, Java cho Payment) — protobuf generate client code cho cả hai.
Notification Service nhận message qua queue — đây là async communication, sẽ đi sâu ở bài tiếp theo. Nhưng nhận thấy ranh giới: sync cho những gì cần response ngay (kiểm tra tồn kho, xử lý thanh toán), async cho những gì "fire and forget" (gửi email, push notification).
Scenario 2: Implement Circuit Breaker cho Payment Service
Payment Service là dependency quan trọng nhất — nếu nó chết, order không thể hoàn tất. Cần circuit breaker với cấu hình hợp lý.
Cấu hình tham khảo (dùng Resilience4j trên JVM):
yaml
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: COUNT_BASED
slidingWindowSize: 20
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 5
slowCallDurationThreshold: 3s
slowCallRateThreshold: 80Ý nghĩa: theo dõi 20 request gần nhất. Nếu ≥50% thất bại hoặc ≥80% chậm hơn 3 giây → mở circuit. Chờ 30 giây → chuyển half-open, cho 5 request thử. Nếu ổn → đóng circuit.
Fallback strategy khi circuit mở: không phải lúc nào cũng trả lỗi. Với payment, có thể chuyển order sang trạng thái "payment_pending" và xử lý sau (eventual processing). Với inventory check, có thể dùng cached stock count với disclaimer "stock may vary". Fallback cần thiết kế cẩn thận theo business context.
Sai lầm điển hình
❌ Dùng REST cho mọi internal service call
JSON serialization/deserialization tốn CPU, payload lớn tốn bandwidth. Với hàng triệu request/giây giữa internal services, overhead cộng dồn đáng kể. gRPC với protobuf giảm payload 5-10x và tận dụng HTTP/2 multiplexing. REST vẫn phù hợp cho external API — đừng dùng nó cho mọi thứ chỉ vì quen thuộc.
❌ Không đặt timeout cho synchronous call
Mặc định nhiều HTTP client có timeout rất dài hoặc không có timeout. Một downstream service bị treo → thread caller bị block vô thời hạn → thread pool cạn kiệt → service caller cũng chết. Mọi synchronous call phải có timeout. Không ngoại lệ.
❌ Retry không có idempotency
Payment request timeout. Client retry. Nhưng request đầu tiên thực ra đã xử lý thành công — chỉ là response bị mất. Kết quả: khách hàng bị charge hai lần. Không có idempotency key, retry trên POST/mutation là canh bạc với tiền thật.
❌ Bỏ qua circuit breaker
Không có circuit breaker, khi downstream service chậm, mọi caller tiếp tục gửi request và chờ. Thread pool đầy, request queue tràn, rồi caller cũng sập. Cascade failure lan truyền ngược lên toàn bộ call chain. Circuit breaker là bắt buộc cho mọi synchronous dependency trong production.
❌ GraphQL không giới hạn query complexity
Client có thể gửi deeply nested query khai thác relationship giữa các type, khiến server thực hiện hàng nghìn database query trong một request duy nhất. Đây là attack vector thực tế. Cần thiết lập: query depth limit, complexity scoring, và rate limiting trên GraphQL endpoint.
Under the Hood
HTTP/2 Multiplexing
HTTP/1.1 bị head-of-line (HOL) blocking: mỗi TCP connection xử lý tuần tự một request tại một thời điểm. Muốn song song → mở nhiều connection (browser giới hạn 6 per domain).
HTTP/2 giải quyết bằng multiplexing: một TCP connection chia thành nhiều stream, mỗi stream mang một request-response độc lập. Các frame từ nhiều stream đan xen trên cùng connection.
gRPC tận dụng điều này triệt để — hàng nghìn concurrent RPC trên cùng một TCP connection, không tốn chi phí thiết lập connection mới. Tuy nhiên, TCP-level HOL blocking vẫn tồn tại: nếu một TCP packet mất, mọi stream trên connection đó đều bị block chờ retransmission. HTTP/3 (QUIC) giải quyết vấn đề này bằng cách chuyển sang UDP.
Protobuf Serialization
Protobuf encode data thành binary format compact. Mỗi field được biểu diễn bằng field number + wire type + value, không chứa field name như JSON.
So sánh payload size cho cùng một object Order:
JSON: {"order_id":"abc123","amount":50000,"status":"confirmed"}
→ ~58 bytes (text, human-readable)
Protobuf: 0A 06 61 62 63 31 32 33 10 D0 86 03 18 02
→ ~14 bytes (binary, compact)Protobuf nhỏ hơn ~4x trong ví dụ này. Với object phức tạp và nested structure, tỷ lệ có thể lên 5-10x. Nhưng trade-off: mất khả năng đọc bằng mắt, cần .proto file để decode, và debugging phức tạp hơn (cần công cụ chuyên dụng như grpcurl).
Schema evolution: Protobuf hỗ trợ backward/forward compatibility tốt nếu tuân thủ quy tắc: không đổi field number, dùng optional/repeated cho field mới, đánh deprecated thay vì xoá field.
Latency Tail Analysis — P50 vs P99
P50 (median) cho biết trải nghiệm "bình thường". P99 cho biết trải nghiệm của 1% request xấu nhất — và đây mới là con số quan trọng.
Tại sao? Vì trong microservices, một user request thường fan out thành nhiều internal call. Nếu service A gọi 5 downstream service song song, xác suất ít nhất 1 trong 5 rơi vào P99 là: 1 - (0.99)^5 ≈ 4.9%. Với 10 parallel calls: 1 - (0.99)^10 ≈ 9.6%. P99 của toàn hệ thống xấu hơn P99 của từng service đơn lẻ rất nhiều.
Retry amplifies tail latency. Retry chỉ trigger khi request chậm hoặc fail — tức là đúng lúc hệ thống đang overloaded. Thêm retry = thêm load vào hệ thống đang chật vật = P99 tệ hơn = trigger thêm retry. Đây là vòng lặp phản hồi dương (positive feedback loop) — nguyên nhân của phần lớn cascade failure.
Connection Pooling
HTTP/1.1: Mỗi request cần một connection. Connection establishment (TCP handshake + TLS handshake) tốn ~2-3 RTT. Connection pool tái sử dụng connection đã mở, nhưng bị giới hạn bởi HOL blocking — mỗi connection chỉ phục vụ 1 request tại một thời điểm.
HTTP/2: Một connection phục vụ mọi request qua multiplexing. Connection pool thường chỉ cần 1-2 connection per destination. Giảm memory footprint và file descriptor usage đáng kể. gRPC mặc định dùng single connection per channel — đủ cho hầu hết use case. Khi throughput cực cao, có thể cấu hình multiple subchannels.
Checklist ghi nhớ
✅ Checklist triển khai
Protocol Selection
- [ ] External API → REST hoặc GraphQL
- [ ] Internal service-to-service → gRPC (ưu tiên) hoặc REST
- [ ] Streaming requirement → gRPC streaming hoặc WebSocket
- [ ] Mobile client, flexible data needs → GraphQL
- [ ] Evaluate trade-off: performance vs developer experience vs ecosystem support
Resilience Patterns
- [ ] Circuit breaker trên mọi synchronous dependency
- [ ] Timeout cho mọi outbound call — không bao giờ vô hạn
- [ ] Retry với exponential backoff + jitter
- [ ] Retry budget ở system level (tối đa 20% traffic là retry)
- [ ] Idempotency key cho mọi mutating operation cần retry
- [ ] Fallback strategy cho mỗi circuit breaker (cached data, degraded mode, pending state)
Performance Tuning
- [ ] Chọn serialization phù hợp: protobuf cho internal, JSON cho external
- [ ] Connection pooling cấu hình đúng (HTTP/1.1 pool size, HTTP/2 channel count)
- [ ] Compression (gzip cho REST, built-in cho gRPC)
- [ ] Đo P99 latency, không chỉ P50 hoặc average
- [ ] Load test với realistic traffic pattern, bao gồm failure scenario
Monitoring & Observability
- [ ] Distributed tracing (OpenTelemetry, Jaeger) qua mọi service
- [ ] Metrics: request rate, error rate, latency percentiles (RED method)
- [ ] Circuit breaker state dashboards — biết ngay khi circuit mở
- [ ] Alert trên P99 latency spike, không chỉ error rate
- [ ] Log correlation ID xuyên suốt request chain
Bài tập luyện tập
🧠 Quiz
Câu 1: Chọn protocol phù hợp
Bạn đang thiết kế hệ thống gồm: (A) public API cho mobile app lấy user profile + posts + comments trong 1 request, (B) internal communication giữa Order Service và Inventory Service cần latency < 10ms, (C) live dashboard hiển thị real-time metrics stream.
Protocol nào cho từng trường hợp?
- [ ] A: REST, B: REST, C: WebSocket
- [x] A: GraphQL, B: gRPC unary, C: gRPC server streaming
- [ ] A: gRPC, B: GraphQL, C: REST + polling
- [ ] A: REST, B: gRPC, C: GraphQL subscription
Giải thích: (A) GraphQL — mobile cần flexible query, lấy nhiều entity trong 1 request, tránh over-fetching. (B) gRPC unary — internal service, low-latency, protobuf binary nhỏ gọn. (C) gRPC server streaming — server push metrics liên tục trên cùng HTTP/2 connection.
Bài 2: Thiết kế Circuit Breaker cho Payment Service — Intermediate
Cho context: Payment Service có P99 latency = 2 giây (bình thường), trung bình xử lý 100 request/giây, và khi degraded có thể spike lên 15 giây.
Yêu cầu:
- Xác định timeout hợp lý cho caller
- Thiết kế circuit breaker config: sliding window size, failure threshold, reset timeout
- Mô tả fallback strategy khi circuit mở
- Vẽ state machine diagram bao gồm cả slow call detection
💡 Gợi ý
- Timeout nên > P99 bình thường nhưng << degraded latency. Ví dụ: 3-5 giây.
- Sliding window: 20-50 requests. Quá nhỏ → false positive. Quá lớn → phản ứng chậm.
- Slow call threshold: dựa trên P99 normal (~2s), đặt slow call ở ~3s.
✅ Lời giải
- Timeout: 3-5 giây (> P99 2s nhưng << degraded 15s)
- Circuit breaker config: sliding window 30 requests, failure threshold 50%, slow call threshold 3s, reset timeout 30s
- Fallback: chuyển order sang
payment_pending, lưu vào retry queue xử lý sau - Cân nhắc separate circuit breaker cho different payment methods (credit card vs e-wallet)
Bài 3: Tính Retry Amplification Factor — Advanced
Cho hệ thống: Service A gọi Service B, Service B gọi Service C.
Tham số:
- Service A → B: timeout 5s, retry 2 lần (tổng 3 attempts)
- Service B → C: timeout 3s, retry 2 lần (tổng 3 attempts)
- Service C failure rate: 30%
Câu hỏi:
- Khi Service C fail, tổng số request mà Service C nhận được cho mỗi request gốc từ A là bao nhiêu?
- Tính total load amplification factor.
- Nếu thêm circuit breaker ở B→C mở sau 5 failures liên tục, load amplification thay đổi thế nào?
💡 Gợi ý
- Mỗi call từ B→C retry 3 lần (1 original + 2 retry)
- Mỗi call từ A→B cũng retry 3 lần, mỗi attempt trigger B gọi C
✅ Lời giải
- Worst case: A gửi 3 request đến B, mỗi cái gửi 3 request đến C → 9 request đến C cho 1 request gốc
- Load amplification factor = 9x
- Với circuit breaker: sau 5 failures, B ngừng gọi C → blast radius giới hạn. Amplification giảm về ~5-6x rồi xuống ~3x (chỉ còn A retry đến B, B trả fallback ngay)
- Thực tế tệ hơn nếu timeout xảy ra thay vì fast failure — mỗi retry tốn thêm thời gian chờ