Product Management
Đăng nhập
ESC

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

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

Microservices performance — service chain

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

Nếu bạn từng test hiệu năng của một ứng dụng monolith (một khối liền mạch), bạn sẽ thấy mọi thứ khá "gọn gàng": một request đến, server xử lý, trả về kết quả. Đo latency, throughput, error rate là xong. Nhưng khi bước vào thế giới microservices — nơi một hệ thống được xẻ nhỏ thành hàng chục, thậm chí hàng trăm service độc lập — bức tranh thay đổi hoàn toàn.

Trong kiến trúc microservices, một request tưởng chừng đơn giản của người dùng có thể kích hoạt cả một chuỗi (chain) service gọi lẫn nhau. Người dùng bấm "Đặt hàng", nhưng bên trong hệ thống có thể có 10, 20, thậm chí 50 lượt gọi service nội bộ diễn ra. Điều đáng sợ là: latency không cộng dồn một cách hiền lành. Một service chậm 100ms tưởng như vô hại, nhưng nếu nó nằm trong chuỗi tuần tự với 9 service khác, hoặc bị gọi lặp lại trong một vòng lặp, con số đó có thể phình lên thành vài giây trước mắt người dùng.

Bài này dạy bạn cách nhìn hệ thống như một chuỗi phụ thuộc, cách latency tích lũy qua fan-out (một service gọi nhiều service con), cách xác định service nào là "mắt xích yếu" kéo tụt cả chuỗi, và cách thiết kế bài test hiệu năng phản ánh đúng thực tế phân tán này. Đây là kỹ năng phân biệt một Performance Engineer chỉ biết chạy tool với một người thực sự hiểu hệ thống.

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

Fan-out và latency cộng dồn

Fan-out là hiện tượng một service, khi nhận một request, phải gọi tiếp nhiều service khác để hoàn thành công việc. Hãy hình dung một request checkout điển hình của một sàn thương mại điện tử:

Checkout request (API Gateway)
  ├─→ Auth Service        (50ms)   — xác thực người dùng
  ├─→ Cart Service        (80ms)   — lấy giỏ hàng
  ├─→ Inventory Service   (120ms)  — kiểm tra tồn kho
  ├─→ Pricing Service     (60ms)   — tính giá, khuyến mãi
  ├─→ Payment Service     (300ms)  — gọi cổng thanh toán
  └─→ Notification Service(40ms)   — gửi email/SMS

Câu hỏi mấu chốt: tổng latency người dùng cảm nhận là bao nhiêu? Câu trả lời phụ thuộc hoàn toàn vào việc các lời gọi này chạy tuần tự (sequential) hay song song (parallel).

  • Nếu tuần tự: 50 + 80 + 120 + 60 + 300 + 40 = 650ms. Mỗi service phải chờ service trước xong mới bắt đầu.
  • Nếu song song: latency ≈ service chậm nhất = 300ms (Payment). Các service khác chạy đồng thời và "ẩn" dưới bóng của service chậm nhất.
Đây là bài học đầu tiên và quan trọng nhất: cùng một tập service, cách điều phối (orchestration) quyết định hiệu năng. Một trong những nhiệm vụ của performance testing microservices là phát hiện những chỗ lẽ ra nên gọi song song nhưng lại đang gọi tuần tự.

Latency amplification — hiệu ứng khuếch đại

Vấn đề nguy hiểm hơn fan-out phẳng là fan-out nhiều tầng (nested/chained). Service A gọi B, B lại gọi C và D, C lại gọi E... Latency không chỉ cộng mà còn khuếch đại theo độ sâu của chuỗi. Nếu mỗi tầng thêm một chút overhead mạng (network hop) và serialization, chuỗi 5 tầng có thể tiêu tốn 5 lần chi phí "đi lại" giữa các service ngay cả khi mỗi service tự nó xử lý rất nhanh.

Có một quy tắc thực nghiệm đáng nhớ: mỗi network hop nội bộ trong data center thường tốn 1–5ms chỉ riêng cho việc truyền tin, chưa tính xử lý. Với 30 hop, riêng chi phí mạng đã là 30–150ms — vô hình với người viết code từng service nhưng rất thật với người dùng cuối.

Tail latency — kẻ thù thầm lặng của chuỗi

Đây là khái niệm mà nếu bỏ qua, bạn sẽ không bao giờ hiểu tại sao hệ thống microservices "chậm ngẫu nhiên". Giả sử mỗi service có p99 latency (99% request nhanh hơn ngưỡng này) là 100ms — nghĩa là chỉ 1% request bị chậm. Nghe rất tốt. Nhưng nếu một request người dùng phải gọi 100 service tuần tự, xác suất để cả 100 lời gọi đều rơi vào nhóm 99% nhanh là 0.99^100 ≈ 37%. Nói cách khác, 63% request người dùng sẽ dính ít nhất một service chậm.

