Bài 02: Entropy — vì sao hệ thống chia nhỏ lại khó kiểm soát hơn
Mục lục

I. Tóm tắt #
Trực giác nói: tách ứng dụng thành nhiều phần nhỏ chỉ là chia code cho gọn. Thực tế, số trạng thái nhân lên kéo theo ba hệ quả: request chậm thành thường lệ, thêm máy làm chậm thêm, và bão retry giữ hệ sập dù nguyên nhân đã hết. Công cụ giải thích là lý thuyết hệ phức tạp và toán hàng đợi. Lối thoát nằm ở tầng dữ liệu: thao tác chỉ thêm, không rút lại, nền tảng của định lý CALM và CRDT, để các bản sao tự khớp nhau mà không chờ nhau. Trừ kho hay trừ tiền thì vẫn phải trả giá cho việc phối hợp.
II. Vì sao tách ứng dụng ra thì số trạng thái nhân lên #
11 giờ 30 trưa ở TP.HCM, bạn mở app giao đồ ăn và bấm “Đặt món”. Giả sử phía sau nút đó có mười service cùng phải trả lời: món, giá, khuyến mãi, phí ship, tài xế, thanh toán… Service nào cũng chạy ổn gần như mọi lúc, vậy mà cả chuỗi lại hay chậm, thỉnh thoảng sập theo kiểu không service nào tự gây ra.
Service là một phần mềm chạy trên server, lo đúng một việc. Request là một lần app gửi yêu cầu tới server, như một lần bấm “Đặt món”.
Cách làm cũ gói mọi việc vào một chương trình, gọi là monolith. Microservices tách chương trình đó thành nhiều service nói chuyện qua mạng, để mỗi đội cập nhật phần mình mà không chờ đội khác. Nghe như chia code, nhưng thật ra nó đổi tính chất của cả hệ.
Trong monolith, lời gọi giữa hai chức năng chạy xong hoặc báo lỗi ngay. Qua mạng, lời gọi có thể mất, đến chậm, đến hai lần, hoặc bên kia làm xong mà bên này không biết. Hệ như vậy gọi là hệ phân tán, và giống phần mềm có AI ở Bài 01, nó chỉ đúng theo xác suất.
Chữ “entropy” trong tiêu đề dùng theo nghĩa gốc. Entropy đo số cách một hệ có thể đang được sắp xếp1: càng nhiều cách, càng khó đoán lúc này nó đang ở cách nào.
Trạng thái là tình trạng của một service lúc đó, như đang chờ ngân hàng, đang khởi động lại, hay đang chạy phiên bản cũ. Mọi tổ hợp trạng thái của mười service gộp lại gọi là state space2; mỗi tổ hợp là một “cách sắp xếp” của app.
Các phần của monolith cùng bật, cùng tắt, cùng phiên bản, cùng vùng nhớ, nên phần lớn tổ hợp không bao giờ xảy ra. Tách ra, mỗi service tự khởi động lại, tự cập nhật, tự chậm, và các tổ hợp đó mở ra hết. Mười service, mỗi service 100 trạng thái, cho ra 10²⁰ tổ hợp: gấp khoảng 230 lần số giây kể từ Big Bang, chưa tính mạng.
Đào sâu: vì sao số trạng thái nhân lên chứ không cộng lại
Đọc bằng lời: số tổ hợp của cả hệ bằng số trạng thái của mạng nhân với số trạng thái của từng service, vì service nào cũng có thể đứng ở bất kỳ trạng thái nào của nó.
- $n$: số service. $|S_i|$: số trạng thái của service thứ $i$.
- $|C|$: số trạng thái của mạng, nơi gói tin có thể mất, chậm, trùng, sai thứ tự.
- $\Omega$: số tổ hợp tối đa, coi mọi tổ hợp đều xảy ra được.
Thay số: $n = 10$, $|S_i| = 100$, bỏ qua mạng ($|C| = 1$): $\Omega = 100^{10} = 10^{20}$, gấp khoảng 230 lần tuổi vũ trụ tính bằng giây ($4{,}4 \times 10^{17}$). Duyệt một tỉ tổ hợp mỗi giây mất $10^{11}$ giây, khoảng 3.200 năm.
Nghe câu “đã test hết các trường hợp”, phản xạ của tôi là gật gù yên tâm. Với 10²⁰ tổ hợp, chạy một tỉ trường hợp mỗi giây thì chữ “hết” mất khoảng 3.200 năm. Và có những hành vi không nằm trong service nào.

