Chuyển đến nội dung chính
  1. Bài viết/

Bài 02: Định luật của sự hỗn loạn — giải mã entropy trong kiến trúc phân tán

Bẻ một monolith thành microservices không phải là chia code — số trạng thái hệ thống có thể rơi vào tăng theo hàm mũ, độ trễ đuôi thành chuyện mặc định, và một cơn bão retry đủ sức giữ hệ thống nằm sập sau khi nguyên nhân đã biến mất. Bài này bóc phần toán đứng sau, rồi chỉ ra cách CALM và CRDT cho phép điều phối hỗn loạn thay vì chống lại nó.

I. Mở đầu: chuyện xảy ra lúc bẻ một khối làm nhiều mảnh #

Bẻ monolith thành microservices không phải bài tập chia code. Số trạng thái hệ thống có thể rơi vào tăng theo hàm mũ chứ không phải phép cộng, và ba hệ quả đi kèm đều đo được: độ trễ đuôi trở thành chuyện mặc định chứ không phải ngoại lệ, hệ thống có thể chậm đi khi thêm tài nguyên, và một cơn bão retry đủ sức giữ nó nằm sập sau khi nguyên nhân gốc đã biến mất. Bài này đi qua từng con số, rồi tới lối thoát: thay vì bắt mọi thứ phải nhất quán tức thì, dùng những phép toán chỉ thêm chứ không rút lại — nền tảng của định lý CALM và CRDT — để các bản sao dữ liệu tự hội tụ mà không cần ai điều phối.

Bài trước lần theo cú dịch chuyển từ logic sang xác suất bên trong một mô hình AI. Ranh giới đó không chỉ nằm trong model. Nó chạy xuyên qua mọi node của những hệ thống phân tán mà chúng ta đang vận hành hằng ngày.

Chỗ dễ bị bỏ qua: bẻ một monolith thành microservices không phải bài tập chia code. Nó là một cú đổi trạng thái của toàn hệ. Trước đó bạn có một hệ thống mà mọi thứ hoặc đúng hoặc sai; sau đó bạn có một hệ thống chỉ đúng theo xác suất.

Từ “entropy” trong tiêu đề dùng theo nghĩa gốc của nó và không huyền bí gì: entropy là số cách một hệ thống có thể đang ở. Càng nhiều cấu hình khả dĩ, càng ít khả năng đoán trước bạn đang đứng ở cấu hình nào. Toàn bộ bài này là câu chuyện con số đó phình ra thế nào, và sống chung với nó ra sao.

II. Cái bẫy của tư duy tách rời: khi kiểm soát chỉ là ảo giác #

Vì sao các phương pháp kiểm chứng truyền thống sụp đổ ở quy mô phân tán? Một chữ: bùng nổ. Không gian trạng thái1 không lớn dần — nó nổ tung.

Trong một khối nguyên vẹn, các biến dùng chung nằm trên cùng một vùng nhớ và được ràng buộc với nhau ngay từ lúc biên dịch. Bóc thành $n$ service, tổng số trạng thái khả dĩ $\Omega$ không phải phép cộng mà là phép nhân:

$$ \Omega = |C| \times \prod_{i=1}^{n} |S_i| $$

Đọc bằng lời: mỗi service tự do đứng ở bất kỳ trạng thái nào của riêng nó, độc lập với các service khác, nên số tổ hợp là tích của tất cả. $|S_i|$ là số trạng thái nội tại của service thứ $i$; $|C|$ là số trạng thái của kênh mạng nối chúng — gói tin có thể mất, đến chậm, đến trùng, hoặc đến sai thứ tự.

Thay số cho dễ hình dung. Hệ thống 10 service, mỗi service 100 trạng thái: $100^{10} = 10^{20}$ tổ hợp. Con số đó lớn hơn số giây trôi qua kể từ Big Bang khoảng mười tỉ lần — và nó còn chưa tính tới kênh mạng. Không có công cụ kiểm chứng nào duyệt hết được không gian ấy, nên mọi lời hứa kiểu “đã test hết các trường hợp” đều sai ngay từ định nghĩa.

A technical conceptual illustration of the Cartesian Product explosion. On the left, a few solid silver cubes representing a Monolith. On the right, those cubes shatter into a vast, expanding galaxy of interconnected glowing cinnabar red points, forming a complex 3D web that fades into a white infinite background. Modern anime vector art style, clean line art, flat colors, cel shading, minimalist professional technical concept, high contrast, pure off-white background.