Đây gọi là tail latency amplification. Trong microservices, p99 của từng service không quan trọng bằng p99 của cả chuỗi. Một service có tail latency xấu (thỉnh thoảng chậm do GC, do cache miss, do lock database) sẽ "lây" sự chậm chạp của nó ra toàn hệ thống. Khi test hiệu năng service chain, bạn phải đo cả latency đầu-cuối lẫn latency từng chặng, và đặc biệt chú ý phân vị cao (p95, p99, p99.9) chứ không chỉ trung bình.

Distributed tracing — bản đồ để đo được chuỗi

Bạn không thể tối ưu thứ bạn không nhìn thấy. Trong monolith, một profiler là đủ. Trong microservices, bạn cần distributed tracing: mỗi request được gắn một trace ID xuyên suốt, mỗi chặng gọi service tạo ra một "span" có thời gian bắt đầu/kết thúc. Ghép các span lại, bạn có bức tranh thác nước (waterfall) cho thấy chính xác thời gian tiêu ở đâu. Các công cụ phổ biến là Jaeger, Zipkin, hoặc chuẩn OpenTelemetry. Khi test tải, bật tracing (dù chỉ sampling một phần) là cách duy nhất để trả lời câu hỏi "service nào đang là điểm nghẽn của chuỗi".

Tình huống thực tế

Ví dụ 1: Sàn TMĐT Việt Nam và cú checkout 2.4 giây

Một sàn thương mại điện tử tầm trung ở Việt Nam — ta gọi là "ShopViet" — nhận phàn nàn rằng bước thanh toán chậm, tỷ lệ bỏ giỏ hàng ở bước cuối cao bất thường. Đội QA chạy load test với k6 mô phỏng 2.000 người dùng đồng thời và đo được p95 của endpoint /checkout2.400ms, trong khi SLA nội bộ đặt ra là 800ms.

Nhìn vào từng service riêng lẻ, không service nào "có vẻ" chậm: Auth 45ms, Cart 70ms, Inventory 110ms, Payment 320ms. Cộng lại nếu tuần tự cũng chỉ khoảng 900ms. Vậy 1.500ms còn lại ở đâu?

Đội bật distributed tracing với Jaeger trong lúc chạy tải và phát hiện thủ phạm: Inventory Service đang bị gọi trong một vòng lặp. Với mỗi sản phẩm trong giỏ, code gọi Inventory một lần riêng (đây gọi là vấn đề N+1 request). Giỏ hàng trung bình mùa sale có 12 sản phẩm, nghĩa là 12 lời gọi Inventory tuần tự × 110ms ≈ 1.320ms chỉ riêng cho kiểm tra tồn kho. Trace waterfall cho thấy rõ 12 span Inventory xếp hàng nối đuôi nhau.

Bài học rút ra: Con số tổng đầu-cuối che giấu vấn đề. Chỉ khi phân rã theo trace mới thấy được một service "nhanh" bị gọi lặp lại đã hủy hoại hiệu năng. Giải pháp là gộp thành một lời gọi batch (getInventory(productIds[])) — kéo p95 checkout từ 2.400ms xuống 780ms mà không cần tối ưu bản thân Inventory Service một dòng nào.

Ví dụ 2: Fintech Đông Nam Á và hiệu ứng tail latency dây chuyền

Một công ty fintech ví điện tử phục vụ thị trường Đông Nam Á có kiến trúc microservices sâu: một giao dịch chuyển tiền đi qua khoảng 18 service (KYC, fraud check, ledger, notification, currency...). Trong điều kiện tải nhẹ, mọi thứ mượt mà, p99 khoảng 400ms. Nhưng vào giờ cao điểm, p99 nhảy vọt lên 3.100ms một cách khó hiểu, dù CPU và bộ nhớ của từng service đều dưới 60%.

Khi chạy soak test kéo dài kết hợp phân tích trace, đội performance phát hiện: một service fraud-check thỉnh thoảng dừng 200–400ms do garbage collection (GC pause) của JVM. Riêng lẻ, điều này chỉ ảnh hưởng vài % request của service đó. Nhưng vì fraud-check nằm trong đường tuần tự bắt buộc của mọi giao dịch, và tài nguyên bị chia sẻ khiến GC pause xảy ra thường xuyên hơn dưới tải, xác suất một giao dịch "trúng" một GC pause tăng vọt. Đây chính là tail latency amplification trong thực tế.

