Phần 1 để lại một câu hỏi lơ lửng: gửi yêu cầu mà không có hồi âm, máy kia chết hay mạng chậm? Mọi phần sau đều dựa vào câu trả lời cho nó — bầu chủ cần biết chủ cũ đã chết, Raft cần biết khi nào mở bầu cử, khoá phân tán cần biết người giữ khoá còn sống. Bài này đo thẳng vào bộ phận trả lời câu hỏi đó: bộ phát hiện lỗi. Năm bộ dò chạy song song trên cùng một luồng heartbeat thật, qua mạng bị làm xấu bằng tc netem, rồi tiến trình gửi bị giết, bị dừng, và được đếm xem ai báo đúng, ai báo oan, và ai báo sớm.

Năm bộ dò, một luồng heartbeat

Ảnh chụp đoạn mã nền tối phát hiện lỗi heartbeat UDP 5 bộ dò chạy song song và gossip với Go 1.23 gửi từ dist-lab qua tc netem nhận ở go-lab heartbeat mỗi 100 ms tick kiểm tra 5 ms, timeout cố định chết bằng im lặng quá T gồm struct fixed với T và last, suspect trả về now trừ last lớn hơn T với T là 300 ms 1 s 3 s, phi accrual Hayashibara 2004 học phân bố khoảng cách heartbeat trả về mức nghi ngờ phi gồm win là 100 khoảng cách gần nhất tính mean và std sàn 10 ms, t là thời gian từ heartbeat cuối, y bằng t trừ mean chia std, e bằng exp của trừ y nhân 1.5976 cộng 0.070566 y bình phương là xấp xỉ CDF chuẩn như Akka, phi bằng trừ log10 của e chia 1 cộng e với phi 8 xấp xỉ xác suất 1e-8 heartbeat còn tới, suspect khi phi lớn hơn ngưỡng 8 Akka và 12, vòng kiểm tra mỗi 5 ms hỏi cả 5 bộ dò in ra mỗi lần đổi trạng thái gồm for now range tick.C, for i d range dets, nếu s bằng d.suspect now khác state i thì gán và log EV, ghi chú bên gửi ticker 100 ms hết giờ thì thoát không báo trước là chết SIGSTOP 600 ms là GC pause, gossip mỗi vòng mỗi nút tăng counter của mình rồi gửi cả bảng cho F nút ngẫu nhiên gồm vòng for x range N nếu n.cnt x lớn hơn m.cnt x thì gán và m.seen x bằng round là max, nghi ngờ x khi round trừ m.seen x lớn hơn Tfail là không thấy tăng quá Tfail vòng

Hình 1: Timeout cố định so với phi accrual, cùng nhận một luồng heartbeat. Phần gossip là mô phỏng trong một tiến trình.

Tiến trình gửi chạy trong dist-lab (có quyền NET_ADMIN để dùng tc netem), bắn một gói UDP mỗi 100 ms sang go-lab. Bên nhận có năm bộ dò:

  • Timeout cố định 300 ms, 1 s, 3 s: nghi ngờ khi im lặng quá T. Đơn giản, dễ hiểu, và là thứ hầu hết mã đang chạy ngoài kia dùng.
  • Phi accrual (Hayashibara 2004, dùng trong Cassandra và Akka): giữ 100 khoảng cách heartbeat gần nhất, tính trung bình và độ lệch chuẩn, rồi trả về φ = −log₁₀ của xác suất "heartbeat vẫn còn đang tới". φ = 8 nghĩa là xác suất đó dưới 10⁻⁸. Ngưỡng 8 là mặc định của Akka; thêm ngưỡng 12 để thấy đánh đổi.

"Chết" là tiến trình gửi thoát không báo trước. "GC pause" là SIGSTOP 600 ms rồi SIGCONT. Mạng xấu là netem delay 80ms 60ms distribution normal loss 15%. Mỗi kịch bản ba lượt 20 giây.

Đo thật: ba kịch bản, một bảng