III. Vì sao tổng thể không bằng phép cộng các phần #

Lý thuyết hệ thống phân biệt rạch ròi hai chữ mà tiếng Việt hay gộp làm một: phức tạp kiểu cơ khí (complicated) và phức hợp kiểu sinh thái (complex).

Một chiếc đồng hồ cơ chứa hàng trăm bánh răng — rất phức tạp, nhưng tháo ra hiểu được từng bánh là hiểu được cả cái đồng hồ. Một mạng microservices thì không: nó có emergence2, tức những hành vi chỉ xuất hiện ở cấp toàn hệ và không nằm trong bất kỳ thành phần nào. Cơn bão retry ở Mục V là ví dụ sạch nhất: không service nào chứa dòng code nào gây ra nó, nó sinh ra từ việc tất cả cùng thử lại một lúc.

Viết bằng ký hiệu, chuyện đó là một dấu “khác”:

$$ f(x_1 + x_2 + \dots + x_n) \neq f(x_1) + f(x_2) + \dots + f(x_n) $$

Đọc bằng lời: đo hiệu năng của từng service khi chạy riêng, cộng lại, bạn không ra được hiệu năng của hệ thống lúc chúng chạy cùng nhau. Phần chênh lệch chính là chi phí phối hợp — và nó luôn âm.

Universal Scalability Law của Neil Gunther đóng đinh điều này thành một đường cong. Nội dung của nó, bỏ hết công thức: thêm tài nguyên mang lại hai thứ đối nghịch. Lợi ích là song song hóa; chi phí là mọi node phải nói chuyện với mọi node khác, và chi phí đó tăng theo bình phương số node. Qua một điểm nhất định, vế thứ hai thắng: thêm máy làm hệ thống chậm đi3. Ai từng thêm worker vào một hàng đợi và thấy throughput giảm đã gặp đúng đoạn đường cong đó.

IV. Ở quy mô lớn, độ trễ đuôi là mặc định #

Kiến trúc sư hay bị con số trung bình đánh lừa. Cái quyết định trải nghiệm người dùng không phải p50 mà là p99 — nghĩa là 1% request chậm nhất. Nghe như chuyện nhỏ, cho tới khi làm phép nhân.

Jeff Dean và Luiz André Barroso chỉ ra điều này trong bài “The Tail at Scale”. Giả sử một request phải gọi song song $n = 100$ service, mỗi service chỉ có 1% khả năng phản hồi chậm. Xác suất để cả request bị kéo chậm:

$$ P(\text{request chậm}) = 1 - P(\text{nhanh})^n = 1 - (0.99)^{100} \approx 63{,}4\% $$

Đọc bằng lời: request chỉ nhanh khi cả trăm service cùng nhanh, mà mỗi service lại có một cửa nhỏ để chậm. Nhân một trăm cửa nhỏ đó với nhau thì “hiếm khi chậm” biến thành “chậm gần hai phần ba số lần”. Với $n = 10$ con số này là 9,6%; với $n = 100$ là 63,4%. Không có gì hỏng trong hệ thống đó cả — nó chỉ đơn giản là số học.

Đối sách thực dụng là hedged request: gửi request đi, chờ tới mốc p95, nếu chưa có trả lời thì bắn thêm một bản sao sang replica khác và lấy kết quả nào về trước. Bạn không làm cho một request chậm nhanh lên được, nhưng bạn thôi phụ thuộc vào đúng một đường. Cái giá là lưu lượng tăng thêm cỡ 5% — chọn mốc p95 chính là để giữ phần dư đó nhỏ.

