deterministik simülasyon testi

raftlab

Sıfırdan yazılmış bir Raft konsensüs implementasyonu ve onu gerçekten hesap vermeye zorlayan simülatör: tek bir tohum, bir kümenin tüm ömrünü yeniden üretir — bölünmeler, çökmeler, GC duraklamaları, saat kayması, paket kaybı — her olaydan sonra on güvenlik değişmezi ve her istemci geçmişi için linearizability denetimiyle.

—simüle edilmiş küme
—denetlenen olay
—küme yaşamı
—gerçek zamana oran

saf Python · sıfır bağımlılık · 56 test · 13 enjekte edilebilir hata · kaynak kod · english

sorun

Konsensüs hataları siz bakarken ortaya çıkmaz

Bir replikasyon log'unda önemli olan arızalar, bölünmenin tam olarak iki belirli mesaj arasında açılmasını, ya da bir liderin girdiyi ekledikten sonra onu kopyalayamadan ölmesini ister. Gerçek bir kümeyi çalıştırıp şansa güvenmek bunları bulmaz; nadiren bulduğunda da gördüğünüzü yeniden üretemezsiniz.

Bu yüzden dünyanın kendisi deterministik hale getirilir: tek thread, sanal saat, tohumlanmış tek bir rastgelelik akışı — FoundationDB, TigerBeetle ve Antithesis'in kurulduğu yaklaşım. Böylece başarısız bir koşu bir hikâye olmaktan çıkıp yeniden oynatabileceğiniz, küçültüp test paketine koyabileceğiniz bir sayı haline gelir.

mimari

Tek tohum, dört aşama

tohum = 4242 tüm girdi bu 1  dünya gecikme · kayıp · kopyalanma bölünme · çökme · duraklama saat kayması ve donması üyelik değişimi · istemci yükü her altsistem için ayrı RNG 2  küme n0n1n2 n3n4 saf durum makineleri: saat yok, soket yok, thread yok step(now, msg) → [msg] 3  denetçi 10 değişmez, her olayda geçmişin linearizability'si iyileşme sonrası canlılık ● ihlal → dur, raporla repro.json minimum, oynatılabilir 4  küçült: bir arızayı sil, yeniden oynat, hâlâ başarısızsa silmeyi koru
Her ok, tek bir süreçteki bir fonksiyon çağrısıdır. Hiçbir şey duvar saatini beklemez: sekiz saniyelik küme yaşamı yaklaşık elli milisaniyeye mal olur.
kampanya · hücre başına 500 tohum

On üç kasıtlı hata ve altyapının onları yakalayıp yakalamadığı

Hiçbir şey yakalamamış bir test altyapısı, sınanmamış bir iddiadır. Aşağıdaki her satır, aynı implementasyonun tek bir kasıtlı protokol hatasıyla derlenmiş halidir — hepsi gerçek bir Raft'ta görülmüş hatalar — dört farklı dünyada koşturulmuştur. En üstteki satır gerçek koddur. Oranlar, hatanın yakalandığı tohumların payıdır.

implementasyonnormalhostileslowchurn ilk tohumyakalayan denetim

* figure8_commit, seçimde no-op girdisi olmadan koşturulur: o no-op hatayı maskeler · üyelik hataları yalnızca churn dünyasında görünür, çünkü yalnızca orada yapılandırma değişir

altyapının bulduğu şeyler

Kendi kodumdaki beş gerçek hata

Bunların hiçbiri planlanmış değildi. Hepsi, doğru olduğuna inandığım implementasyonun binlerce tohumda koşturulmasıyla ortaya çıktı — ve her biri düzeltilip ölçüldü.

bulgubelirtiçözüm ve ölçüm
bulgu 01 · canlılık

Kitaptaki Raft yavaş toparlanıyor — ve ilk ölçümüm bunu abartmıştı

Binlerce tohumda tüm güvenlik değişmezleri tuttu. Sonra canlılık denetimi konuşmaya başladı: koşuların küçük bir kısmında küme, ağ iyileştikten sonra bir daha hiçbir istemci isteğine cevap vermedi. İz, mekanizmayı açıkça gösteriyor — iki düğüm sırayla birbirini tahtından ediyor; her seçim daha yüksek bir terim taşır ve daha yüksek terim, çalışan bir lideri çekilmeye zorlar.

