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

Bài 07: Lý thuyết thấm — vì sao thêm đường dự phòng là mở thêm đường cho sự cố

Thêm đường dự phòng tưởng như tăng độ an toàn, nhưng lại tạo ra các cao tốc dẫn truyền sự cố khi quá tải dồn toa. Bài này giải thích cơ chế sập dây chuyền qua lý thuyết thấm, và vì sao cắt bớt liên kết bằng kiến trúc tế bào mới là lối thoát.

I. Tóm tắt #

Trực giác nói: mỗi bản dự phòng thêm vào là một lớp an toàn, và các lớp cộng dồn với nhau. Hệ thống thật không như vậy. Mỗi đường dự phòng là một cạnh mới trên đồ thị. Cạnh nào truyền tải dịch vụ theo một chiều thì cũng truyền dẫn sự cố theo chiều ngược lại. Lý thuyết thấm cho biết ngưỡng nằm ở đâu; mô hình lan lỗi theo tải cho biết vì sao sự cố lan truyền dọc theo cạnh. Mạng đủ dày để dịch vụ thông suốt cũng đủ dày để lỗi đi xuyên cả hệ. Bài toán kiến trúc vì vậy là chọn cắt cạnh nào, không phải thêm bao nhiêu cạnh. Ba hệ quả đo lường cụ thể: tính headroom theo tải sau khi chuyển dự phòng, liệt kê những thứ mọi bản dự phòng cùng phụ thuộc, và cắt đồ thị bằng cell thay vì tin vào số lượng bản sao.

II. Điều mà cách nghĩ “thêm đường dự phòng là thêm an toàn” bỏ sót #

43 giây mất kết nối mạng giữa hai trung tâm dữ liệu. 24 giờ hệ thống lệch dữ liệu và gián đoạn dịch vụ. Ở giữa hai con số đó không có thiết bị nào hỏng. Hệ thống chỉ có một cơ chế chuyển dự phòng tự động chạy đúng như thiết kế1.

Trực giác về dự phòng chạy theo một phép tính rất dễ chịu. Redundancy là việc chạy nhiều bản giống nhau của cùng một thứ, để bản này hỏng thì bản kia gánh. Failover là thao tác tự động chuyển việc từ bản hỏng sang bản còn sống. Nếu một bản có xác suất hỏng 1%, hai bản chạy song song sẽ cùng hỏng với xác suất 0,01%. Phép nhân đó là nền của mọi bảng tính độ sẵn sàng tôi từng thấy trình bày trong họp.

Phép nhân ấy chỉ đúng khi hai bản sao độc lập với nhau. Trong hệ thống thật, điều kiện đó gần như không bao giờ đúng. Gần như không ai kiểm tra giả định này. Hai bản sao thường dùng chung một tủ rack, một nguồn điện, một hệ thống cấu hình, hoặc cùng phục vụ một nhóm người dùng có thói quen giống nhau.

Có một chỗ trong đời sống ở TP.HCM mô tả chuyện này khá chính xác: các tuyến vượt sông Sài Gòn. Cầu Sài Gòn, cầu Thủ Thiêm, hầm Thủ Thiêm — ngày thường mỗi tuyến đều dư sức cho lượng xe của chính nó. Khi một cây cầu bị phong tỏa, lượng xe của nó không biến mất; nó chảy sang những tuyến còn lại. Tuyến nhận thêm chỉ dư 20% mà phải gánh thêm 50% thì tê liệt, khiến xe lại chảy tiếp sang tuyến thứ ba. Không cây cầu nào sập; chúng bị đánh sập bởi chính cơ chế đi đường vòng.

Trong phần mềm, chuyện này còn nhanh hơn. Xe kẹt trên cầu thì đứng chờ. Request bị timeout thì biến mất kèm theo toàn bộ năng lực đã bỏ ra để xử lý nó. Cơ chế đó tôi đã nói ở Bài 06 và sẽ không lặp lại. Ở đây chỉ cần nhớ một câu: hệ phần mềm tụt năng lực nhanh hơn dòng xe.

Phép tính mà sơ đồ kiến trúc không bao giờ ghi thì đơn giản đến mức nhẩm được. AZ là availability zone, một khu nhà máy chủ riêng biệt của nhà cung cấp cloud, có nguồn điện và đường mạng tách khỏi khu bên cạnh. Giả sử một hệ chạy trên 3 AZ, mỗi AZ đang dùng hết một tỉ lệ nhất định sức chứa của nó — gọi là utilization. Mất 1 AZ thì 2 AZ còn lại gánh toàn bộ, tức mỗi bên lên gấp rưỡi mức cũ. Muốn không vỡ thì utilization ngày thường không vượt quá hai phần ba.

Ba AZ cùng chạy 70% là một cụm đã hết đường lùi, trong khi mọi dashboard đều xanh. Đây là con số đầu tiên của bài, và tôi cố tình chọn con số dễ nhẩm. Nó là phép tính đáng ra phải nằm trong đầu mọi người trước khi nằm trong tài liệu.

Trên sơ đồ kiến trúc, mũi tên đứt nét ghi chữ failover trông rất yên tâm. Nó vẽ chiều đi của luồng request khi có lỗi. Nó không vẽ chiều mà tải trọng sẽ dội ngược lại.

Hiện tượng thứ hai ít người chú ý hơn: nhiều bản dự phòng chết cùng lúc vì cùng một lý do. Control plane là phần hệ thống lo việc điều phối — quyết định ai chạy ở đâu, cấu hình nào đang có hiệu lực, địa chỉ nào trỏ về máy nào. Data plane là phần trực tiếp phục vụ người dùng. Ba bản sao đặt ở ba AZ khác nhau vẫn đọc cấu hình từ cùng một control plane, và lúc đó chúng không còn là ba thứ độc lập.

Cái thiếu không phải số lượng bản sao. Cái thiếu là hình dạng của đồ thị phụ thuộc. Có một nhánh toán học chuyên trả lời đúng câu hỏi đó, và nó ra đời từ một câu hỏi về mặt nạ phòng độc.

III. Lý thuyết thấm: ngưỡng ở đâu, và ai đẩy nó xuống #

Thấm là gì, và ngưỡng nằm ở đâu #

Thấm ở đây mang đúng nghĩa đen của chất lỏng chảy qua vật liệu xốp. Simon Broadbent và John Hammersley (1957) dựng lý thuyết này để trả lời câu hỏi: chất khí độc có lọt qua lớp than hoạt tính trong mặt nạ phòng độc hay không?

