Giao diện
Microservices — Kiến trúc vi dịch vụ
Năm 2020, một đội ngũ platform tại một công ty fintech Đông Nam Á quyết định tách monolith thanh toán thành 12 microservices trong vòng 3 tháng. Kết quả: 6 tháng sau, hệ thống có latency p99 tăng gấp 4 lần, mỗi lần deploy cần coordinate giữa 5 team, và incident on-call trở thành cơn ác mộng vì không ai hiểu request đi qua bao nhiêu service. Họ đã xây dựng một distributed monolith — tệ hơn cả monolith ban đầu.
Microservices không phải là chia nhỏ code rồi deploy riêng. Đó là một chiến lược tổ chức hệ thống đòi hỏi bạn giải quyết đồng thời ba bài toán: service discovery (tìm nhau), resilience (chịu lỗi), và observability (nhìn thấy). Nếu bạn chỉ giải quyết một trong ba, hệ thống sẽ sụp đổ theo cách mà monolith không bao giờ gặp.
Bài viết này đi sâu vào cơ chế kỹ thuật đằng sau kiến trúc microservices — từ circuit breaker giữ hệ thống đứng vững khi một service chết, đến service mesh kiểm soát traffic ở tầng infrastructure, và distributed tracing cho phép bạn trace một request xuyên qua 20 service trong vài giây.
Bức tranh tư duy
Hãy hình dung một thành phố hiện đại. Mỗi quận (service) có chức năng riêng: quận tài chính xử lý thanh toán, quận hành chính quản lý hồ sơ người dùng, quận logistics lo vận chuyển. Các quận giao tiếp qua hệ thống đường giao thông (network) với biển chỉ dẫn (service discovery), đèn giao thông (circuit breaker), và camera giám sát toàn thành phố (observability).
Khi nào phép so sánh này sai? Thành phố có hạ tầng cố định — đường xá không tự co giãn. Nhưng trong microservices, số lượng instance của mỗi service thay đổi liên tục (auto-scaling). Một service có thể có 3 instance lúc thấp tải và 50 instance lúc peak. Đây là lý do service discovery phải dynamic, không phải hardcode địa chỉ IP.
Cốt lõi kỹ thuật
Service Discovery — Tìm đúng service trong mạng lưới động
Khi Order Service cần gọi Payment Service, nó không thể hardcode payment:8003 vì Payment Service có thể đang chạy trên 10 instance khác nhau, trên các node khác nhau trong cluster. Service Discovery giải quyết bài toán: "Service X đang chạy ở đâu, trên port nào, instance nào healthy?"
Client-side Discovery
Service gọi trực tiếp hỏi Service Registry (Consul, etcd, ZooKeeper) để lấy danh sách instance, rồi tự quyết định gọi instance nào thông qua load balancing algorithm ở phía client.
Ưu điểm: Client có toàn quyền chọn strategy (weighted, least-connections, consistent hashing). Không có single point of failure ở tầng proxy.
Nhược điểm: Mỗi service phải implement logic discovery và load balancing. Coupling giữa business code và infrastructure code.
Ví dụ với Spring Cloud và Consul:
java
@Configuration
public class PaymentClientConfig {
@Bean
@LoadBalanced // Tự động resolve tên service qua registry
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class OrderService {
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public PaymentResult chargeOrder(Order order) {
// "payment-service" được resolve qua Consul
// Load balancer chọn instance tự động
return restTemplate.postForObject(
"http://payment-service/api/v1/charge",
new ChargeRequest(order.getId(), order.getTotal()),
PaymentResult.class
);
}
}Server-side Discovery
Client gọi qua một load balancer trung gian (Nginx, AWS ALB, Kubernetes Service). Load balancer biết danh sách instance và tự route.
Ưu điểm: Client đơn giản — chỉ cần biết DNS name. Infrastructure team quản lý routing. Nhược điểm: Load balancer trở thành single point cần high availability. Thêm một network hop = thêm latency.
Trong Kubernetes, server-side discovery là mặc định: ClusterIP Service tạo virtual IP, kube-proxy handle routing đến các Pod healthy. Đây là lý do hầu hết team chạy K8s không cần Consul riêng.
Circuit Breaker — Ngắt mạch khi service downstream chết
Khi Payment Service chậm hoặc chết, nếu Order Service cứ tiếp tục gửi request, thread pool sẽ bị exhaust, rồi cascading failure lan ra toàn hệ thống. Circuit Breaker hoạt động giống cầu dao điện: khi phát hiện lỗi vượt ngưỡng, nó ngắt mạch — trả lỗi ngay lập tức thay vì chờ timeout.
Ba trạng thái của Circuit Breaker:
| Trạng thái | Hành vi | Khi nào chuyển |
|---|---|---|
| Closed | Cho request đi qua bình thường. Đếm lỗi trong sliding window | Failure rate vượt threshold → Open |
| Open | Reject tất cả request ngay lập tức. Trả fallback response | Sau wait duration → Half-Open |
| Half-Open | Cho một số request thử. Nếu thành công → Closed | Success → Closed, Fail → Open |
Ví dụ với Resilience4j (Java):
java
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreaker paymentCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // Mở khi 50% request lỗi
.slowCallRateThreshold(80) // Mở khi 80% request chậm
.slowCallDurationThreshold(Duration.ofSeconds(3))
.slidingWindowType(SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10) // Đánh giá trên 10 request gần nhất
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(3)
.build();
return CircuitBreaker.of("payment-service", config);
}
}
@Service
public class OrderService {
private final CircuitBreaker circuitBreaker;
private final PaymentClient paymentClient;
public PaymentResult processPayment(Order order) {
// Wrap call trong circuit breaker
return CircuitBreaker.decorateSupplier(circuitBreaker,
() -> paymentClient.charge(order))
.recover(CallNotPermittedException.class,
ex -> PaymentResult.pending("Circuit open — retry later"))
.recover(TimeoutException.class,
ex -> PaymentResult.pending("Payment timeout — queued"))
.get();
}
}Lưu ý production: Circuit breaker cần được monitor. Một circuit mở quá lâu có thể nghĩa là downstream service đã chết hoàn toàn và cần alert cho on-call engineer. Kết hợp với retry (có backoff) và timeout để tạo resilience layer hoàn chỉnh.
Sidecar Pattern và Service Mesh
Sidecar Pattern — Tách infrastructure concern ra khỏi business code
Thay vì mỗi service tự implement circuit breaker, retry, mTLS, tracing, rate limiting — sidecar proxy (thường là Envoy) chạy song song với mỗi service instance và xử lý toàn bộ network concern.
Business code không biết sidecar tồn tại. Order App gọi http://payment-service:8080/charge — Envoy proxy intercept request, thêm mTLS encryption, apply circuit breaker policy, inject trace headers, rồi forward đến Envoy proxy bên Payment Service. Toàn bộ transparent với application code.
Service Mesh — Quản lý traffic toàn mạng lưới
Service Mesh mở rộng sidecar pattern lên toàn hệ thống. Nó gồm hai thành phần:
- Data Plane: Tất cả sidecar proxy (Envoy) tạo thành mesh xử lý traffic
- Control Plane: Quản lý cấu hình, policy, certificate cho toàn bộ data plane
So sánh hai service mesh phổ biến:
| Tiêu chí | Istio | Linkerd |
|---|---|---|
| Proxy | Envoy (C++) | linkerd2-proxy (Rust) |
| Tài nguyên | Nặng (~100MB/sidecar) | Nhẹ (~20MB/sidecar) |
| Tính năng | Rất phong phú (traffic management, security, observability) | Tập trung vào reliability và observability |
| Độ phức tạp | Cao — learning curve dốc | Thấp hơn — dễ adopt |
| Khi nào chọn | Cần fine-grained traffic control, canary, fault injection | Cần mesh nhẹ, team nhỏ, không cần advanced traffic management |
Khi nào cần service mesh? Khi hệ thống có từ 15-20 service trở lên, khi bạn cần mTLS giữa tất cả service, hoặc khi bạn muốn canary deployment và traffic splitting mà không đụng vào application code. Dưới 10 service, overhead của service mesh thường không đáng.
Observability — Nhìn thấy bên trong hệ thống phân tán
Trong monolith, bạn đọc log file là thấy toàn bộ flow. Trong microservices, một request đi qua 8 service, mỗi service có log riêng, metric riêng. Observability là khả năng hiểu trạng thái bên trong hệ thống chỉ từ dữ liệu nó phát ra.
Ba trụ cột của Observability:
| Trụ cột | Câu hỏi trả lời | Công cụ phổ biến | Dữ liệu |
|---|---|---|---|
| Metrics | "Hệ thống có healthy không? Latency bao nhiêu?" | Prometheus + Grafana | Số liệu dạng time-series (request rate, error rate, duration) |
| Logs | "Chuyện gì đã xảy ra với request cụ thể này?" | Loki, Elasticsearch | Structured log events với context |
| Traces | "Request này đi qua những service nào, mỗi bước mất bao lâu?" | Jaeger, Zipkin, Tempo | Span tree với timing và metadata |
Distributed Tracing với OpenTelemetry
OpenTelemetry (OTel) là bộ tiêu chuẩn vendor-neutral cho instrumentation. Nó thu thập traces, metrics, logs rồi export sang backend tùy chọn (Jaeger, Datadog, Grafana Cloud).
Mỗi Span đại diện cho một đơn vị công việc (một HTTP call, một database query, một message publish). Các span liên kết với nhau qua trace_id và parent_span_id tạo thành trace tree. Khi request chậm, bạn nhìn trace tree là biết ngay bottleneck nằm ở span nào.
Cấu hình OTel cho Node.js service:
typescript
// tracing.ts — Khởi tạo trước khi import bất kỳ module nào
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: '2.1.0',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'http://otel-collector:4318/v1/metrics',
}),
exportIntervalMillis: 15000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true },
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0));
});Lưu ý production: Auto-instrumentation bắt được HTTP calls và database queries tự động. Nhưng business logic quan trọng (ví dụ: "đã validate coupon", "đã tính phí shipping") cần manual span để trace có ý nghĩa cho việc debug.
Thực chiến
Migration từ Monolith: Strangler Fig Pattern
Strangler Fig là chiến lược migration an toàn nhất: bạn không rewrite monolith từ đầu, mà dần dần chuyển từng feature sang microservice mới, trong khi monolith vẫn chạy. Tên gọi lấy từ loài cây bóp nghẹt — mọc bao quanh cây chủ rồi thay thế hoàn toàn.
Quy tắc migration thực chiến:
Bắt đầu từ boundary rõ ràng nhất — Module nào có ít dependency nhất vào phần còn lại của monolith, tách trước. Thường là notification, reporting, hoặc payment.
Anti-Corruption Layer (ACL) — Tạo layer trung gian giữa service mới và monolith cũ. ACL translate giữa hai domain model, ngăn service mới bị "nhiễm" bởi data model cũ.
Database tách sau code — Khi mới tách, service mới có thể vẫn đọc database cũ (qua read-only view). Tách database là bước cuối, khi bạn đã chắc chắn service mới stable.
Feature flag mọi thứ — Route traffic giữa monolith và service mới qua feature flag. Rollback trong vài giây nếu service mới có lỗi.
Kubernetes Deployment Patterns cho Microservices
Mỗi service là một Deployment + Service + HPA
yaml
# payment-service.yaml — Production-grade K8s manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
labels:
app: payment-service
version: v2.1.0
spec:
replicas: 3
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
version: v2.1.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: payment
image: registry.internal/payment-service:v2.1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: payment-db-secret
key: host
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector.monitoring:4317"
---
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector:
app: payment-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"Canary Deployment với Istio
Khi deploy version mới, bạn không muốn 100% traffic đổ vào ngay. Istio cho phép traffic splitting — gửi 5% traffic vào v2, monitor, rồi tăng dần.
yaml
# VirtualService — Route 95% traffic vào v1, 5% vào v2
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment-service
spec:
hosts:
- payment-service
http:
- route:
- destination:
host: payment-service
subset: v1
weight: 95
- destination:
host: payment-service
subset: v2
weight: 5
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
subsets:
- name: v1
labels:
version: v1.9.0
- name: v2
labels:
version: v2.0.0Sai lầm điển hình
Sai lầm 1: Distributed Monolith
| Mô tả | |
|---|---|
| SAI | Tách thành 15 service nhưng tất cả dùng chung một database, deploy cùng lúc, và mỗi service gọi trực tiếp 5-6 service khác qua synchronous HTTP |
| ĐÚNG | Mỗi service sở hữu database riêng. Giao tiếp async qua event bus khi có thể. Service chỉ cần biết contract (API schema), không biết implementation bên trong |
| Hậu quả | Thay đổi schema một bảng → deploy lại toàn bộ 15 service. Một service chậm → cascading failure. Team không thể deploy độc lập |
Sai lầm 2: Thiếu Observability từ đầu
| Mô tả | |
|---|---|
| SAI | Build 10 service trước, rồi mới nghĩ đến monitoring. Log ở dạng plain text, không có trace ID, mỗi service log format khác nhau |
| ĐÚNG | Observability là day-zero concern. Setup tracing, structured logging, và metrics cùng lúc với service đầu tiên. Mọi log phải có trace_id, service_name, request_id |
| Hậu quả | Incident xảy ra → mất hàng giờ grep log trên 10 service. Không trace được request flow. Mean Time To Recovery (MTTR) tăng 5-10x |
Sai lầm 3: Tách service quá nhỏ (Nano-services)
| Mô tả | |
|---|---|
| SAI | Mỗi entity một service: UserService, UserAddressService, UserPreferenceService, UserNotificationSettingService. Tổng cộng 40 service cho một hệ thống nhỏ |
| ĐÚNG | Tách theo business domain boundary (DDD Bounded Context). UserService quản lý toàn bộ user domain. Chỉ tách khi có lý do rõ ràng về scaling hoặc team ownership |
| Hậu quả | Network latency cộng dồn. Operational overhead (CI/CD pipeline × 40, monitoring × 40). Cognitive load cho developer quá lớn |
Sai lầm 4: Synchronous chain quá dài
| Mô tả | |
|---|---|
| SAI | Client → API Gateway → Order → Inventory → Payment → Notification → Email. Chuỗi 6 sync calls. Latency = tổng latency 6 service. Một service chết = cả chain chết |
| ĐÚNG | Giữ sync chain tối đa 2-3 hop. Notification và Email nên async qua message queue. Order chỉ cần confirm "payment initiated", không cần đợi email gửi xong |
| Hậu quả | p99 latency tăng tuyến tính với số hop. Availability giảm theo công thức: A_total = A1 × A2 × ... × An. Với 6 service 99.9% → availability tổng chỉ còn 99.4% |
Sai lầm 5: Bỏ qua Contract Testing
| Mô tả | |
|---|---|
| SAI | Team Payment thay đổi response format từ { "status": "ok" } sang { "result": "success" } mà không báo team Order. Integration test chỉ chạy trên staging |
| ĐÚNG | Sử dụng Consumer-Driven Contract Testing (Pact). Consumer (Order) định nghĩa kỳ vọng, Provider (Payment) verify contract trong CI. Breaking change bị chặn trước khi merge |
| Hậu quả | Service deploy thành công ở CI nhưng fail ở production vì contract bị phá vỡ. Incident phát hiện bởi user, không phải automated test |
Under the Hood
Latency overhead của Service Mesh
Mỗi request qua sidecar proxy thêm 0.5-2ms latency (Envoy). Trong chain 5 service, overhead có thể là 5-10ms — thường chấp nhận được. Nhưng với latency-sensitive workload (trading, real-time gaming), đây là trade-off cần cân nhắc.
| Cấu hình | Latency overhead/hop | Memory overhead/pod | Phù hợp khi |
|---|---|---|---|
| Không mesh | 0ms | 0MB | < 5 service, team nhỏ |
| Linkerd | ~0.5ms | ~20MB | 10-30 service, cần mTLS + observability |
| Istio | ~1-2ms | ~80-100MB | 30+ service, cần advanced traffic management |
Cost Trade-off: Khi nào microservices đáng giá?
Microservices không hề "rẻ" — chúng đổi complexity lấy scalability và team autonomy.
Operational Cost = (Số service) × (CI/CD + Monitoring + Infra)| Quy mô | Kiến trúc khuyến nghị | Lý do |
|---|---|---|
| 1-3 dev, MVP | Modular Monolith | Overhead quản lý microservices lớn hơn benefit |
| 5-15 dev, 1 product | Modular Monolith → tách 2-5 service cho phần scale khác nhau | Tách theo nhu cầu, không tách theo trend |
| 20+ dev, nhiều team | Microservices với platform team | Team autonomy trở thành bottleneck chính, microservices giải quyết organizational scaling |
| 100+ dev | Microservices + Service Mesh + Platform Engineering | Cần standardize cross-cutting concerns ở tầng infrastructure |
Kubernetes Resource Management cho Microservices
Resource requests quyết định scheduling — Pod được đặt lên node nào. Resource limits quyết định throttling — Pod bị kill khi vượt memory limit (OOMKilled) hoặc bị throttle khi vượt CPU limit.
Công thức ước tính resource cho một service:
CPU request = p50 CPU usage trong 7 ngày × 1.2 (buffer 20%)
CPU limit = p99 CPU usage trong 7 ngày × 1.5
Memory request = p99 memory usage (memory không nên burst)
Memory limit = Memory request × 1.3Sai lầm phổ biến: Set requests = limits cho mọi service. Điều này tắt bursting — service không thể dùng thêm CPU khi cần, dẫn đến latency spike lúc peak. Chỉ set requests = limits khi bạn cần Guaranteed QoS class cho service critical.
Checklist ghi nhớ
✅ Checklist triển khai
Service Discovery & Communication
- [ ] Mỗi service đăng ký vào service registry hoặc sử dụng Kubernetes DNS
- [ ] Không hardcode IP/port của service khác — luôn dùng service name
- [ ] Phân biệt rõ sync call (HTTP/gRPC) và async event (message queue)
- [ ] Sync chain không quá 3 hop — chuyển sang async cho các bước không cần response ngay
Resilience
- [ ] Circuit breaker cho mọi outbound call đến service khác
- [ ] Timeout được set cho mọi HTTP/gRPC call (không dùng default infinity)
- [ ] Retry với exponential backoff và jitter — tránh thundering herd
- [ ] Fallback response khi circuit open (cached data, degraded response, queue for later)
Observability
- [ ] Distributed tracing được setup từ ngày đầu (OpenTelemetry)
- [ ] Mọi log đều có trace_id, service_name, và structured format (JSON)
- [ ] Metrics RED (Rate, Error, Duration) cho mọi service endpoint
- [ ] Alert rule cho error rate > threshold và latency p99 > SLO
Deployment & Operations
- [ ] Mỗi service có CI/CD pipeline riêng, deploy độc lập
- [ ] Health check endpoint: /health/ready (cho routing) và /health/live (cho restart)
- [ ] Resource requests và limits được set dựa trên production profiling
- [ ] Canary deployment hoặc blue-green cho service critical
Bài tập luyện tập
Bài 1: Thiết kế Circuit Breaker Configuration
Hệ thống e-commerce có Order Service gọi Payment Service. Dữ kiện:
- Payment Service có SLA: latency p99 < 2s, availability 99.9%
- Trung bình 500 requests/phút vào giờ cao điểm
- Khi Payment down, order vẫn phải được tiếp nhận (queued for later)
Yêu cầu: Thiết kế circuit breaker configuration bao gồm:
- Threshold, window size, wait duration
- Fallback strategy khi circuit open
- Alert rule cho circuit state change
🧠 Quiz
Câu hỏi: Tại sao slidingWindowType: TIME_BASED thường được ưu tiên hơn COUNT_BASED cho production system?
- A. TIME_BASED nhanh hơn
- B. TIME_BASED phản ánh đúng tình trạng gần đây hơn khi traffic thay đổi
- C. COUNT_BASED không hỗ trợ slow call detection
- D. TIME_BASED dùng ít memory hơn
Đáp án: B
Gợi ý thiết kế
java
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
// SLA p99 = 2s → slow call = 2s
.slowCallDurationThreshold(Duration.ofSeconds(2))
.slowCallRateThreshold(70) // 70% slow → open
.failureRateThreshold(50) // 50% fail → open
.slidingWindowType(SlidingWindowType.TIME_BASED)
.slidingWindowSize(30) // Đánh giá trong 30 giây
.minimumNumberOfCalls(10) // Ít nhất 10 calls mới evaluate
.waitDurationInOpenState(Duration.ofSeconds(15))
.permittedNumberOfCallsInHalfOpenState(5)
.build();Fallback strategy:
- Khi circuit open → ghi order vào
pending_paymentsqueue (Kafka/SQS) - Background worker retry payment khi circuit close
- User nhận response "Đơn hàng đã được tiếp nhận, thanh toán đang xử lý"
Alert rule (Prometheus):
promql
# Alert khi circuit chuyển sang Open
resilience4j_circuitbreaker_state{service="payment", state="open"} == 1Bài 2: Phân tích Trace để tìm Bottleneck
Dưới đây là trace của một request tạo order (tổng thời gian 4.2 giây):
Trace ID: abc-789-xyz
├── [250ms] api-gateway.route
│ ├── [1800ms] order-service.create
│ │ ├── [50ms] order-service.validate
│ │ ├── [1200ms] inventory-service.reserve ← ???
│ │ │ ├── [1150ms] inventory-db.query ← ???
│ │ ├── [500ms] payment-service.charge
│ ├── [2100ms] notification-service.send
│ │ ├── [100ms] template-service.render
│ │ ├── [2000ms] email-provider.send ← ???Yêu cầu:
- Xác định 2 bottleneck chính
- Đề xuất giải pháp cho mỗi bottleneck
- Tính latency tối ưu sau khi fix
🧠 Quiz
Câu hỏi: Notification service mất 2100ms, nhưng user không cần đợi email. Giải pháp tối ưu nhất là gì?
- A. Tăng timeout cho notification service
- B. Cache template ở notification service
- C. Chuyển notification sang async (publish event, notification subscribe)
- D. Thêm nhiều instance notification service
Đáp án: C
Phân tích chi tiết
Bottleneck 1: inventory-db.query — 1150ms
- Database query quá chậm. Có thể thiếu index, hoặc lock contention khi nhiều request reserve cùng lúc
- Fix: Thêm index cho
inventorytable trên cộtproduct_id+warehouse_id. Xem xét dùngSELECT ... FOR UPDATE SKIP LOCKEDđể giảm lock contention - Target: giảm từ 1150ms → 50-100ms
Bottleneck 2: notification-service.send — 2100ms (blocking)
- Notification là sync call nhưng user không cần đợi
- Fix: Order Service publish
OrderCreatedevent vào Kafka. Notification Service subscribe và xử lý async - Target: loại bỏ hoàn toàn khỏi critical path
Latency sau khi fix:
Trước: 250 + max(1800, 2100) = 250 + 2100 = 2350ms (nếu parallel)
hoặc 250 + 1800 + 2100 = 4150ms (nếu sequential)
Sau fix:
- Notification async → loại khỏi critical path
- Inventory query: 1200ms → 150ms
- New total: 250 + (50 + 150 + 500) = 950msBài 3: Strangler Fig Migration Plan
Bạn có monolith e-commerce với các module: User, Product Catalog, Order, Payment, Inventory, Notification, Reporting. Team có 12 developers.
Yêu cầu: Lập kế hoạch migration 6 tháng:
- Xác định thứ tự tách service (và lý do)
- Vẽ architecture ở mỗi phase
- Xác định rủi ro chính và mitigation strategy
🧠 Quiz
Câu hỏi: Module nào nên tách đầu tiên trong hệ thống trên?
- A. Order — vì nó quan trọng nhất
- B. Notification — vì ít dependency nhất và dễ async hóa
- C. User — vì mọi service khác đều cần
- D. Product Catalog — vì traffic nhiều nhất
Đáp án: B
Gợi ý kế hoạch
Tháng 1-2: Notification + Reporting
- Lý do: Ít dependency, có thể async hoàn toàn, risk thấp
- Setup event bus (Kafka), monolith publish event, Notification/Reporting subscribe
- Không cần tách database ngay — read từ shared DB qua read replica
Tháng 2-3: Payment
- Lý do: Business domain rõ ràng, compliance requirement cần isolation
- Anti-corruption layer giữa Payment Service mới và Order module cũ
- Database mới riêng cho payment (PCI compliance)
Tháng 3-5: Order + Inventory
- Lý do: Hai module coupling chặt, tách cùng lúc giảm integration complexity
- Strangler proxy route traffic dần (10% → 50% → 100%)
- Event sourcing cho order state changes
Tháng 5-6: User + Product Catalog
- Lý do: Core domain, tách cuối vì mọi service đều phụ thuộc
- User service cung cấp auth token, các service khác validate qua JWT (không gọi User service mỗi request)
Rủi ro chính:
- Data consistency giữa monolith DB và service DB mới → Mitigation: dual-write với reconciliation job
- Team chưa quen K8s → Mitigation: platform team setup template, training sprint đầu
- Feature development bị slow down trong migration → Mitigation: feature flag, migration chỉ chiếm 30% bandwidth mỗi sprint
Liên kết học tiếp
Nhớ: Microservices là giải pháp cho bài toán tổ chức (team autonomy, independent deployment), không phải bài toán kỹ thuật đơn thuần. Nếu một team 5 người có thể quản lý monolith tốt, đừng tách chỉ vì "ai cũng dùng microservices". Chi phí vận hành hệ thống phân tán luôn lớn hơn bạn nghĩ.