Burada bir kodlama hatası yok; bu, makalede tarif edilen algoritmanın kendisi — ve Raft tezinin pre-vote (§9.6) ile lider yapışkanlığını (§4.2.3) eklemesinin sebebi tam olarak budur. İmplemente edildi ve varsayılmak yerine ölçüldü:

t=6730  node 2: aday    -> LİDER    terim 21
t=6757  node 0: takipçi -> aday     terim 22
t=6770  node 2: lider   -> takipçi  terim 22
t=6907  node 0: aday    -> LİDER    terim 22
t=6937  node 2: takipçi -> aday     terim 23
t=6952  node 0: lider   -> takipçi  terim 23
       ... terimler tırmanır, hiçbir şey commit olmaz

Ama ilk ölçümüm yanlıştı ve bunu da altyapı yakaladı. "Seçtiğim pencerede hiçbir işlem tamamlanmadı", "küme çökmüş" ile aynı şey değil: ağır ayrışmış bir log'u onaran küme, bir seçimden uzun sürebilir. Artık canlılık denetimi kendini doğruluyor: tıkanmış görünen koşu, kuyruğu iki katına çıkarılarak yeniden oynatılıyor ve yalnızca hâlâ sessiz olan küme raporlanıyor. Bu doğrulamayla birlikte pre-vote'lu ya da pre-vote'suz her sürüm eninde sonunda toparlanıyor — hiçbiri kalıcı olarak ölmemişti.

Gerçek etki ikili bir cevap değil, bir dağılım — ve yerini aldığı iddiadan daha değerli:

sürümkoşumedyanp90p99en kötü1 sn üstü

son arıza iyileştikten sonra kümenin bir sonraki isteği cevaplaması için geçen süre · hücre başına 1.000 tohum · hiçbir sürümde "hiç toparlanmadı" vakası yok

Pre-vote, kümeyi zaten yaşanmayacak bir kesintiden kurtarmıyor. Yaptığı şey, toparlanma medyanını birkaç kat düşürmek ve çok saniyelik kuyruğu tamamen ortadan kaldırmak — yani bir kesinti ile bir gecikme arasındaki fark.

bulgu 02 · denetçi

İki hata görünmezdi ve suçlu arıza enjektörü değildi

commit_index_unclamped · 6.000'de 0 → 10'da 9

Sonucu değil, nedeni denetle

Liderin commit indeksini olduğu gibi benimseyen bir takipçinin, er geç geri alınacak bir girdiyi uygulaması gerekirdi. Hiç olmadı: çakışan bir ekleme log'u sonuna kadar buduyor, dolayısıyla ayrışan kuyruk uygulanacak kadar yaşamıyor — doğru bir mekanizma, başka bir mekanizmanın hatasını maskeliyor. Oysa neden tek bir karşılaştırma: commitIndex > lastLogIndex doğru Raft'ta imkânsızdır. Bunu denetlemek, tespiti altı bin koşuda sıfırdan on koşuda dokuza taşıdı.

no_persist_vote · iki lider gerekiyordu → tek çifte oy yeter

Oy, kalıcı bir sözdür

Yeniden başlatmada oyunu unutan bir düğüm aynı terimde iki kez oy verebilir. Ders kitabındaki sonucu beklemek, her iki adayın da çoğunluk toplamasını beklemek demektir — tesadüf üstüne tesadüf. Çifte oyun kendisini izlemek, aynı ölçüde sağlam bir değişmezdir ve hatayı yüzlerce tohum daha erken bulur.

çıkarım

Denetçinin duyarlılığı, enjektörün erişiminden fazlasını kazandırır

Bir hata fuzz kampanyasından sağ çıktığında insanın içinden daha sert arıza enjekte etmek gelir. İlişkili arıza patlamaları ve lider hedefli kaos, buradaki en zor hatada kabaca 4× iyileşme getirdi. İki yeni değişmez — ucuz, sağlam, dörder satır — birkaç büyüklük mertebesi getirdi.

kontrollü deney · §6.4

Bir okumayı cevaplamanın üç yolu, ve saati duran bir makine

Aynı tohumlar, aynı arızalar, aynı dünya — değişen tek şey okuma yolu. Log üzerinden okuma her okumayı bir yazma gibi replike eder. ReadIndex, lider olduğunu tek bir heartbeat turuyla doğrular ve kendi durum makinesinden cevaplar. Lider kirası ise hiçbir şey sormadan, kirasının hâlâ geçerli olduğuna inandığı sürece cevaplar.