Trước khi đi tiếp, ba từ cần thống nhất. Đồ thị là một tập điểm nối với nhau bằng các đường. Node là một điểm. Cạnh là một đường nối hai điểm. Cả bài này, node là một máy chủ hoặc một service, còn cạnh là một lời gọi hoặc một tuyến dữ liệu giữa hai node.

Lý thuyết thấm hỏi một câu duy nhất: mạng còn nguyên vẹn bao nhiêu thì còn đi xuyên qua được? Nó có hai phiên bản. Site percolation: mỗi node còn sống với một tỉ lệ nhất định — tương ứng tỉ lệ server còn chạy trong cụm. Bond percolation: mỗi cạnh còn thông suốt với một tỉ lệ nhất định — tương ứng tỉ lệ đường mạng hoặc lời gọi API chưa đứt.

Mạng hẻm ở TP.HCM là mô hình gần như hoàn hảo cho cả hai. Đường lớn kẹt thì luồn hẻm. Khả năng luồn từ đầu khu sang cuối khu không phụ thuộc vào một con hẻm cụ thể, mà phụ thuộc tỉ lệ hẻm đang thông. Một căn nhà bị rào lại là site; một con hẻm bị chặn là bond.

Mạng hẻm thông suốt là tin tốt cho người đi làm về. Nó cũng là tin tốt cho nước ngập: hẻm này ngập thì hẻm kế bên ngập theo, và nước đi qua khu bằng đúng những lối bạn vẫn đi. Cùng một đồ thị, đổi từ góc nhìn dòng dịch vụ sang dòng sự cố sẽ ra hai kết luận trái ngược nhau. Cả bài này nằm trong khoảng cách giữa hai cách đọc đó.

Khái niệm trung tâm của lý thuyết là khối liên thông lớn. Đó là cụm mạng chính vẫn gắn kết thành một khối duy nhất và chiếm phần lớn toàn mạng. Dưới một ngưỡng nhất định của tỉ lệ còn nguyên vẹn, mạng chỉ còn những mảnh vụn rời rạc. Trên ngưỡng đó, một khối lớn xuất hiện. Ngưỡng ấy gọi là ngưỡng thấm.

Vài con số để neo. Trên lưới vuông hai chiều, ngưỡng bond là 0,5 — đúng một nửa số cạnh. Ngưỡng site là khoảng 0,593. Trên một đồ thị ngẫu nhiên mà mỗi node nối trung bình 4 node khác, ngưỡng thấm bằng 0,25.

Dịch câu cuối sang ngôn ngữ hàng ngày: một cụm gồm 200 service nối ngẫu nhiên phải mất tới 75% số máy thì đồ thị mới tan rã hoàn toàn. Đó là con số tôi từng dùng để thấy yên tâm. Nó đúng về toán học, nhưng nó trả lời một câu hỏi hoàn toàn khác với câu hỏi mà production đang đặt ra.

Vì sao mạng dày lại khó vỡ, và vì sao đó không phải tin tốt #

Michael Molloy và Bruce Reed (1995) chứng minh một điều phản trực giác. Khối lớn đó tồn tại hay không gần như không phụ thuộc vào số lượng node hay cạnh. Nó phụ thuộc chủ yếu vào độ chênh lệch số liên kết giữa các node. Mạng mà ai cũng nối 4 người, và mạng mà đa số nối 2 người còn vài node nối 200 người, có thể cùng bậc trung bình nhưng ứng xử hoàn toàn khác nhau.

Đồ thị service trong thực tế thuộc loại thứ hai. Luôn có vài hub — service xác thực, cổng thanh toán, database lõi — bị hàng trăm service khác gọi tới. Reuven Cohen và cộng sự (2000) chỉ ra hệ quả: độ chênh càng lớn thì ngưỡng thấm càng thấp, và những mạng như vậy đặc biệt bền trước lỗi ngẫu nhiên. Bạn rút phích cắm hú họa rất nhiều máy mà hệ vẫn liên lạc được, đơn giản vì xác suất rút trúng hub rất thấp.

Đó là một tin mừng có thật, và tôi muốn nói nó ra đầy đủ trước khi lật. Chaos Monkey — công cụ của Netflix tự tắt ngẫu nhiên máy chủ trong production để kiểm tra sức chịu đựng — hoạt động được chính nhờ tính chất này.

Rồi tới chỗ lật, và nó có hai tầng.

Tầng thứ nhất: Réka Albert, Hawoong Jeong và Albert-László Barabási (2000) chỉ ra chính các hub làm mạng cực kỳ mỏng manh trước lỗi có chọn lọc. Mà sự cố thật không chọn ngẫu nhiên. Sự cố đi theo tải, tải đi theo hub, nên kịch bản mặc định trong vận hành là kịch bản tệ nhất trong lý thuyết2.

Tầng thứ hai sâu hơn và là luận điểm chính của bài. Khi hỏi mạng còn liên lạc được không, ta muốn ngưỡng thấm thật thấp. Khi hỏi một sự cố cục bộ có lan ra cả hệ không, ta muốn điều ngược lại — vì bậc càng chênh, ngưỡng để một mầm lỗi quét xuyên mạng càng thấp. Cùng một phép đo, hai mong muốn mang dấu ngược nhau. Không có cấu hình nào tối ưu được cả hai.

Đào sâu: ngưỡng thấm tính ra từ đâu

Tiêu chuẩn Molloy–Reed (1995) viết gọn thành hai dòng:

$$ \kappa = \frac{\langle k^2 \rangle}{\langle k \rangle}, \qquad p_c = \frac{1}{\kappa - 1} $$

Đọc bằng lời: Khối lớn còn tồn tại chừng nào một node còn sống trung bình còn kéo theo được nhiều hơn một node sống khác. Đại lượng $\kappa$ không đo số hàng xóm của một node, mà đo số hàng xóm của một node lân cận điển hình. Hai con số này lệch nhau rất xa khi mạng có hub.

  • $\langle k \rangle$: Số liên kết trung bình của một node trên đồ thị.
  • $\langle k^2 \rangle$: Trung bình của bình phương số liên kết. Phép bình phương khuếch đại mạnh tác động của các hub.
  • $p_c$: Tỉ lệ node còn nguyên vẹn tối thiểu để khối liên thông lớn tiếp tục tồn tại.