Ảnh chụp output thật nền tối nhanh hay ít báo oan không có cả hai với heartbeat 100 ms mỗi kịch bản 3 lượt 20 s rồi tiến trình gửi thoát đột ngột, mục 1 báo oan trong 60 s và thời gian phát hiện chết thật trung bình 3 lượt gồm bảng bộ dò mạng ổn định và jitter 80 cộng trừ 60 ms mất 15 phần trăm gói, timeout 300 ms 0 báo oan chết 307 ms và 9 báo oan nghi oan 520 ms chết 332 ms, timeout 1 s 0 chết 1007 ms và 0 chết 1032 ms, timeout 3 s 0 chết 3007 ms và 0 chết 3032 ms, phi lớn hơn 8 0 chết 157 ms và 1 báo oan 35 ms chết 554 ms, phi lớn hơn 12 0 chết 167 ms và 0 chết 635 ms, mục 2 dừng tiến trình gửi 600 ms bằng SIGSTOP giả GC pause 3 lần một lượt gồm timeout 300 ms báo oan 3 trên 3 ở mạng ổn định và báo oan 3 trên 3 cộng 3 lần do mất gói liên tiếp ở mạng jitter, phi lớn hơn 8 báo oan 3 trên 3 nhưng chậm dần 155 420 415 ms và im lặng 0 trên 3 vì đã học khoảng cách rộng, phi lớn hơn 12 báo oan 3 trên 3 và im lặng 0 trên 3, timeout 1 s 3 s im lặng cả hai, chết thật sau đó phi 8 558 ms ổn định sau 3 lần dừng và phi 8 831 ms phi 12 951 ms jitter, mục 3 gossip N 100 nút giết 1 nút 30 lần thử mỗi cấu hình mô phỏng seed 42 gồm bảng F Tfail nút đầu nghi tất cả nghi nghi oan nút vòng thông điệp mỗi vòng với F 2 Tfail 3 TB 1.7 vòng TB 7.5 471.970 194, F 2 Tfail 5 TB 5.0 TB 9.1 7.624 194, F 2 Tfail 10 TB 10.0 TB 14.0 0 194, F 3 Tfail 10 TB 10.0 TB 12.6 0 291, heartbeat trực tiếp mọi nút tới mọi nút 9.900 thông điệp mỗi vòng, lan 1 tin mới tới 100 phần trăm nút với F 2 N 100 7.4 vòng N 1.000 10.7 vòng N 10.000 14.0 vòng, nói thẳng không bộ dò nào biết chết chỉ biết im lặng lâu hơn mức tôi quen phi accrual học được jitter nên bớt báo oan nhưng phát hiện chậm hơn 5 lần 157 tới 831 ms gossip đổi 9.900 thông điệp lấy 194 trả bằng khoảng 10 vòng trễ

Hình 2: Kết quả thật. Mục 1 và 2 đo qua mạng giữa hai container; mục 3 là mô phỏng 100 nút trong một tiến trình, seed cố định.

bộ dò ổn định: báo oan / phát hiện jitter + mất gói: báo oan / phát hiện
timeout 300 ms 0 / 307 ms 9 lần / 332 ms
timeout 1 s 0 / 1.007 ms 0 / 1.032 ms
timeout 3 s 0 / 3.007 ms 0 / 3.032 ms
phi > 8 0 / 157 ms 1 lần / 554 ms
phi > 12 0 / 167 ms 0 / 635 ms

Mạng ổn định thì cái gì cũng đúng, và phi nhanh nhất. Không bộ dò nào báo oan trong 60 giây. Phi > 8 phát hiện chết sau 157 ms — nhanh hơn cả timeout 300 ms — vì nó biết heartbeat đến đều 100 ± vài ms, nên im lặng 150 ms đã là bất thường ở mức 10⁻⁸. Timeout cố định thì phát hiện đúng bằng T cộng vài ms, không hơn không kém: nhanh hay chậm là do bạn chọn trước, không liên quan gì tới mạng thật.

Thêm jitter và mất gói, timeout 300 ms báo oan 9 lần một phút. Mất 15% gói nghĩa là cứ khoảng 44 lần thì có một lần hai gói liên tiếp cùng mất, tức 300 ms im lặng — đúng ngưỡng. Mỗi lần báo oan ngắn (tổng 520 ms trong 60 giây), nhưng nếu phía sau là "chuyển chủ" hay "mở bầu cử" thì 9 lần một phút là thảm hoạ. Timeout 1 s và 3 s không báo oan, đổi lại phát hiện chết sau 1 và 3 giây. Phi > 12 không báo oan lần nào và phát hiện chết sau 635 ms: nó tự nới ngưỡng theo mạng — cùng một dòng mã, mạng xấu hơn thì chờ lâu hơn. Đó chính là lý do nó tồn tại.

Mục 2 là chỗ phi lộ ra bản chất. Dừng tiến trình gửi 600 ms ba lần. Trên mạng ổn định, phi > 8 báo oan cả ba lần — tất nhiên, vì 600 ms im lặng trên nền 100 ± 10 ms là φ rất lớn. Nhưng để ý thời điểm báo: lần đầu 155 ms sau heartbeat cuối, lần hai 420 ms, lần ba 415 ms. Nó đã học luôn cả lần dừng đầu tiên vào phân bố và nới ngưỡng ra; đến khi chết thật thì mất 558 ms mới báo thay vì 157. Trên mạng jitter, phi im lặng suốt ba lần dừng — nghe như thắng lợi, nhưng cùng lúc đó nó phát hiện chết thật chậm tới 831 ms, gấp hơn 5 lần mạng ổn định. Phi không phân biệt được "mạng xấu" với "tiến trình hay dừng": với nó mọi thứ đều là phân bố khoảng cách heartbeat.