graph LR User((User)) --> GW[API Gateway] GW -->|"gọi song song 100 service"| Fan[Fan-out] Fan --> S1["Service 1
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ì bắn 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

V. Khi hệ thống nằm sập sau lúc nguyên nhân đã hết #

Dạng lỗi khó chịu nhất trong hệ phân tán là loại vẫn tiếp diễn sau khi tác nhân ban đầu biến mất. Đó là metastable failure4: nguyên nhân gốc đã hết từ lâu, hệ thống vẫn nằm im trong trạng thái sập và không tự bò dậy được.

Thủ phạm quen mặt nhất là retry storm. Độ trễ vượt timeout, client tự động thử lại. Mỗi lần thử lại là một request mới cộng vào hệ thống vốn đã quá tải, đẩy hệ số sử dụng $\rho$ tiến sát 1. Công thức Kingman mô tả cái giá:

$$E[W] \propto \frac{\rho}{1 - \rho}$$

Trong đó $\rho$ là hệ số sử dụng — tỉ lệ giữa tải đến và khả năng xử lý; $E[W]$ là thời gian chờ trung bình. Đọc bằng lời: thời gian chờ không tăng đều theo tải, nó tăng theo phần dung lượng còn trống, và phần đó thì đang co lại. Thay số: $\rho = 0{,}5$ cho hệ số 1; $\rho = 0{,}9$ cho 9; $\rho = 0{,}99$ cho 99. Nghĩa là chặng từ 90% lên 99% tải làm thời gian chờ đội lên gấp mười một lần, dù trên dashboard nó chỉ trông như “thêm 9% nữa thôi”.

Định lý Little khép vòng lặp: $L = \lambda W$ — số việc đang nằm trong hệ thống bằng tốc độ việc đến nhân thời gian mỗi việc ở lại. $W$ nổ thì $L$ nổ theo. Hàng đợi phình ra vắt kiệt thread pool và bộ nhớ, request mới bị timeout, client lại thử lại. Vòng lặp tự nuôi chính nó, và đó là lý do tắt nguồn tác nhân ban đầu không cứu được gì.

Đối sách gồm hai lớp:

  • Exponential backoff kèm jitter. Backoff giãn dần khoảng chờ giữa các lần thử. Jitter cộng thêm một lượng ngẫu nhiên vào khoảng chờ đó — thiếu nó, hàng nghìn client cùng nhận lỗi một lúc sẽ cùng thử lại một lúc, tạo ra những đợt sóng dội đều đặn (thundering herd).
  • Circuit breaker. Sau một số lần lỗi liên tiếp, ngắt hẳn đường gọi trong vài chục giây và trả lỗi ngay lập tức. Việc này cho service phía dưới khoảng trống để xả hết hàng đợi tồn — thứ mà retry sẽ không bao giờ cho nó.

VI. Lối thoát: điều phối hỗn loạn thay vì chống lại nó #

Nếu ép nhất quán tức thì và điều phối đồng bộ đảm bảo sụp đổ dưới tải, cái gì sống sót? Câu trả lời nằm ở tầng dữ liệu.

Định lý CALM (Consistency As Logical Monotonicity) phát biểu một điều đơn giản đến bất ngờ: một phép toán chỉ thêm thông tin, không bao giờ rút lại, thì chạy được an toàn ở mọi bản sao mà không cần bất kỳ bộ điều phối trung tâm nào.

Phân biệt bằng ví dụ:

  • Đơn điệu (chạy được không cần điều phối): thêm một người vào danh sách tham dự; tăng bộ đếm lượt xem; ghi nhận “đơn hàng này đã thanh toán”. Hai node xử lý theo thứ tự khác nhau vẫn ra cùng kết quả.
  • Không đơn điệu (bắt buộc phải điều phối): trừ tồn kho khi kho chỉ còn một món. Ở đây thứ tự quyết định ai được món hàng, nên không có cách nào tránh được một vòng thống nhất.

Từ đó ra CRDT (Conflict-Free Replicated Data Type): cấu trúc dữ liệu tự hợp nhất, không cần khóa. Ba tính chất khiến nó chạy được, nói bằng lời thường: thứ tự các thay đổi đến 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 hai lần cũng không sao. Cộng ba điều đó lại thì mọi bản sao tự hội tụ về cùng một trạng thái, kể cả sau khi đứt mạng. Đây là thứ đứng sau các trình soạn thảo nhiều người sửa cùng lúc.

Với những nghiệp vụ không đơn điệu — trừ tồn kho, trừ tiền — không có phép màu nào cả. Ở đó ta chấp nhận nhất quán sau (eventual consistency) và triển khai bằng Saga cùng Outbox Pattern: mỗi bước có một bước bù trừ, và mọi thay đổi được ghi kèm sự kiện trong cùng một transaction để không bao giờ mất dấu.

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 #



  1. State Space Explosion: số trạng thái khả dĩ của hệ thống 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 hay mô phỏng nào duyệt hết nổi. ↩︎

  2. 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. ↩︎

  3. Retrograde scalability: đoạn đường cong trong phương trình của Neil Gunther 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 giữa các thành phần vượt qua lợi ích của việc chạy song song. ↩︎

  4. Metastable failure: hệ thống kẹt vĩnh viễn trong trạng thái sập do 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ự phục hồi đượ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. ↩︎