Thay số 1 — đồ thị ngẫu nhiên Erdős–Rényi không có hub. Đồ thị 2.000 node dùng trong mô phỏng A có $\langle k \rangle = 3{,}98$ và $\langle k^2 \rangle = 19{,}9$, cho $\kappa = 5{,}01$ và $p_c = 0{,}249$. Con số này khớp chính xác với lý thuyết 0,25.

Thay số 2 — đồ thị có hub, cùng bậc trung bình. Đồ thị hub 2.000 node trong cùng mô phỏng đó có $\langle k \rangle = 4{,}00$ nhưng $\langle k^2 \rangle = 47{,}2$, cho $\kappa = 11{,}8$ và $p_c = 0{,}093$. Node lớn nhất nối tới 113 node khác.

Thay số 3 — một đồ thị service giả định, lệch mạnh hơn. Hệ thống gồm 500 service: 497 service chỉ nối 3 service khác, còn 3 hub bị 200 service gọi tới. Bậc trung bình chỉ là 4,18 nhưng $\langle k^2 \rangle$ vọt lên 249, cho $\kappa = 59{,}5$ và $p_c = 0{,}017$. Chỉ ba hub đã kéo ngưỡng thấm từ 25% xuống dưới 2%.

Nói lại kết luận của khối trên bằng lời: giữ nguyên số máy và số liên kết, chỉ cần dồn liên kết về vài hub là ngưỡng thấm tụt từ 25% xuống dưới 10%. Với đồ thị service lệch mạnh, con số này xuống dưới 2%. Đó là ranh giới mong manh mà sự cố cần vượt qua để lan xuyên toàn cụm.

Khi một mạng phải dựa vào một mạng khác #

Quay lại bài toán ba bản sao ở ba AZ. Trên slide, đó là ba node độc lập. Trong thực tế, cả ba cùng đọc cấu hình từ một cụm service discovery, hoặc cùng phân giải địa chỉ qua một cụm DNS nội bộ. Service discovery là sổ danh bạ của hệ thống: service muốn gọi service khác thì hỏi nó địa chỉ. Đó không phải ba node độc lập; đó là hai mạng ghép vào nhau.

Sergey Buldyrev và cộng sự (2010) mô hình hóa đúng tình huống này, lấy cảm hứng từ đợt mất điện toàn nước Ý năm 2003, nơi lưới điện và mạng điều khiển kéo nhau xuống. Phát hiện của họ: khi hai mạng phụ thuộc lẫn nhau, sự cố kéo sập chéo giữa hai tầng mạng. Lớp A mất node do lỗi phần cứng, khiến lớp B mất các node phụ thuộc vào A. Lớp B tan rã lại làm lớp A mất tiếp các node đang chạy tốt, và vòng lặp tiếp diễn.

Vòng lặp ấy đổi hẳn hình dạng của cú sập. Một mạng đứng riêng suy giảm theo đường dốc thoải. Hai mạng phụ thuộc nhau suy giảm theo một vách đá: gần như không thấy dấu hiệu gì, rồi sập toàn bộ.

Con số chốt, và đây là con số quan trọng nhất của bài. Cùng bậc trung bình 4: một mạng đứng riêng chịu được việc mất tới 75% số node trước khi tan rã hoàn toàn. Nhưng khi ghép với một control plane phụ thuộc hai chiều, hệ thống sập khi chỉ mới mất 39%. Và nó không vỡ dần, nó vỡ trong một bậc3.

Bài 06 tìm ra hình dạng vách đá này từ phía tải: hệ quá tải không chậm dần đều mà rơi thẳng rồi kẹt lại. Đây là đúng hình dạng đó, tìm ra từ cấu trúc kết nối của mạng, không cần nhắc tới tải một lần nào. Hai con đường khác nhau, một kết luận: hệ thật không suy giảm tuyến tính, nó nhảy bậc.

IV. Từ đồ thị sang hệ thống của bạn: bảng quy đổi #

Để quy đổi được, cần tách bạch hai tấm bản đồ mà người ta hay nhầm là một.

  • Đồ thị dịch vụ. Ai gọi ai để phục vụ người dùng. Đây là sơ đồ mọi đội kỹ thuật đều có.
  • Đồ thị sự cố. Lỗi ở đây thì kéo theo lỗi ở đâu. Nó gồm toàn bộ cạnh của đồ thị dịch vụ, cộng thêm mọi tuyến failover, mọi chỗ dùng chung tài nguyên, và mọi phụ thuộc vào control plane.

Đồ thị sự cố luôn dày hơn, và nó mới là đồ thị quyết định ngưỡng thấm. Sơ đồ kiến trúc trên tài liệu nội bộ có 12 mũi tên. Đồ thị sự cố có đủ cả 12 mũi tên đó, cộng thêm mọi thứ không ai vẽ vì nếu vẽ vào thì sơ đồ sẽ biến thành mạng nhện.

Bảng dưới là từ điển quy đổi giữa ba thứ tiếng: toán học, mỏ neo đời thường, và hệ thống thật:

