Bài 04: Mạng phi tỷ lệ và hình học của power law
Mục lục

I. Tóm tắt #
Microservices được bán kèm một lời hứa dễ chịu: bẻ monolith ra thì tải phân tán, lỗi bị cô lập. Mô hình Barabási-Albert nói ngược lại. Một mạng lưới cứ lớn lên trong khi các kết nối mới có xu hướng chọn những node vốn đã đông kết nối thì sớm muộn cũng méo về một dạng cố định: rất nhiều service lèo tèo vài kết nối, một nhúm service ôm hàng trăm. Không ai thiết kế ra những siêu node đó — chúng mọc lên như hệ quả số học của việc mạng lưới tăng trưởng. Bài này bóc phần toán, chỉ ra vì sao một siêu node nghẽn lại kéo sập cả hệ theo chiều ngược với trực giác, và ba cách sống chung với hiện tượng đó.
II. Vấn đề: lời hứa phân tán và cái thực sự xảy ra #
Câu chuyện chính thống nghe rất hợp lý: chia monolith thành microservices, tải sẽ trải đều ra toàn mạng, rủi ro cũng trải theo.
Đồ thị ngầm trong câu chuyện đó là một mạng ngẫu nhiên kiểu Erdős-Rényi: mỗi cặp node có xác suất nối nhau như nhau, nên hầu hết node đều có số kết nối xấp xỉ giống nhau, quanh một giá trị trung bình. Trong mạng như vậy, “service trung bình” là một khái niệm có nghĩa, và thêm node thì mật độ phụ thuộc loãng ra.
Kéo đồ thị phụ thuộc của bất kỳ hệ thống thật nào ra xem, câu chuyện sụp. Hệ phần mềm không lớn lên ngẫu nhiên. Chúng hội tụ về một sự lệch cực đoan: phần lớn service có vài kết nối, một nhúm service cốt lõi phình thành những khối khổng lồ mà cả tổ chức phụ thuộc vào. Những khối đó lặng lẽ cài một điểm chết duy nhất vào một kiến trúc lẽ ra sinh ra để không có điểm chết nào.
III. Khung lý thuyết: vì sao siêu node là tất yếu #
Mô hình Barabási–Albert (BA) cho phần toán, và nội dung của nó chỉ gồm hai quy tắc:
- Mạng lưới liên tục lớn lên — luôn có service mới được thêm vào.
- Kết nối mới thiên vị node đã đông — service mới cần auth, cần user profile, cần catalog, nên nó nối vào đúng những service mà mọi người khác cũng đang nối vào.
Quy tắc thứ hai gọi là gắn kết ưu tiên. Cơ chế của nó giống hệt chuyện tài khoản mạng xã hội: ai đã đông người theo dõi thì càng dễ được theo dõi thêm, không phải vì họ đột nhiên hay hơn, mà vì họ dễ được nhìn thấy hơn.
Viết thành công thức, xác suất một node mới nối vào node $i$ là:
$$ \Pi(k_i) = \frac{k_i}{\sum_{j} k_j} $$Đọc bằng lời: khả năng được chọn của một service tỉ lệ thuận với số kết nối nó đang có. $k_i$ là số kết nối hiện tại của service $i$; mẫu số là tổng số kết nối của toàn mạng. Service đang có 40 kết nối trong một mạng tổng 400 kết nối thì có 10% khả năng hứng trọn kết nối tiếp theo — trong khi một service mới ra đời gần như không có cửa.
Cho mạng chạy tiếp với mỗi node mới mang theo $m$ kết nối, tốc độ mà một node tích thêm kết nối là:
$$ \frac{\partial k_i}{\partial t} = m \cdot \frac{k_i}{\sum_{j} k_j} = \frac{k_i}{2t} $$Đọc bằng lời: tốc độ một service hút thêm phụ thuộc mới tỉ lệ với lượng phụ thuộc nó đã có. Đây chính là định nghĩa của tăng trưởng lãi kép — giàu càng giàu, và khoảng cách nới ra theo thời gian chứ không co lại.
Giải phương trình đó ra phân phối số kết nối của toàn mạng:
$$ P(k) \sim k^{-\gamma} $$Đọc bằng lời: số service có $k$ kết nối giảm rất nhanh khi $k$ tăng, nhưng không bao giờ giảm về không — nên vẫn luôn tồn tại vài node lớn khủng khiếp. Đây là dạng phân phối power law, và trong mô hình BA chuẩn $\gamma = 3$.
Điều đáng nói nhất của $\gamma = 3$: phương sai của phân phối phân kỳ khi mạng lớn lên. Nói bằng lời thường, con số trung bình mất hết ý nghĩa. Trong mạng 200 service với mỗi service mới mang theo 2 kết nối, số kết nối trung bình là 4, còn node lớn nhất có cỡ 28 — gấp bảy lần. Nhân mạng lên 20.000 service, trung bình vẫn là 4, còn node lớn nhất nhảy lên cỡ 280. Trung bình đứng yên, cực đại chạy theo quy mô mạng. Đó là lý do mọi bài toán dung lượng dựa trên “service trung bình” đều sai đúng ở chỗ nguy hiểm nhất.
IV. Quy đổi sang kiến trúc phần mềm #
| Biến số lý thuyết | Tương ứng trong hệ thống thật |
|---|---|
| Node ($N$) | Đơn vị chạy độc lập: microservice, API gateway, database |
| Cạnh ($E$) | Một phụ thuộc: lời gọi HTTP/gRPC đồng bộ, hoặc một truy vấn database |
| Bậc vào ($k_{in}$) | Số service đang gọi đồng bộ tới service này |
| Gắn kết ưu tiên | Đội mới cần đăng nhập, cần thông tin người dùng — họ nối vào đúng service mà mọi đội khác đã nối |
| Siêu node | “God service”: nơi mọi luồng nghiệp vụ đi qua, và mọi sự cố lớn đều ghé thăm |
Không chỉ là chuyện tuổi đời. Mô hình Bianconi-Barabási bổ sung một tham số gọi là fitness ($\eta_i$), làm xác suất được chọn thành $\Pi_i \propto \eta_i k_i$. Dịch sang tiếng người: một service không chỉ đông kết nối vì nó ra đời sớm, mà còn vì nó hữu dụng — auth service sinh sau vẫn hút phụ thuộc nhanh hơn một reporting service ra đời trước, đơn giản vì ai cũng cần đăng nhập. Điều này giải thích vì sao tách một God service thường thất bại: bạn tách được code, nhưng không tách được lý do mọi người cần nó.
Nghịch lý service mới sinh: bậc vào bằng không #
Mô hình BA gốc mô tả đồ thị vô hướng, còn phụ thuộc phần mềm thì có hướng: A gọi B, không có nghĩa B gọi A. Áp công thức gắn kết ưu tiên thuần túy vào bậc vào, ta gặp một chỗ vô lý: service vừa deploy có $k_{in} = 0$, nên xác suất nhận được kết nối đầu tiên cũng bằng 0. Nó không bao giờ được ai gọi.
Mô hình Derek de Solla Price (1976) sửa bằng cách cộng thêm một hằng số “sức hút ban đầu” $a > 0$:
$$ \Pi(k_{in,i}) = \frac{k_{in,i} + a}{\sum_{j} (k_{in,j} + a)} $$Đọc bằng lời: mỗi service mới được phát một lượng cơ hội tối thiểu, bất kể chưa ai gọi nó. Trong thực tế, $a$ chính là những thứ khiến người ta biết đến một service dù nó chưa nổi tiếng: một dòng trong service catalog, một buổi demo nội bộ, hay đơn giản là service đó giải đúng bài toán ai đó đang cần.
Cộng lại, hai cơ chế này giải thích vì sao phi tập trung hóa thất bại theo thời gian. Qua vài chục sprint, gắn kết ưu tiên có hướng cộng với fitness ép đồ thị dồn bậc vào cực đại về vài node — và God service hình thành mà không ai từng quyết định tạo ra nó.
V. Điểm bùng phát: chuyện xảy ra khi siêu node nghẽn #
Mạng phi tỷ lệ có một tính cách kép. Rút một node ngẫu nhiên, mạng nhún vai — vì node bốc trúng gần như chắc chắn là một node bé. Nhắm thẳng vào siêu node mà rút, mạng tan rã. Chịu nhiễu tốt, chịu đòn nhắm thì kém: đó là đặc điểm quyết định giới hạn ổn định của mọi hệ dựng trên hình thái này.
Điều kiện biên #
Gọi $k_{max}$ là trần dung lượng vật lý của siêu node — giới hạn connection pool, giới hạn thread pool — trước khi request bị xếp hàng hoặc bị từ chối. Gọi $k_{in}(t)$ là số service đang gọi nó tại thời điểm $t$. Hệ thống còn khỏe chừng nào:
$$ k_{in}(t) \times \text{tải mỗi kết nối} \le k_{max} $$Thay số cho cụ thể: auth service cấu hình 200 connection, mỗi service client giữ trung bình 5 kết nối đồng thời. Trần chịu được là 40 service client. Sprint nào đó, đội thứ 41 lên production — và không có cảnh báo nào kêu, vì trên dashboard CPU vẫn 45%.
Sụp đổ chạy ngược chiều #
Đây là chỗ hệ phần mềm khác hẳn các mạng vật lý.
Trong mô hình sụp đổ dây chuyền kiểu Motter & Lai (2002) — vốn mô tả lưới điện — một node chết thì tải của nó bị đẩy sang các node hàng xóm, các node đó quá tải và chết tiếp. Lỗi lan ngang.
Microservices không tái định tuyến kiểu đó. Khi God service đứng hình, lưu lượng của nó không đi đâu cả — nó nằm chờ. Vì các cạnh là lời gọi mạng đồng bộ, độ trễ của siêu node lập tức chiếm giữ thread pool của mọi service đang gọi nó. Bốn mươi service client, mỗi service treo 5 luồng chờ một câu trả lời không tới. Rồi các service gọi tới bốn mươi service đó cũng cạn luồng theo.
Sụp đổ chạy ngược theo chiều phụ thuộc, từ nhà cung cấp về phía người gọi. Cơ chế này có tên: synchronous backpressure. Một siêu node kẹt, và cơn thiếu luồng cuộn ngược qua toàn bộ đồ thị.
VI. Mô phỏng định lượng #
Đoạn mô phỏng dưới đây dựng một đồ thị Barabási-Albert rồi vẽ phân phối số kết nối của nó. Phần đáng chú ý là cách vẽ: chia rổ theo thang logarit (logarithmic binning), vì nếu chia rổ đều tay thì phần đuôi — đúng chỗ chứa các siêu node — chỉ còn vài điểm nhiễu và biến mất khỏi biểu đồ.
| |
Đọc kết quả: trên thang log-log, một power law hiện ra thành đường thẳng — và độ dốc của đường thẳng đó chính là $\gamma$. Với 20.000 service, số kết nối trung bình vẫn quanh 4, còn node lớn nhất rơi vào khoảng 250-300. Không có tham số nào trong đoạn code được chỉnh để ép ra kết quả này; nó chỉ là hệ quả của hai quy tắc ở Mục III.