Bài học rút ra: Trong service chain, một service có tail latency xấu sẽ chi phối trải nghiệm toàn hệ thống nhiều hơn bạn tưởng. Giải pháp không chỉ là tối ưu GC của fraud-check, mà còn là thiết kế lại luồng: đưa fraud-check nặng vào xử lý bất đồng bộ (async) sau khi giao dịch được ghi nhận sơ bộ, giảm chuỗi tuần tự bắt buộc từ 18 xuống 11 service. Khi test, đội bắt đầu theo dõi p99 và p99.9 của cả chuỗi thay vì chỉ nhìn trung bình.

Ví dụ 3: Retry storm biến sự cố nhỏ thành sập dây chuyền

Một startup giao đồ ăn có service restaurant-menu bị chậm nhẹ khi database gặp lock trong 30 giây. Bản thân sự cố nhỏ. Nhưng service order gọi restaurant-menu được cấu hình retry 3 lần khi timeout. Khi restaurant-menu chậm, order timeout và retry, nhân ba tải lên restaurant-menu, khiến nó càng chậm hơn. Service api-gateway phía trên lại retry order... Kết quả: một lock database 30 giây leo thang thành sự cố toàn hệ thống kéo dài 15 phút — hiện tượng gọi là retry storm hay cascading failure.

Đội chỉ phát hiện ra khi tái hiện bằng một spike test có chủ đích: cố tình làm chậm restaurant-menu và quan sát tải khuếch đại lan ngược lên chuỗi.

Bài học rút ra: Performance testing microservices không chỉ đo tốc độ lúc bình thường, mà phải kiểm tra hành vi của chuỗi khi một mắt xích suy yếu. Retry mù quáng, thiếu circuit breaker và timeout hợp lý biến độ phụ thuộc thành thuốc nổ. Bài test phải bao gồm kịch bản "một service chậm/lỗi" để lộ ra các retry storm tiềm ẩn.

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

Đây là quy trình test hiệu năng cho một service chain, áp dụng được với cả JMeter lẫn k6:

Bước 1 — Vẽ bản đồ phụ thuộc (dependency map). Trước khi viết một dòng test, hãy vẽ ra sơ đồ: request người dùng đi qua những service nào, theo thứ tự tuần tự hay song song, service nào gọi lại service nào. Nguồn tốt nhất là distributed tracing của một request thật trên môi trường staging. Không có bản đồ này, bạn đang test mù.

Bước 2 — Xác định các "hành trình" (critical path) cần test. Trong hệ thống có hàng chục endpoint, hãy ưu tiên vài hành trình quan trọng nhất về mặt kinh doanh: đăng nhập, tìm kiếm, thêm giỏ, checkout. Mỗi hành trình là một chuỗi service riêng và cần một kịch bản test riêng.

Bước 3 — Bật tracing trước khi chạy tải. Đảm bảo OpenTelemetry/Jaeger được cấu hình để lấy mẫu (sampling) đủ trace trong lúc test. Nếu không, khi thấy p95 xấu bạn sẽ không biết chặng nào có lỗi.

Bước 4 — Test cả từ ngoài vào (end-to-end) lẫn từng service. Chạy test qua API Gateway để đo latency người dùng thật cảm nhận. Song song, chạy test trực tiếp vào từng service quan trọng để biết "ngân sách latency" (latency budget) mỗi service tiêu tốn. So sánh tổng các phần với con số end-to-end sẽ lộ ra overhead ẩn (network, serialization, orchestration).

Bước 5 — Đặt latency budget cho từng chặng. Nếu SLA đầu-cuối là 800ms, hãy chia ngân sách: Auth ≤ 50ms, Cart ≤ 80ms, Payment ≤ 350ms... Test để kiểm tra từng service có nằm trong ngân sách của nó không. Đây là cách biến một mục tiêu mơ hồ thành các ngưỡng đo được (dùng thresholds/assertions).

Bước 6 — Đo phân vị cao, không chỉ trung bình. Cấu hình báo cáo hiển thị p95, p99, p99.9 của cả chuỗi. Trong microservices, đuôi phân phối mới là nơi vấn đề ẩn nấp.

Bước 7 — Test kịch bản suy thoái (degradation). Cố tình làm chậm hoặc tắt một service phụ thuộc (dùng fault injection đơn giản như thêm delay, hoặc chặn network) và quan sát chuỗi phản ứng ra sao: có retry storm không, circuit breaker có kích hoạt không, có graceful degradation không.

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