Khái niệm trên đồ thịTrên mạng hẻm / cầu Sài GònTrong hệ thống phần mềm
NodeMột căn nhà, một cây cầuMột service instance, một pod, một AZ, một cell
CạnhMột con hẻm nối hai nhàMột lời gọi API, một tuyến sao chép dữ liệu, một đường failover, một pool dùng chung
Tỉ lệ còn nguyên vẹnTỉ lệ hẻm còn đi đượcTỉ lệ instance còn báo khỏe về load balancer
Bậc trung bìnhMỗi nhà thông ra mấy hẻmMột service gọi trung bình bao nhiêu service khác
Độ chênh lệch bậcVài ngã tư lớn nối hàng chục hẻmMức dồn liên kết về hub của đồ thị phụ thuộc, đo được từ service mesh
Ngưỡng thấmTỉ lệ hẻm tối thiểu để còn xuyên được khuTỉ lệ instance tối thiểu để cụm còn phục vụ được, đồng thời là ngưỡng để một lỗi lan xuyên
Khối liên thông lớnVùng bạn còn đi tới được bằng hẻmPhần hệ còn phục vụ được sau sự cố, cũng là phạm vi ảnh hưởng tối đa của một lỗi
Mảnh vụn rời khỏi cụm lớnVài con hẻm cụt không thông ra đườngNhóm instance vẫn chạy nhưng không ai định tuyến tới: sống mà không phục vụ
Đồ thị nối chéo đầy đủBa cây cầu đều đổ xe cho nhau3 AZ cấu hình failover chéo: không phải ba thứ độc lập, mà là một khối 3 đỉnh nối hết
Lỗi ngẫu nhiênMột hẻm tình cờ bị đàoMột pod bị thu hồi, một đĩa hỏng, một lần restart
Lỗi có chọn lọcPhong tỏa đúng cây cầu đông nhấtSự cố nổ ở service tải cao nhất — tức kịch bản mặc định, vì tải đi theo hub
Tải trên một nodeLượng xe qua cầu mỗi giờRequest mỗi giây, số kết nối, số truy vấn database mà instance đó gánh
Sức chứa thiết kếSức chứa thiết kế của cầuSức chứa của instance trước khi độ trễ vượt ngưỡng chịu đựng
Tỷ lệ headroom đệmCầu dư bao nhiêu phần trăm so với xe thườngPhần sức chứa chừa lại: mức đệm 0,5 nghĩa là chạy ở 67% sức chứa
Phân phối lại tải khi node chếtXe chảy sang những cầu còn lạiLoad balancer đẩy request sang instance hoặc AZ còn sống
Cạnh phụ thuộc giữa hai mạngHệ thống đèn tín hiệu giao thông phụ thuộc vào lưới điện thành phốCả ba bản sao cùng đọc DNS, service discovery, config, IAM, cùng qua một pipeline deploy
Vòng sập chéo giữa hai tầngCúp điện làm tắt đèn kéo theo kẹt xe, xe thợ điện kẹt không tới sửa được trạm nên mất điện kéo dài và kẹt xe lan tiếpApp mất control plane, control plane mất client, app còn lại mất cấu hình, lặp tiếp
Cú sập đột ngột (bước nhảy tê liệt)Cả khu đứng cứng gần như tức thìVách đá của Bài 06, lần này sinh ra từ hình dạng đồ thị chứ không từ mức tải
Cắt đồ thị thành nhiều cụmChia khu thành các ô không thông hẻm với nhauCell-based architecture, shuffle sharding, bulkhead

Vì sao sự cố nằm sẵn trong cấu trúc #

Bốn bước, mỗi bước kết bằng một con số:

1. Dự phòng sinh ra cạnh mới. Mỗi tuyến failover từ A sang B là một cạnh trên đồ thị sự cố, dù trên đồ thị dịch vụ nó không tồn tại. Ba AZ dự phòng chéo nhau tạo thành tam giác liên kết có 3 đỉnh nối chéo với nhau, tức 3 cạnh mà không ai đếm.

2. Thêm cạnh thì độ chênh bậc tăng, ngưỡng tụt. Cạnh dự phòng gần như luôn nối vào chỗ vốn đã đông. Cái gì đã được gọi nhiều thì càng đáng tin để gọi thêm, đúng cơ chế tôi đã mô tả ở Bài 04. Vì vậy, độ lệch bình phương tăng nhanh hơn bậc trung bình, kéo ngưỡng thấm tụt sâu theo Molloy–Reed. Trong mô phỏng A, phép dồn đó kéo ngưỡng từ 0,249 xuống 0,093.

3. Tải chảy dọc theo cạnh. Node chết, tải của nó sang hàng xóm. Hàng xóm sống sót nếu tải mới còn nằm trong sức chứa. Phép tính 3 AZ ở Mục II tổng quát hóa thành một dòng: với n vùng, mất 1 vùng thì cần headroom ít nhất bằng 1 chia cho (n trừ 1). Ba AZ cần dư 50%; năm cell chỉ cần dư 25%.

4. Phụ thuộc chung biến suy giảm thành sập. Vòng sập chéo giữa hai tầng không để hệ xuống dốc từ từ. Nó giữ hệ trông như bình thường rồi lấy đi tất cả trong một bước nhảy tê liệt. Ngưỡng chịu đựng đi từ mất 75% xuống mất 39%.

Bước 3 là lập luận định lượng mạnh nhất cho kiến trúc cell, và nó chỉ dài một dòng. Mục V nói về đường đi của sự cố; Mục VI kiểm tra cả bốn bước bằng mô phỏng.

V. Sự cố đi đường nào, và tại sao cắt bớt đường lại là cách chặn #

Ba đường lan, tách bạch #

  • Lan theo phụ thuộc. A gọi B, B treo, A ngồi chờ tới lúc cạn tài nguyên rồi chết theo. Đây là đường đã bàn ở Bài 04, và là đường mà circuit breaker sinh ra để chặn.
  • Lan theo tải. A chết, tải của A sang B, B vỡ, tải lại sang C. Đây là đường mà redundancy trực tiếp mở ra, và là trọng tâm của bài này.
  • Lan theo phụ thuộc chung. A và B không gọi nhau, không chia tải cho nhau, phục vụ hai nghiệp vụ khác hẳn. Cả hai cùng đọc một service cấu hình. Service đó trả về rác, cả hai chết trong cùng một phút, và trên sơ đồ luồng dữ liệu không có cạnh nào nối chúng.

Đường thứ ba là đường khó nhất, vì nó không để lại dấu vết trên bất kỳ sơ đồ nào — kể cả sơ đồ đúng.

Ngưỡng nằm ở đâu #

Điều kiện để một đợt lan lỗi tắt viết được bằng một câu: mỗi node chết trung bình phải làm chết dưới một node khác. Đây chính là hệ số lây lan dịch bệnh mà ai cũng quen sau năm 2020. Nếu hệ số này lớn hơn 1 thì dịch bùng phát, còn nhỏ hơn 1 thì dịch tự tắt dần.

Hệ số nhân đó là tích của hai thông số: mật độ cạnh và độ chặt của khớp nối tải. Mật độ cạnh xác định tải rẽ sang bao nhiêu nhánh khi một node chết. Khớp nối tải đo xem bên nhận có đủ đệm để không chết theo hay không.

Hệ quả kiến trúc nói thẳng: bạn có đúng hai núm vặn. Cell cắt số cạnh. Headroom hạ độ chặt khớp nối.

Vì ngưỡng là tích của hai yếu tố, ta không thể chỉ phụ thuộc vào một núm vặn duy nhất. Trong thực tế, bạn không thể cắt sạch 100% cạnh, cũng không có ngân sách nào duyệt vô hạn headroom. Bắt buộc phải giảm đồng thời cả hai thì hệ số nhân mới xuống dưới 1. Mô phỏng B ở Mục VI cho thấy chuyện đó rõ đến mức khó chịu.