VII. Hệ quả kiến trúc và những đánh đổi #
Nếu gắn kết ưu tiên là tất yếu về mặt số học, nước đi thành thật là xây để sống chung với nó. Ba biện pháp dưới đây không phá được quy luật — chúng đi vòng.
- Dịch chuyển siêu node xuống tầng hạ tầng (EDA và CQRS). Kiến trúc hướng sự kiện thay các lời gọi đồng bộ bằng việc bắn sự kiện lên một message broker; CQRS tách đường ghi khỏi đường đọc để hai bên không tranh nhau. Điều cần nói thẳng: cách này không xóa được siêu node, nó chỉ chuyển chỗ. Kafka hay RabbitMQ trở thành siêu node mới. Cái được nằm ở chỗ broker sinh ra để làm đúng việc đó — nó ghi tuần tự xuống đĩa và chịu được trần $k_{max}$ cao hơn hẳn một service nghiệp vụ, đồng thời việc gọi trở thành bất đồng bộ nên không còn ai treo luồng ngồi chờ.
- Cô lập bằng bulkhead. Khi không bỏ được siêu node, chia cứng tài nguyên của nó theo từng client: mỗi service gọi tới chỉ được dùng tối đa một phần thread pool định trước. Một client phát điên và ăn hết phần của nó thì 39 client còn lại vẫn chạy. Tên gọi mượn từ vách ngăn khoang tàu thủy, và ý tưởng y hệt: thủng một khoang không làm chìm tàu.
- Chặn cuộn ngược bằng circuit breaker. Đặt cầu dao ở phía người gọi: khi độ trễ của siêu node vượt ngưỡng, cắt đường gọi và trả lỗi ngay thay vì ngồi chờ. Đây là biện pháp trực tiếp nhất chống lại cơ chế backpressure ở Mục V — nó cắt đúng cái vòng khiến thread pool phía client cạn sạch.
| Biện pháp | Được gì | Trả giá gì |
|---|---|---|
| EDA / CQRS | Chuyển siêu node xuống tầng hạ tầng, nơi trần dung lượng cao hơn nhiều | Nghiệp vụ phải chấp nhận nhất quán sau; broker thành điểm phụ thuộc tập trung mới, cần vận hành nghiêm túc |
| Bulkhead | Giới hạn được chính xác phạm vi ảnh hưởng khi một client phát điên | Tài nguyên bị chia sẵn nên dùng không hết; phải quy hoạch dung lượng cho từng client |
| Circuit breaker | Cắt vòng cuộn ngược, giữ lại thread pool phía người gọi | Người dùng nhận lỗi nhanh thay vì chờ lâu; cần thiết kế đường lui khi cầu dao mở |
VIII. Tài liệu tham khảo #
- Barabási, A.-L., & Albert, R. (1999). Emergence of Scaling in Random Networks.
- Bianconi, G., & Barabási, A.-L. (2001). Competition and multiscaling in bipartite networks.
- Clauset, A., Shalizi, C. R., & Newman, M. E. J. (2009). Power-Law Distributions in Empirical Data.
- Motter, A. E., & Lai, Y.-C. (2002). Cascade-based attacks on complex networks.
- Price, D. d. S. (1976). A general theory of bibliometric and other cumulative advantage processes.