Kira, saatlerin sınırlı hata payıyla çalıştığını varsayar. Simülatörün arıza modelinde bir duraklama saati de durdurabilir — askıya alınmış bir sanal makine budur. Uyandığında eski lider, aradan neredeyse hiç zaman geçmediğine inanır ve kirasını hâlâ geçerli sayar:

okuma yolutohumbayat okumap50p99log/koşunot
t=1467  node 0 LİDER olur              terim 2
t=1918  node 0 DURAKLAR — saati de durur
t=2415  node 4 LİDER olur              terim 4
t=2894  node 0 uyanır; kendi saatine göre
       neredeyse hiç zaman geçmemiştir,
       kirası hâlâ geçerli görünür
t=3581  node 0 nihayet tahttan indiğini öğrenir

Bu pencerede lider, artık geçerli olmayan yerel durumundan okuma servis eder. Hiçbir iç Raft değişmezi bunu göremez — log kusursuz biçimde replike edilmiştir. Yalnızca uçtan uca linearizability denetçisi yakalar.

Saat donması kapatıldığında (--freeze 0.0) kira okumaları bütün koşularda temiz geçer. Yani bu, kirayı çürüten bir kanıt değil; kiranın hangi varsayıma dayandığının ve o varsayım düştüğünde ne olduğunun ölçümüdür.

keşif kalitesi

Fuzzer gerçekte neye ulaşıyor?

Bir kampanya, hiç snapshot kurulumu üretmediyse InstallSnapshot'ı test etmemiştir — kaç tohum harcadığı fark etmez. Bu yüzden her koşu, ulaştığı ilginç durumları işaretler:

ulaşılan durumkoşupay

dört dünyada 1.600 koşu · bu tablo bir kez gerçek bir boşluk gösterdi: "seçim sırasında yeniden başlatma" hiç tetiklenmiyordu, çünkü o ölçüm noktası yanlış yere konmuştu

uçtan uca bir hata

Bir tohumdan, insanın okuyabileceği bir şeye

Fuzzer bir tohum bildirir. Küçültücü, yalnızca gerçekten önemli olan arızalar kalana dek arıza silip yeniden oynatır. Sonra koşu bir ızgara olarak çizilir: kim liderdi, kim kesildi, commit nerede durdu.

yükleniyor…

Eski lider izole edilir; node 2 bir sonraki terimi kazanır ve ilk işi kendi no-op girdisini eski liderin zaten uyguladığı indekse yazmaktır. İki durum makinesi, aynı indekste iki farklı komut. Repro, her makinede aynı şekilde oynayan bir JSON dosyasıdır.

kapsam

Kutuda ne var, ne yok

İmplemente edildi

  • Lider seçimi, hızlı geri-sarma ve peer başına uçuş penceresiyle log replikasyonu
  • §5.4.2 commit kuralları, kalıcılık ve çökme kurtarma
  • Pre-vote (§9.6) ve lider yapışkanlığı (§4.2.3)
  • InstallSnapshot ile log sıkıştırma (§7) — anlık görüntü oturumları da taşır
  • Tek sunuculuk üyelik değişiklikleri (§4.1)
  • §6.4'ün üç okuma yolu ve taze liderin okuma bariyeri
  • Yeniden denemeler altında tam-bir-kez istemci oturumları (§6.3)
  • Wing & Gong linearizability araması, anahtar bazlı bölümlemeyle
  • Delta-debugging küçültücü, JSON repro'lar, paralel tohum kampanyaları

İmplemente edilmedi

  • Birleşik konsensüs (joint consensus) ve §4.2.1'deki yakalama aşaması — yeni sunucu eklenirken kısa bir erişilebilirlik düşüşü olabilir
  • Gerçek bir RPC yığını, disk veya işletim sistemi: bu, protokolü test eder
  • Linearizability araması en kötü durumda üsteldir; bütçe dolunca "geçti" demek yerine UNKNOWN bildirir
  • hostile ve slow dünyaları Raft'ın kendi zamanlama varsayımını ihlal edebilir; denetçi bunu kod hakkında değil, o dünya hakkında bir ifade olarak raporlar