Nhát cắt của kiến trúc cell #

Sơ đồ dưới vẽ ba đường lan trên cùng một hệ, và chỗ mà kiến trúc cell cắt đồ thị. Mũi tên liền là đường sự cố đi thật. Mũi tên đứt nét là cạnh đã bị cắt đi có chủ đích — cell-based architecture chính là quyết định không vẽ cạnh đó.

graph TD subgraph CP["Control plane: DNS, service discovery, config"] SD[Service discovery] end subgraph CellA["Cell A"] A1[Service A - AZ1] A2[Service A - AZ2] end subgraph CellB["Cell B"] B1[Service B - AZ1] B2[Service B - AZ2] end A1 -- "1. AZ1 chết, tải dồn sang hàng xóm" --> A2 A2 -- "2. A2 vỡ theo, dội ngược lên control plane" --> SD SD -- "3. Cắt tỉa ngược: mọi node phụ thuộc mất cấu hình" --> B1 SD --> B2 A1 -.->|"Nhát cắt của cell: cạnh này cố ý không tồn tại"| B1

Chỗ đáng chú ý nhất của sơ đồ là mũi tên số 3. Cell A và Cell B không có cạnh nào nối nhau, vậy mà Cell B vẫn chết — qua control plane. Cắt cạnh ngang là cần, nhưng chưa đủ nếu cạnh dọc vẫn còn.

Hai sự cố thật #

AWS EBS, tháng 4/2011 — khi cơ chế dự phòng là thủ phạm. EBS là dịch vụ ổ đĩa mạng của AWS, cho phép máy ảo thuê ổ đĩa đặt trên cụm lưu trữ riêng với hai bản sao. Trong một thao tác nâng cấp mạng định kỳ ở vùng US-East-1, lưu lượng bị chuyển nhầm sang một mạng phụ không đủ băng thông. Hàng loạt node lưu trữ lập tức mất liên lạc với bản sao của mình.

Từ đây, mọi thứ chạy đúng thiết kế. Mất bản sao thì phải tìm chỗ trống trong cụm để tạo bản sao mới — cơ chế này gọi là re-mirroring nhằm bảo vệ dữ liệu. Vấn đề là hàng nghìn node cùng làm việc đó trong cùng một khoảnh khắc. Cơn bão re-mirroring ngốn sạch băng thông mạng và năng lực xử lý dự phòng của cụm, làm nghẽn chính lớp điều phối4.

Khoảng 13% số volume trong AZ bị ảnh hưởng kẹt lại ở trạng thái không đọc không ghi được, và phải mất bốn ngày mọi thứ mới về bình thường. Nghẽn lan tiếp sang RDS, dịch vụ database của AWS — kể cả những database cấu hình chạy nhiều AZ vốn quảng bá khả năng miễn nhiễm với sự cố một AZ. Đây là mô hình Motter–Lai chạy trên hạ tầng thật: tải dự phòng bóp nghẹt chính hệ sinh ra nó.

Facebook, ngày 4/10/2021 — khi control plane chung kéo tất cả xuống. Một lệnh bảo trì vô tình cắt kết nối mạng trục toàn cầu. Công cụ kiểm tra tự động lẽ ra đã chặn lệnh đó, nhưng nó dính lỗi và hỏng im lặng.

Phần sau mới là phần đáng học. Các máy chủ DNS của Facebook cài đặt cơ chế tự kiểm tra sức khỏe. Nếu không liên lạc được với trung tâm dữ liệu, chúng kết luận mình không khỏe và tự thu hồi các tuyến BGP. BGP là hệ thống biển báo của Internet, chỉ đường cho các mạng khác biết tuyến đi. Với một máy DNS gặp sự cố, tự rút lui là phản xạ đúng.

Khi mạng trục đứt, mọi máy DNS trên toàn cầu nhận chung một chẩn đoán. Tất cả đồng loạt thu hồi BGP, và Facebook biến mất khỏi bản đồ Internet trong khoảng sáu tiếng. Hệ thống không hỏng; nó đồng loạt tự rút lui bằng đúng cơ chế bảo vệ của nó.

Vòng sập chéo khép lại ở mắt xích cuối cùng. Các công cụ liên lạc nội bộ, hệ thống mở cửa tòa nhà và cổng sửa lỗi từ xa đều phụ thuộc vào hạ tầng mạng vừa bị cô lập5. Mạng điều khiển chết vì mất kết nối; công cụ sửa chết vì mất mạng điều khiển. Đây là Buldyrev viết bằng tiếng Anh thay vì tiếng toán.

VI. Ba mô phỏng cho ba luận điểm #

Mỗi mô phỏng kiểm tra một luận điểm. Script đầy đủ kèm seed cố định nằm trong thư mục assets của series; mọi con số dưới đây là kết quả chạy thật, không phải con số lý thuyết chép lại.

Mô phỏng A — ngưỡng là có thật, và lỗi ngẫu nhiên khác hẳn lỗi có chọn lọc. Tôi xét hai đồ thị 2.000 node cùng bậc trung bình 4: một đồ thị ngẫu nhiên không có hub và một đồ thị có hub. Bỏ dần node theo hai cách — hú họa, và theo thứ tự node nhiều liên kết nhất trước — rồi đo phần mạng còn liền khối.

Cách đọc: trục ngang là tỉ lệ node đã mất, trục dọc là tỉ lệ mạng còn dính thành một khối. Hai đường cam là đồ thị có hub, hai đường đen xám là đồ thị ngẫu nhiên Erdős–Rényi; nét liền là mất ngẫu nhiên, nét đứt là mất node đông liên kết trước.

Percolation simulation: a hub-heavy graph resists random node loss but shatters when the highest-degree nodes are removed first

Khoảng cách giữa hai đường cam là toàn bộ luận điểm. Mất 20% số node một cách hú họa: 78% mạng vẫn liền khối. Mất đúng 20% số node đông liên kết nhất: còn lại 1,7%. Cùng một mạng, cùng một số node mất đi, kết quả lệch nhau 45 lần.

Tắt ngẫu nhiên vài máy rồi thấy hệ vẫn sống, tôi từng coi đó là bằng chứng hệ bền. Mô phỏng này nói đó là bằng chứng tôi đã bốc trúng những máy không ai cần.

Mô phỏng B — cascade do phân phối lại tải, và giá của headroom. Đây là mô hình Motter–Lai (2002) trên một đồ thị 400 node có hub. Tải ban đầu của mỗi node lấy theo betweenness: số đường đi ngắn nhất trong mạng buộc phải chạy qua node đó6. Sức chứa bằng tải ban đầu nhân với một hệ số dư — chính là tỷ lệ headroom đệm. Đánh sập đúng một node, tính lại tải, node nào vượt sức chứa thì chết theo, lặp tới khi dừng.

Cách đọc: trục ngang là tỷ lệ headroom đệm, trục dọc là phần mạng còn liền khối sau khi cascade đã dừng hẳn. Đường đen đứt nét là đánh sập một node tải trung bình; đường cam là đánh sập đúng node tải cao nhất.

Motter-Lai load cascade simulation: network surviving after the cascade, plotted against headroom, for a killed highest-load node versus a killed median-load node

Hai đường kể hai câu chuyện khác hẳn nhau. Đánh sập node tải trung bình: chỉ cần 6% headroom là 99% mạng sống sót, và từ đó đường nằm phẳng ở đỉnh. Chuyện thú vị là ở mốc headroom bằng 0, việc tắt đột ngột một node vô hại như vậy cũng ăn mất 46% mạng. Không có dự phòng đệm thì không có gì miễn phí.

Đường cam thì khác. Đánh sập node tải cao nhất khi không có headroom đệm: mạng còn 1,3%. Đồ thị nằm bẹp dưới 7% cho tới mức headroom khoảng 0,29, rồi bò lên theo từng bậc: 13% ở mức 0,29; 33% ở mức 0,41; 50% ở mức 0,58. Ở mức 0,70, tức chạy ở 59% sức chứa, mạng vẫn chỉ giữ được 55%.

Kết luận không dễ chịu, nhưng đúng với luận điểm ở Mục V: headroom là một trong hai núm vặn, và nó không tự mình đủ. Mua headroom tới mức không ngân sách nào duyệt vẫn chưa cứu được đồ thị dồn tải vào vài hub. Muốn đủ thì phải vặn cả núm còn lại, tức cắt bớt cạnh. Mô hình này dùng betweenness làm đại diện cho tải nên con số tuyệt đối không mang sang hệ thật; thứ mang sang được là hình dạng hai đường và khoảng cách giữa chúng.

Đào sâu: bao nhiêu headroom thì cascade tắt

Motter và Lai (2002) định nghĩa sức chứa và luật xả tải như sau:

$$ C_i = (1 + \alpha)\, L_i(0), \qquad L_i(t+1) = L_i(t) + \sum_{j \in \text{failing neighbors}} \frac{L_j}{d_j} $$

Đọc bằng lời: Mỗi node có sức chứa bằng tải lúc bình thường cộng thêm một phần dư. Khi một hàng xóm chết, tải của nó chia đều cho những node còn sống quanh nó. Node nào nhận quá phần dư của mình thì chết, và lại chia tải tiếp.

  • $L_i$: Tải trên node $i$, đơn vị là request mỗi giây.
  • $\alpha$: Tỷ lệ headroom đệm. $\alpha = 0{,}5$ nghĩa là node chạy ở 67% sức chứa, chừa 33% làm đệm an toàn.
  • $d_j$: Số hàng xóm còn sống của node $j$ vừa chết.
  • $C_i$: Sức chứa tối đa của node $i$ trước khi nó sập.

Thay số 1 — trường hợp chia đều, 3 AZ. Mỗi AZ đang xử lý 1.000 request mỗi giây. Mất 1 AZ, 1.000 request đó chia cho 2 AZ còn lại, mỗi bên nhận thêm 500. Tải mới là 1.500, tức gấp rưỡi. Cần $\alpha \ge 0{,}5$, nghĩa là ngày thường không được chạy quá 67% sức chứa.

Thay số 2 — trường hợp chia nhỏ hơn, 5 cell. Mỗi cell 600 request mỗi giây. Mất 1 cell, 600 request chia cho 4 cell còn lại, mỗi bên nhận thêm 150, tức tăng 25%. Cần $\alpha \ge 0{,}25$, nghĩa là được chạy tới 80%.

Tổng quát: $n$ vùng thì cần $\alpha \ge 1/(n-1)$. Hệ quả tài chính cần nói chính xác: phần dư phải mua giảm từ 50% xuống 25% của tải. Như vậy, tổng đội máy giảm từ 1,5 lần tải xuống 1,25 lần tải — tiết kiệm khoảng 17%, chứ không giảm một nửa. Phần đệm thì đúng là giảm một nửa.

Nói lại bằng lời: chia hệ thành càng nhiều vùng thì headroom cần cho mỗi vùng càng ít, vì tải của vùng chết được chia cho nhiều bên hơn. Đó là lập luận ngân sách cho kiến trúc cell, và nó là số học chứ không phải gu thẩm mỹ.

Mô phỏng C — ghép hai mạng vào nhau thì dốc thoải thành vách. Hai đồ thị ngẫu nhiên 2.500 node, cùng bậc trung bình 4, ghép phụ thuộc hai chiều một–một. Bỏ ngẫu nhiên một phần node ở lớp thứ nhất, rồi để thuật toán lặp sập chéo giữa hai tầng tới khi ổn định. Đường so sánh là một mạng đứng riêng, cùng kích thước, cùng bậc.

Cách đọc: trục ngang là tỉ lệ node còn nguyên vẹn ban đầu, đi từ trái sang phải là mạng ngày càng lành lặn. Trục dọc là phần mạng còn liền khối sau khi mọi thứ dừng lại. Đường đen là mạng đơn, đường cam là cặp mạng phụ thuộc, vạch chấm dọc là ngưỡng lý thuyết.

Interdependent percolation simulation: a single network degrades continuously from p_c = 0.25 while two interdependent networks stay at zero until a discontinuous jump at p_c = 0.61

Đường đen rời khỏi đáy quanh 0,25 rồi bò lên đều đặn, đúng ngưỡng lý thuyết. Đường cam nằm ở 0 cho tới tận mốc 0,575, tức mất 42,5% số node mà mạng không còn một cụm chung nào. Sau đó, nó nhảy thẳng lên 25% ở mốc 0,60 và 46% ở mốc 0,65. Không có đoạn chuyển tiếp nào ở giữa.