Mục 3 — gossip thay cho heartbeat trực tiếp. 100 nút mà ai cũng gửi heartbeat cho mọi người là 9.900 thông điệp mỗi vòng; gossip với fanout 2 là 194. Giá là độ trễ lan truyền: một tin mới cần 7,4 vòng để tới đủ 100 nút, 14 vòng với 10.000 nút — tỉ lệ log N, đúng lý thuyết dịch bệnh. Và Tfail phải lớn hơn độ trễ đó: với Tfail = 3 vòng, hệ thống nghi oan 471.970 lần (nút-vòng) trong 30 lần thử vì counter của một nút sống bình thường cũng cần vài vòng mới tới được nút ở xa; Tfail = 10 thì sạch, đổi lại cả cụm mất trung bình 14 vòng mới đồng ý nút kia chết. Fanout 3 rút xuống 12,6 vòng với giá 291 thông điệp.

Đánh đổi cần cân nhắc

Không có bộ dò nào biết "chết". Tất cả chỉ biết "im lặng lâu hơn mức tôi chờ". Bộ dò nhanh thì báo oan nhiều; bộ dò ít báo oan thì chậm. Phi accrual không phá được đánh đổi này, nó chỉ tự chỉnh điểm cân bằng theo mạng thật thay vì để bạn đoán một con số lúc viết cấu hình. Đó là lợi thế lớn khi cùng một mã chạy ở nhiều môi trường khác nhau, và là bất lợi khi bạn cần một cam kết cố định kiểu "phát hiện trong 500 ms".

Hậu quả của báo oan quyết định ngưỡng, không phải thẩm mỹ. Báo oan để chuyển lưu lượng sang bản sao khác thì rẻ, chọn ngưỡng thấp. Báo oan để mở bầu cử, cắt khoá, hoặc coi một nút là chết rồi phân lại dữ liệu (phần 7 đã đo di chuyển dữ liệu tốn thế nào) thì đắt, chọn ngưỡng cao hoặc đòi nhiều nút cùng nghi ngờ (SWIM gọi là indirect ping). Vì thế hệ thống thật thường tách nghi ngờ khỏi kết luận: nghi ngờ nhanh và rẻ, kết luận chậm và cần đồng thuận.

Phi accrual học cả thứ bạn không muốn nó học. Vài lần GC pause là đủ để nó nới ngưỡng và chậm đi; Akka chữa bằng acceptable-heartbeat-pause cộng thẳng vào trung bình, Cassandra dùng phân bố mũ thay vì chuẩn để bớt nhạy với đuôi. Cửa sổ 100 mẫu ở đây tương ứng 10 giây — ngắn thì phản ứng nhanh với mạng đổi, dài thì nhớ lâu hơn những lần dừng cũ.

Gossip đổi băng thông lấy độ trễ và độ chắc. Phù hợp cho thành viên cụm lớn (Cassandra, Consul dùng SWIM), không phù hợp cho thứ cần phát hiện dưới một giây. Và mô phỏng ở đây không có mất gói lẫn trễ mạng; Tfail an toàn ngoài đời phải cộng thêm cả hai.

Phạm vi đo. Hai container trên cùng máy, loopback qua cầu Docker; jitter là netem, không phải mạng thật. Ba lượt mỗi kịch bản là đủ để thấy xu hướng, không đủ để nói về đuôi phân bố. Gói "reset" giữa các lượt cũng đi qua netem và một lần đã mất — phải tách lượt theo mốc thời gian thay vì theo gói đánh dấu.

Ba ý mang về

  1. Phát hiện lỗi là đánh đổi tốc độ với báo oan, không có cấu hình nào thoát: timeout 300 ms phát hiện trong 307 ms nhưng báo oan 9 lần một phút khi mạng xấu; timeout 3 s sạch nhưng mất 3 giây; phi tự dịch điểm cân bằng từ 157 ms lên 831 ms theo chất lượng mạng.
  2. Phi accrual học mọi thứ nó thấy, kể cả GC pause: tiện vì không phải đoán ngưỡng, nguy vì vài lần dừng là nó chậm đi gấp ba. Tách "nghi ngờ" khỏi "kết luận", và chọn ngưỡng theo giá của hành động đi sau lời báo.
  3. Gossip giảm thông điệp từ N² xuống N·F (9.900 → 194) với giá log N vòng trễ, và Tfail phải lớn hơn độ trễ lan truyền — Tfail 3 nghi oan gần nửa triệu lần, Tfail 10 thì không lần nào.

Nguồn

Phần sau rời khỏi câu hỏi "ai còn sống" để sang "dữ liệu nằm ở đâu": chia mảnh theo băm hay theo khoảng, mảnh nóng khi khoá phân bố lệch kiểu Zipf, và cân bằng lại khi đang chạy — đo trên PostgreSQL thật.