Lỗi 1 — Chỉ test end-to-end mà không phân rã. Bạn thấy p95 là 2 giây nhưng không biết vì sao. Luôn kết hợp end-to-end với tracing để phân rã theo span.

Lỗi 2 — Bỏ qua vấn đề N+1. Rất phổ biến: một service bị gọi lặp trong vòng lặp. Nhìn số lượng lời gọi trong trace, không chỉ nhìn latency. Nếu thấy 12 span giống nhau xếp hàng, bạn đã tìm ra vàng.

Lỗi 3 — Chỉ nhìn trung bình. Trung bình che giấu tail latency. Trong chuỗi dài, tail của từng service khuếch đại thành tail của toàn hệ thống. Luôn báo cáo p99.

Lỗi 4 — Test từng service riêng lẻ rồi kết luận về hệ thống. Từng service đạt chuẩn không có nghĩa chuỗi đạt chuẩn. Tương tác, network hop, và tuần tự hóa mới là nơi latency thật phát sinh.

Lỗi 5 — Quên test hành vi khi lỗi. Hệ thống nhanh lúc khỏe nhưng sập khi một service yếu. Retry storm và cascading failure chỉ lộ ra khi bạn chủ động gây suy thoái.

Mẹo: Khi phát hiện chuỗi tuần tự dài, câu hỏi đầu tiên luôn là "có thể song song hóa được không?". Nhiều lời gọi độc lập bị gọi tuần tự chỉ vì code viết theo thói quen. Song song hóa thường là cải thiện lớn nhất, rẻ nhất.

Mẹo: Đặt correlation ID (trace ID) vào log của mọi service. Khi test tải lộ ra request chậm, bạn ghép được log toàn chuỗi của đúng request đó — thay vì mò kim đáy bể trong hàng triệu dòng log.

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

  • Vẽ bản đồ chuỗi: Chọn một hành trình quen thuộc (ví dụ "đặt món trên app giao đồ ăn"). Liệt kê 6–10 service nội bộ bạn nghĩ nó phải gọi, gán một con số latency giả định cho mỗi service, rồi tính tổng latency trong hai trường hợp: hoàn toàn tuần tự và hoàn toàn song song. So sánh chênh lệch.
  • Tính tail latency: Một chuỗi có 40 service tuần tự, mỗi service có p99 = 50ms (tức 1% request của mỗi service chậm). Ước tính xác suất một request người dùng dính ít nhất một service chậm (dùng công thức 1 − 0.99^40). Con số này nói lên điều gì về việc cần giảm độ dài chuỗi?
  • Phát hiện N+1: Cho một trace waterfall giả định trong đó service pricing xuất hiện 8 lần liên tiếp, mỗi lần 40ms. Đề xuất cách sửa và tính latency tiết kiệm được nếu gộp thành một lời gọi batch 60ms.
  • Thiết kế latency budget: Với SLA đầu-cuối 1.000ms cho luồng checkout gồm Auth, Cart, Inventory, Pricing, Payment, hãy phân bổ ngân sách latency hợp lý cho từng service (nhớ chừa dư địa cho network overhead) và viết ra dưới dạng các ngưỡng threshold có thể kiểm tra.

Tóm tắt

Microservices biến một request đơn giản của người dùng thành một chuỗi phụ thuộc phức tạp, nơi hiệu năng không còn là chuyện của một service mà là chuyện của cả mạng lưới. Những điểm cốt lõi cần khắc sâu:

  • Fan-out khiến một request tỏa ra nhiều lời gọi; latency cộng dồn nếu tuần tự, chỉ bằng service chậm nhất nếu song song. Song song hóa thường là đòn bẩy lớn nhất.
  • Latency khuếch đại theo độ sâu chuỗi và theo số network hop — những chi phí vô hình với người viết từng service nhưng rất thật với người dùng.
  • Tail latency amplification: trong chuỗi dài, p99 của từng service không quan trọng bằng p99 của cả chuỗi; một service có đuôi chậm sẽ "lây" ra toàn hệ thống.
  • Distributed tracing là công cụ bắt buộc để phân rã latency theo từng chặng và tìm điểm nghẽn thật.
  • Phải test cả lúc khỏe lẫn lúc suy thoái — retry storm và cascading failure chỉ lộ ra khi bạn chủ động làm yếu một mắt xích.
Tư duy cần mang theo: đừng bao giờ hài lòng với một con số latency tổng. Hãy luôn hỏi "thời gian này tiêu ở đâu, service nào, bao nhiêu lần, và điều gì xảy ra nếu mắt xích này yếu đi". Đó là tư duy của một Performance Engineer thực thụ trong thế giới phân tán.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi