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.
saf Python · sıfır bağımlılık · 56 test · 13 enjekte edilebilir hata · kaynak kod · english
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.
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.
| implementasyon | normal | hostile | slow | churn | ilk tohum | yakalayan 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
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ü.
| bulgu | belirti | çözüm ve ölçüm |
|---|
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üm | koşu | medyan | p90 | p99 | en 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.
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ı.
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.
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.
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 yolu | tohum | bayat okuma | p50 | p99 | log/koşu | not |
|---|
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.
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 durum | koşu | pay |
|---|
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
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.
UNKNOWN bildirir