III. Vì sao hiểu từng service chưa phải là hiểu cả hệ #
Hiểu từng bánh răng của chiếc đồng hồ cơ là hiểu cả chiếc đồng hồ. Hiểu từng loài trong một hệ sinh thái vẫn chưa đoán được cả hệ. Microservices thuộc kiểu phức tạp thứ hai, vì có emergence3.
Emergence là hành vi chỉ xuất hiện khi các thành phần chạy cùng nhau, không nằm trong thành phần nào. Lúc 11 giờ 30, không khách nào định làm sập app; mỗi người chỉ bấm “Thử lại” một lần. Cái sập nằm ở hàng chục nghìn lần bấm cộng lại. Mục V cho thấy phía máy cũng vậy: không service nào có dòng code gây sập, cái sập sinh ra khi tất cả cùng gửi lại một lúc.
Vì thế, cộng hiệu năng của từng service không ra hiệu năng của cả hệ; phần chênh lệch thường là chi phí phối hợp. Neil Gunther vẽ chi phí đó thành đường cong Universal Scalability Law, ước lượng throughput khi thêm máy. Throughput là số request xử lý xong mỗi giây.
Thêm máy thì được thêm sức làm song song, nhưng các máy phải chờ nhau và giữ dữ liệu khớp nhau. Phần giữ khớp tăng theo số cặp máy, gần như theo bình phương số máy. Qua một ngưỡng, phần đó thắng, và thêm máy làm throughput giảm4.
Họp 4 người có 6 cặp cần hiểu ý nhau, họp 12 người có 66 cặp. Ai từng họp 12 người đều biết nó không xong nhanh gấp ba. Với app giao đồ ăn, thêm máy cho service thanh toán giờ trưa có thể làm số đơn xử lý được giảm đi. Còn một kiểu chậm mà thêm máy không chạm tới: phép nhân xác suất.
IV. Ở quy mô lớn, request chậm là chuyện thường ngày #
Độ trễ là thời gian từ lúc bấm tới lúc có kết quả. Dashboard hay đưa lên p50: một nửa số request nhanh hơn mức này.
Người dùng thì nhớ p99: cứ 100 request thì 1 cái chậm hơn mức này. Nhóm chậm nhất đó gọi là tail latency, và 1% nghe như chuyện nhỏ cho tới khi làm phép nhân.
Jeff Dean và Luiz André Barroso làm phép nhân đó trong bài “The Tail at Scale” (2013). Giả sử một request phải chờ 100 service cùng trả lời, mỗi service có 1% khả năng chậm, độc lập với nhau. Request chỉ nhanh khi cả 100 cùng nhanh: 0,99 nhân với chính nó 100 lần, khoảng 37%. Vậy khoảng 63% request sẽ chậm. Nếu các service cùng chờ một database đang nghẽn, chúng không còn độc lập và con số sẽ khác.
Mỗi service tự báo cáo một con số rất đẹp: 99% request nhanh. Ghép 100 con số đẹp đó lại, khách chờ gần hai trên ba lần. Với mười service như app giao đồ ăn, mỗi service cũng 1% chậm, con số là 9,6%, tức cứ khoảng mười lần bấm “Đặt món” có một lần phải chờ. Không có gì hỏng cả; đây là số học.
Đối sách thực dụng là hedged request: request đầu về trễ thì gửi thêm một bản sao tới replica. Replica là một bản chạy khác của cùng service, trên máy khác.
p95 là mức độ trễ mà 95% request nhanh hơn. Hedged request chờ tới mốc p95 mới gửi bản sao và lấy kết quả nào về trước, nên chỉ khoảng 5% request phải gửi thêm, lưu lượng tăng cỡ 5%. Request chậm không nhanh lên, nhưng khách thôi phụ thuộc vào đúng một đường. Cách này hợp với request chỉ đọc dữ liệu; gửi trùng một lệnh trừ tiền là chuyện khác.
Sơ đồ dưới đọc từ trái sang phải: request tỏa ra 100 service, chờ đủ phản hồi, rồi rẽ nhánh nhanh hoặc chậm.
1% chậm"] Fan --> S2["Service 2
1% chậm"] Fan --> Sn["Service ...100
1% chậm"] S1 --> Join{Chờ đủ mọi phản hồi} S2 --> Join Sn --> Join Join -->|"63,4% số lần có ít nhất 1 service chậm"| Slow["Request chậm"] Join -->|"36,6%"| Fast["Request nhanh"] Slow -.->|"quá mốc p95 thì gửi bản sao sang replica"| Hedge["Hedged request"] style Slow fill:#f96,stroke:#333,stroke-width:2px style Hedge fill:#f96,stroke:#333,stroke-width:2px
Câu nên hỏi đội kỹ thuật: màn hình quan trọng nhất phải chờ bao nhiêu service, và dashboard có theo dõi p99 không?
V. Vì sao hệ thống vẫn nằm sập khi nguyên nhân đã hết #
Dạng lỗi khó chịu nhất là loại kéo dài sau khi nguyên nhân đã hết. Nó tên là metastable failure5: hệ kẹt trong trạng thái sập vì một vòng lặp tự duy trì.
Thủ phạm quen mặt nhất là retry. Retry là việc phía gọi tự gửi lại request khi chưa có trả lời. Gọi ai không thấy nhấc máy, phản xạ đầu tiên của tôi cũng là gọi lại ngay. Nhân phản xạ đó với vài chục nghìn điện thoại lúc 11 giờ 30 là ra retry storm: hàng loạt máy cùng retry.
Giả sử cổng thanh toán mà app dùng chậm đi hai phút, và request tới đó vượt timeout. Timeout là thời gian tối đa phía gọi chịu chờ trước khi bỏ cuộc. App tự retry, khách bấm lại, và mỗi lần thử lại là một request mới đổ vào service đã quá tải. Utilization là lượng việc đổ vào chia cho lượng việc service làm được, và retry đẩy nó lên sát 100%.
Lý thuyết hàng đợi là phần toán về chuyện xếp hàng. Công thức Kingman của nó cho biết cái giá: thời gian chờ tăng theo kiểu chia cho phần năng lực còn trống. Lấy mức chờ ở utilization 50% làm mốc 1 thì ở 90% là 9, ở 99% là 99, tức gấp 11 lần chỉ sau 9 điểm phần trăm.
Bếp của một quán trên app cũng vậy: giả sử bếp bận 90% thì mỗi đơn chờ 10 phút mới tới lượt nấu, bận 99% thì chờ khoảng 110 phút.
Đào sâu: công thức Kingman
Đọc bằng lời: thời gian chờ trong hàng bằng hệ số tải, nhân mức dao động của dòng việc, nhân thời gian xử lý một việc. Đây là xấp xỉ, sát nhất khi hệ đã bận nhiều.
- $\rho$: utilization, từ 0 đến 1. $\tau$: thời gian xử lý trung bình một việc.
- $c_a$, $c_s$: độ lệch chuẩn chia cho trung bình của khoảng cách giữa hai lần việc tới, và của thời gian xử lý; bằng 1 khi cả hai hoàn toàn ngẫu nhiên.
- $E[W]$: thời gian chờ trung bình trong hàng, chưa tính lúc xử lý.
Thay số với $c_a = c_s = 1$: $E[W]$ bằng $\tau$ ở $\rho = 0{,}5$, $9\tau$ ở $0{,}9$, $99\tau$ ở $0{,}99$. Với bếp giả định, $\tau \approx 67$ giây: $9\tau \approx 10$ phút, $99\tau \approx 110$ phút.
Định luật Little khép vòng lặp: số request nằm trong hệ bằng tốc độ request đến nhân thời gian mỗi request ở lại. Thời gian ở lại kéo dài thì hàng chờ phình theo, chiếm hết bộ nhớ và thread pool. Thread pool là nhóm luồng xử lý có số lượng cố định, như các quầy trong phòng giao dịch.
Request mới lại timeout, app lại retry, và vòng lặp tự nuôi chính nó. Cổng thanh toán đã nhanh trở lại mà hàng chờ vẫn đầy request người gửi đã bỏ đi. Hết nguyên nhân là chưa đủ; phải cắt bớt tải.
Đối sách gồm hai lớp. Lớp thứ nhất sửa cách retry:
- Exponential backoff: giãn dần khoảng chờ giữa các lần retry, ví dụ 1, 2, 4, 8 giây.
- Jitter: cộng một khoảng ngẫu nhiên vào mỗi lần chờ. Thiếu nó, hàng nghìn điện thoại lỗi cùng giây sẽ retry cùng giây, thành những đợt sóng dội gọi là thundering herd.
Lớp thứ hai chặn hẳn đường gọi:
- Circuit breaker: công tắc trong code, lỗi liên tiếp vài lần thì ngắt đường gọi vài chục giây và trả lỗi ngay. Nó giống cầu dao điện trong nhà, chỉ khác là tự thử đóng lại sau một lúc. Khoảng lặng đó cho service phía sau cơ hội xả hàng chờ, thứ retry không bao giờ cho.
VI. Lối thoát: những thao tác không cần các máy chờ nhau #
Nếu bắt các máy chờ nhau là thứ dễ sụp dưới tải, cái gì đứng vững? Câu trả lời nằm ở tầng dữ liệu.
Theo định lý CALM (Hellerstein và Alvaro tóm tắt), thao tác chỉ thêm thông tin mà không bao giờ rút lại sẽ chạy đúng trên mọi replica mà không cần chúng thống nhất trước. Thao tác kiểu đó gọi là đơn điệu.
- Đơn điệu: thêm món vào giỏ đặt nhóm từ điện thoại của mấy đồng nghiệp; tăng bộ đếm lượt xem; ghi nhận “đơn đã thanh toán”. Nhận theo thứ tự nào cũng ra cùng kết quả.
- Không đơn điệu: trừ kho khi chỉ còn một phần. Thứ tự quyết định ai được phần đó; không thống nhất trước thì một phần cơm có thể bán cho hai người.
Đọc tới đây, tôi rất muốn dùng cách này cho cả suất khuyến mãi cuối cùng của một đợt flash sale. CALM trả lời thẳng là không được: “còn hàng” là một sự thật có thể bị rút lại ngay khi người khác mua trước.
CRDT (Conflict-free Replicated Data Type) áp dụng đúng ý tưởng này. Đây là kiểu dữ liệu mà các replica tự gộp với nhau được nhờ ba tính chất. Thứ tự thay đổi không quan trọng, gom nhóm kiểu nào cũng ra một kết quả, và nhận trùng một thay đổi cũng không sao.
Khi mạng nối lại và các replica đã nhận đủ thay đổi của nhau, chúng khớp về cùng một trạng thái. Giả sử giỏ đặt nhóm làm bằng CRDT: hai điện thoại mất sóng vẫn thêm món được, có sóng lại thì hai giỏ gộp làm một. Một số trình soạn thảo nhiều người gõ cùng lúc cũng làm vậy.
Trừ kho, trừ tiền thì không có lối tắt. Ở đó ta chấp nhận eventual consistency: các service lệch nhau một lúc, rồi khớp lại sau. Cách làm thường gặp là Saga: chia nghiệp vụ thành chuỗi bước, bước nào cũng có bước bù trừ, như trừ tiền thất bại thì trả phần cơm về kho.
Saga thường đi cùng Outbox pattern: ghi thay đổi dữ liệu và sự kiện báo cho service khác trong cùng một transaction. Transaction là nhóm thao tác trên database cùng thành công hoặc cùng không xảy ra, nên không có chuyện dữ liệu đã đổi mà sự kiện chưa được ghi.
Câu nên hỏi đội kỹ thuật: thao tác nào trong sản phẩm chỉ thêm dữ liệu, và thao tác nào giống suất cuối cùng, buộc phải thống nhất trước?
Kỹ sư không chống lại entropy. Kỹ sư chọn chỗ để trả giá cho nó.
VII. Tài liệu tham khảo và đọc thêm #
- Bhatti, S. (2025). Testing Distributed Systems Failures with Interactive Simulators.
- Bobbio, A., Codetta-Raiteri, D., Montani, S., & Portinale, L. (2009). Modeling Cascading Failure Propagation through Dynamic Bayesian Networks.
- Dean, J., & Barroso, L. A. (2013). The Tail at Scale. Communications of the ACM, 56(2), 74–80.
- DoorDash Engineering (2023). Failure Mitigation for Microservices: An Intro to Aperture.
- Golshani, H. (2021). Understanding CAP Theorem in Microservices.
- Gunther, N. (2007). Guerrilla Capacity Planning. Springer.
- Hellerstein, J. M., & Alvaro, P. (2020). Keeping CALM: When Distributed Consistency Is Easy. Communications of the ACM.
- Kingman, J. F. C. (1961). The Single Server Queue in Heavy Traffic. Mathematical Proceedings of the Cambridge Philosophical Society, 57(4), 902–904.
- Little, J. D. C. (1961). A Proof for the Queuing Formula: L = λW. Operations Research, 9(3), 383–387.
- Wang, T., & Qi, G. (2024). A Comprehensive Survey on Root Cause Analysis in (Micro) Services: Methodologies, Challenges, and Trends.
Nói chính xác, entropy tỉ lệ với logarit của số cách sắp xếp (công thức Boltzmann), nên số cách nhân lên thì entropy cộng thêm. ↩︎
State space explosion: số trạng thái tăng theo hàm mũ khi số thành phần tăng, tới mức không công cụ kiểm chứng nào duyệt hết được từng trạng thái. ↩︎
Emergence: hành vi chỉ xuất hiện ở cấp toàn hệ, không quy được về và không đoán được từ hành vi của bất kỳ thành phần đơn lẻ nào. ↩︎
Retrograde scalability: đoạn đường cong trong Universal Scalability Law nơi thêm tài nguyên (node, thread, process) làm giảm tổng throughput, vì chi phí phối hợp vượt qua lợi ích của việc chạy song song. ↩︎
Metastable failure: hệ kẹt trong trạng thái sập vì một vòng lặp phản hồi tự nuôi chính nó, thường là retry, nên không tự hồi phục dù nguyên nhân gốc đã biến mất. Thoát ra thường phải cắt tải từ bên ngoài. ↩︎