Cùng số máy, cùng số liên kết, chỉ thêm một sợi dây phụ thuộc dọc: ngưỡng sống sót dịch từ mất 75% mới vỡ sang mất 39% là hết. Hình dạng đồ thị đổi từ dốc sang bậc thang. Một điều cần nói thêm cho công bằng với đường đen: ngưỡng 0,25 là mốc khối lớn biến mất hẳn, không phải mốc hệ còn phục vụ được. Ngay trên ngưỡng đó, cụm còn lại chỉ ôm vài phần trăm số node. Ngưỡng thấm là biên của việc còn tồn tại, không phải biên của việc còn dùng được.

Đào sâu: hằng số của Buldyrev và code ba mô phỏng

Với hai mạng ngẫu nhiên phụ thuộc hoàn toàn vào nhau, Buldyrev và cộng sự (2010) tính ra ngưỡng:

$$ p_c = \frac{2{,}4554}{\langle k \rangle} $$

Đọc bằng lời: Ghép hai mạng lại thì ngưỡng sống sót cao hơn khoảng 2,46 lần so với để chúng đứng riêng. Bạn cần giữ được nhiều hơn gấp 2,46 lần số node thì hệ mới tồn tại.

Thay số với bậc trung bình 4: một mạng đứng riêng có ngưỡng thấm $p_c = 1/(\kappa - 1) = 0{,}25$; cặp mạng phụ thuộc có ngưỡng thấm $p_c = 2{,}4554/4 = 0{,}614$. Mô phỏng C xuất hiện bước nhảy ở khoảng 0,60; sai lệch này nằm trong dung sai thông thường của mạng 2.500 node.

Lõi code của ba mô phỏng, dùng networkx và numpy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import numpy as np
import networkx as nx

# A — bỏ node theo hai cách, đo cụm liên thông lớn nhất
def evaluate(G0, mode, N, steps=50):
    G, nodes = G0.copy(), list(G0.nodes())
    if mode == "random":
        np.random.shuffle(nodes)
    else:                                    # bỏ node nhiều liên kết nhất trước
        nodes.sort(key=lambda n: G.degree(n), reverse=True)
    out, prev = [], 0
    for idx in (np.linspace(0, 0.9, steps) * N).astype(int):
        G.remove_nodes_from(nodes[prev:idx]); prev = idx
        out.append(len(max(nx.connected_components(G), key=len)) / N if len(G) else 0.0)
    return out

# B — cascade theo tải: đánh sập 1 node, xả tải, ai vượt sức chứa thì chết theo
def cascade(G0, load0, alpha, target, N):
    G = G0.copy()
    cap = {n: (1 + alpha) * load0[n] for n in G}
    G.remove_node(target)
    while len(G):
        load = nx.betweenness_centrality(G, normalized=False)
        dead = [n for n in G if load[n] > cap[n]]
        if not dead:
            break
        G.remove_nodes_from(dead)
    return len(max(nx.connected_components(G), key=len)) / N if len(G) else 0.0

# C — vòng lặp sập chéo giữa hai mạng phụ thuộc, tới khi không ai chết thêm
def mutual_component(GA, GB, alive_A, alive_B):
    while True:
        prev = len(alive_A) + len(alive_B)
        for G, keep, other in ((GA, "A", "B"), (GB, "B", "A")):
            live = alive_A if keep == "A" else alive_B
            sub = G.subgraph(live)
            if not len(sub):
                return set(), set()
            live = set(max(nx.connected_components(sub), key=len))
            # Giả định ID của node mạng A và mạng B khớp nhau 1-1
            if keep == "A":
                alive_A = live; alive_B &= alive_A
            else:
                alive_B = live; alive_A &= alive_B
        if len(alive_A) + len(alive_B) == prev:
            return alive_A, alive_B

Script đầy đủ, seed cố định và các con số in ra nằm trong post-assets/07-percolation-cascading-failures/.

VII. Ba việc nên làm, và cái giá của từng việc #

Trước khi kê đơn, một phản biện chính đáng cần trả lời. Cô lập tài nguyên đi ngược lại chính lý do người ta lên cloud. Gộp chung tài nguyên để các phần san tải cho nhau là nguồn gốc của hiệu quả kinh tế; chia thành cell là chủ động từ bỏ một phần hiệu quả đó.

Với hệ nhỏ và vừa, một cụm dùng chung với headroom tử tế thường vẫn là lựa chọn đúng, và tôi sẽ không khuyên khác. Ranh giới chỉ xuất hiện khi thiệt hại từ một sự cố lan rộng đã vượt quá khoản chi phí tiết kiệm được nhờ dùng chung tài nguyên. Ba việc dưới đây dành cho lúc đã qua ranh giới đó.

Cần phân biệt với Bài 04 ngay từ đầu, vì nghe qua rất giống. Bulkhead và circuit breaker ở Bài 04 chia tài nguyên bên trong một service để một bên gọi hỏng không ngốn hết thread pool. Ba việc dưới đây cắt trên toàn bộ đồ thị.

1. Cắt đồ thị bằng cell, thay vì tin vào số lượng bản sao.

Cell-based architecture: mỗi cell là một bản đầy đủ của hệ thống — đủ ứng dụng, đủ cache, đủ database — và khách hàng gán cố định vào một cell. Điều phân biệt nó với kiến trúc nhiều bản sao thông thường nằm ở một luật duy nhất: giữa các cell không có cạnh nào. Không gọi API chéo, không dùng chung database, không failover qua lại. Nếu cạnh đã không tồn tại thì sự cố không có đường để lan rộng khắp hệ thống.

Shuffle sharding đi thêm một bước. Thay vì chia khách hàng thành các khối tách rời, nó gán mỗi khách hàng vào một tổ hợp node khác nhau, sao cho hai khách hàng rất khó trùng trọn bộ. Một khách hàng gửi truy vấn độc hại làm chết đúng tổ hợp của họ; khách hàng bên cạnh còn ít nhất một node khác biệt nên vẫn chạy.

Câu nên hỏi đội kỹ thuật: một sự cố tệ nhất ở tầng này chạm tới bao nhiêu phần trăm khách hàng — và con số đó đến từ thiết kế hay từ may mắn?

2. Tính headroom theo tải sau khi chuyển dự phòng, không theo tải bình thường.

