Skip to content

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áiHành viKhi nào chuyển
ClosedCho request đi qua bình thường. Đếm lỗi trong sliding windowFailure rate vượt threshold → Open
OpenReject tất cả request ngay lập tức. Trả fallback responseSau wait duration → Half-Open
Half-OpenCho một số request thử. Nếu thành công → ClosedSuccess → 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íIstioLinkerd
ProxyEnvoy (C++)linkerd2-proxy (Rust)
Tài nguyênNặng (~100MB/sidecar)Nhẹ (~20MB/sidecar)
Tính năngRất phong phú (traffic management, security, observability)Tập trung vào reliability và observability
Độ phức tạpCao — learning curve dốcThấp hơn — dễ adopt
Khi nào chọnCần fine-grained traffic control, canary, fault injectionCầ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ộtCâu hỏi trả lờiCông cụ phổ biếnDữ liệu
Metrics"Hệ thống có healthy không? Latency bao nhiêu?"Prometheus + GrafanaSố 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, ElasticsearchStructured 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, TempoSpan 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_idparent_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:

  1. 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.

  2. 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ũ.

  3. 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.

  4. 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.0

Sai lầm điển hình

Sai lầm 1: Distributed Monolith

Mô tả
SAITá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
ĐÚNGMỗ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ả
SAIBuild 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
ĐÚNGObservability 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ả
SAIMỗi entity một service: UserService, UserAddressService, UserPreferenceService, UserNotificationSettingService. Tổng cộng 40 service cho một hệ thống nhỏ
ĐÚNGTá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ả
SAIClient → 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
ĐÚNGGiữ 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ả
SAITeam 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
ĐÚNGSử 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ìnhLatency overhead/hopMemory overhead/podPhù hợp khi
Không mesh0ms0MB< 5 service, team nhỏ
Linkerd~0.5ms~20MB10-30 service, cần mTLS + observability
Istio~1-2ms~80-100MB30+ 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 scalabilityteam autonomy.

Operational Cost = (Số service) × (CI/CD + Monitoring + Infra)
Quy môKiến trúc khuyến nghịLý do
1-3 dev, MVPModular MonolithOverhead quản lý microservices lớn hơn benefit
5-15 dev, 1 productModular Monolith → tách 2-5 service cho phần scale khác nhauTách theo nhu cầu, không tách theo trend
20+ dev, nhiều teamMicroservices với platform teamTeam autonomy trở thành bottleneck chính, microservices giải quyết organizational scaling
100+ devMicroservices + Service Mesh + Platform EngineeringCầ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.3

Sai 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:

  1. Threshold, window size, wait duration
  2. Fallback strategy khi circuit open
  3. 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_payments queue (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"} == 1

Bà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:

  1. Xác định 2 bottleneck chính
  2. Đề xuất giải pháp cho mỗi bottleneck
  3. 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 inventory table trên cột product_id + warehouse_id. Xem xét dùng SELECT ... 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 OrderCreated event 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) = 950ms

Bà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:

  1. Xác định thứ tự tách service (và lý do)
  2. Vẽ architecture ở mỗi phase
  3. 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ĩ.