Áp đúng phép tính ở Đào sâu 2: với n vùng thì cần headroom ít nhất bằng 1 chia cho (n trừ 1). Rồi mở rộng nó thành một phép thử cho từng mũi tên đứt nét trên sơ đồ. Khi tuyến failover kích hoạt, tải chảy về đâu, và bên nhận ngày thường đang chạy ở bao nhiêu phần trăm? Không ai trả lời được thì tuyến đó là giả định chưa kiểm tra.

Đây là câu hỏi thứ ba của series về headroom, và nó khác hai câu trước. Bài 05 hỏi headroom có đủ để hệ khỏi trôi tới mép không. Bài 06 hỏi sức chứa nên tính tại điểm nào. Bài 07 hỏi headroom nằm ở đâu, và để dành nhận tải của ai.

Câu nên hỏi đội kỹ thuật: nếu mất một AZ ngay bây giờ, hai AZ còn lại lên bao nhiêu phần trăm — và ai biết con số đó mà không cần chạy thử?

3. Lập danh sách những thứ mọi bản dự phòng cùng phụ thuộc.

Với mỗi cặp dự phòng lẫn nhau, hãy liệt kê mọi thứ cả hai cùng cần để hoạt động. Danh sách gồm: DNS, service discovery, kho cấu hình, hệ thống phân quyền, nhà cung cấp chứng chỉ, pipeline deploy, hệ thống giám sát. Trớ trêu nhất là cả VPN và máy chủ trung gian mà kỹ sư trực cần dùng để vào khắc phục sự cố. Mỗi dòng trong danh sách là một cạnh phụ thuộc ở Mục V, thứ biến ba bản sao độc lập thành một điểm nghẽn duy nhất.

Đối sách kỹ thuật cho nhóm này gọi là static stability: data plane chạy tiếp bằng cấu hình đã nạp sẵn khi control plane không liên lạc được. Cái giá rất thật: hệ chạy bằng dữ liệu cũ, và với nghiệp vụ tiền bạc, cũ nhiều khi đồng nghĩa với sai. Giữa sai cục bộ có kiểm soát và sập cả hệ, đó là một lựa chọn phải cân, không phải một lựa chọn hiển nhiên.

Đây là việc buồn tẻ nhất trong ba việc. Nó không đẻ ra sơ đồ nào đẹp, không thành bài nói hội thảo nào, và kết quả bàn giao là một bảng tính. Nó cũng là việc duy nhất tìm ra được thứ không nằm trên bất kỳ sơ đồ nào.

Câu nên hỏi đội kỹ thuật: nếu service discovery không trả lời trong 30 phút, hệ vẫn phục vụ được, hay chỉ đứng chờ?

Biện phápĐược gìTrả giá gì
Cắt đồ thị bằng cell / shuffle shardingPhạm vi ảnh hưởng tối đa là con số bạn chọn, không phải con số bạn phát hiện sau sự cố; ngăn chặn triệt để nguy cơ sập toàn hệ thốngMất phần hiệu quả của việc gộp tài nguyên; thao tác xuyên cell trở nên đắt; bộ định tuyến cell thành hub mới, đúng bài toán của Bài 04; số việc vận hành nhân lên
Headroom tính theo tải sau failoverCascade theo tải tắt thay vì lan; số máy cần thiết thành một phép tính thay vì một cuộc tranh luậnTiền máy tăng, và đây là khoản khó bảo vệ nhất trong họp ngân sách vì nhìn qua giống hệt máy chạy không; phải đo lại mỗi lần cấu trúc liên kết thay đổi
Danh sách phụ thuộc chung + static stabilityTìm ra những cạnh không nằm trên sơ đồ nào; data plane sống sót khi control plane chếtViệc thủ công, phải làm lại định kỳ và nhanh lạc hậu; chạy bằng cấu hình cũ có lúc đồng nghĩa với chạy sai; tách riêng control plane cho từng cell thì nhân đôi chi phí vận hành nó

VIII. Tài liệu tham khảo #


  1. GitHub, 21/10/2018. Một sự cố bảo trì cáp quang làm mất kết nối 43 giây giữa trung tâm dữ liệu bờ Đông nước Mỹ và một điểm trung chuyển mạng. Cơ chế bầu chọn tự động của cụm database chuyển vai trò ghi sang bờ Tây, và trong khoảng thời gian đó hai bờ nhận ghi những dữ liệu khác nhau. Hệ quả là 24 giờ 11 phút dịch vụ suy giảm, phần lớn dành cho việc đối chiếu thủ công. ↩︎

  2. Doyle và cộng sự (2005), Willinger và cộng sự (2009) cảnh báo rằng hình dạng kết nối router thật của Internet không dồn về hub như mô hình giả định. Lý do là router vật lý bị giới hạn bởi số cổng kết nối. Kết luận về hub trong bài này áp dụng cho đồ thị service — nơi một lời gọi chỉ là một dòng cấu hình — chứ không áp thẳng cho hạ tầng mạng vật lý. ↩︎

  3. Con số 39% giả định hai mạng phụ thuộc hoàn toàn và ghép ngẫu nhiên. Parshani, Buldyrev và Havlin (2010) chứng minh rằng khi chỉ một phần node phụ thuộc lẫn nhau, cú nhảy mềm dần và ở mức phụ thuộc đủ thấp thì quay về dạng dốc thoải. Trong hệ thật, giảm tỉ lệ thành phần phụ thuộc vào control plane chính là cách làm cú sập mềm đi. ↩︎

  4. Theo bản tổng kết của AWS (2011): cụm EBS không giới hạn tốc độ tìm dung lượng trống trên toàn cụm. Đồng thời, vòng lặp tìm kiếm không giãn khoảng cách giữa các lần thử lại. Hai chi tiết đó biến một đợt mất kết nối ngắn thành bốn ngày sự cố. ↩︎

  5. Theo bản tổng kết của Meta (2021): công cụ kiểm tra lệnh bị lỗi nên không chặn được lệnh ngắt mạng. Việc hệ thống mở cửa tòa nhà phụ thuộc vào cùng hạ tầng mạng khiến kỹ sư mất nhiều giờ để tiếp cận phần cứng. ↩︎

  6. Betweenness của một node là số cặp node khác mà đường đi ngắn nhất giữa chúng phải chạy qua node đó. Chỉ số này đại diện cho tải với giả định mọi luồng đều đi đường ngắn nhất và mọi cặp đều giao tiếp như nhau. Hệ thống thực tế không thỏa hai giả định này, nên đây chỉ là đại diện định tính. ↩︎