Bir ödeme sağlayıcısının API'si bir keresinde seksen milisaniye yerine sekiz saniyede yanıt vermeye başladı. Çökmüş değildi — sadece yavaştı. Dört dakika içinde, iki yıldır tek bir olay yaşamadan çalışan bir ödeme servisi tümüyle karanlığa gömüldü. İstek iş parçacıkları, asla hayır demeyen bir bağımlılığı bekleyerek yığıldı; bu bağımlılık yalnızca yeterince hızlı evet demiyordu.
Çoğu ekibin asla planlamadığı başarısızlık biçimi işte budur: ölü bağımlılık değil, yavaş bağımlılık. Mutlu senaryoya göre tasarlarız, bir yük dengeleyici ekleriz ve buna dayanıklı deriz. Ama dayanıklılık satın alınabilecek bir şey değildir. Bir parçası bozulduğunda — bozulursa değil, bozulduğunda — sisteminizin nasıl davrandığına dair bir dizi karardır.
Başarısızlık istisna değil, varsayılan durumdur
Tek bir makineyi geçtikten sonra, her zaman bir şey kısmen bozuktur. Bir disk doluyor, bir düğüm yeniden başlıyor, bir ağ bağlantısı binde bir paket düşürüyordur. Hesabı yapın: her biri %99,95 erişilebilir olan 30 servise dokunan bir isteğin en iyi durumdaki başarı oranı yaklaşık %98,5'tir — yani her 65 istekten kabaca 1'i en az bir bozulmuş bağımlılığa denk gelir. Günde bir milyon istekte bu, epey sayıda öfkeli kullanıcı demektir.
Önemli olan değişim zihinseldir. "Bunun başarısız olmasını nasıl engellerim?" diye sormayı bırakın ve "bu başarısız olduğunda sonra ne olur?" diye sormaya başlayın. Aşağıdaki her şey bu sorudan doğar.
Değerini kanıtlayan desenler
Bir servis ağına (service mesh) ve bir doktora derecesine ihtiyacınız yok. Dört desen, etki alanının çoğunu kapsar:
- Her ağ çağrısında zaman aşımı. Çoğu HTTP istemcisindeki varsayılan zaman aşımı fiilen sonsuzdur. Tek bir yavaş üst kaynak, iş parçacığı havuzunuzu işte böyle tüketir. Saldırgan, açıkça belirtilmiş zaman aşımları ayarlayın — ve bunları çağıranın zaman aşımından daha kısa tutun ki başarısızlıklar ortada birikeceğine kenarda yüzeye çıksın.
- Geri çekilme (backoff) ve dağınıklık (jitter) ile yeniden denemeler. Saf yeniden denemeler, küçük bir aksaklığı tam bir kesintiye dönüştürür: bir servis toparlandığı an, her istemci ona aynı anda yüklenir. Üstel geri çekilme, denemeleri zamana yayar; rastgele dağınıklık ise onların bir "gürleyen sürü" hâlinde eşzamanlanmasını engeller. Ve idempotent olmayan bir yazma işlemini asla körü körüne yeniden denemeyin.
- Devre kesiciler (circuit breakers). Bir bağımlılık açıkça çökmüşse, onu çağırmayı bırakın. Bir kesici, N başarısızlıktan sonra devreye girer, bir soğuma penceresi boyunca hızlıca başarısız olur, ardından ince bir trafik akışının suları test etmesine izin verir. Bu, hem sizi hem de aksi hâlde toparlanma sürecinde DDoS saldırısına maruz bırakacağınız servisi korur.
- Bölmeler (bulkheads). Kaynakları yalıtın ki boğulan bir bağımlılık gemiyi batıramasın. Her alt servis için ayrı iş parçacığı havuzları veya bağlantı havuzları, takılıp kalan bir öneri çağrısının ödeme yolunu işçilerden yoksun bırakamayacağı anlamına gelir.
Kasıtlı olarak kademe düşürün
Amaç "asla başarısız olmamak" değildir. Amaç "kullanıcının neredeyse hiç fark etmediği bir biçimde başarısız olmaktır." Buna zarif seviye düşürme (graceful degradation) denir ve bu, en az bir mühendislik kararı olduğu kadar bir ürün kararıdır.
Doğruluk kaynağına erişilemediğinde önbellekten biraz eskimiş veri sunun. Boş bir raf yerine genel öneriler gösterin. Kritik olmayan bir yazma işlemini kuyruğa alıp daha sonra onaylayın. Pahalı ve isteğe bağlı işlerin önüne özellik bayrakları (feature flags) koyun ki baskı altında çökmek yerine bazı şeyleri kapatarak yük atabilesiniz. Danışmanlık çalışmalarımda, en hızlı toparlanan ekipler, hangi özelliklerin taşıyıcı, hangilerinin süs olduğuna önceden karar vermiş olanlardır.
Onu kendiniz kırmadan dayanıklılığa sahip olamazsınız
Hiç tetiklenmemiş bir yük devretme (failover) bir güvenlik ağı değil, bir teoridir. Kusursuz belgelenmiş bir veritabanı kopyasının bir yıl boyunca kullanılmadan durduğunu, sonra da tam kendisi için inşa edildiği o tek olayda terfi etmeyi başaramadığını gördüm; çünkü bir yapılandırma sessizce kaymıştı.
Test edilmemiş bir yük devretme, yanına bir YAML dosyası iliştirilmiş bir umuttan başka bir şey değildir.
Bunu pratikle çözün. Oyun günleri (game days) düzenleyin: bir saat ayırın, hazırlık ortamında (veya cesursanız ve hazırlıklıysanız canlı ortamda) bir düğümü öldürün ve gerçekte ne olduğunu izleyin. Yalnızca kesintiler değil, gecikme de enjekte edin — yavaş, ölüden daha zorludur. Beklenen zirvenizin ötesinde yük testi yapın ki uçurumun nerede olduğunu, müşterileriniz sizin yerinize bulmadan önce bilin.
Pazartesi nereden başlamalı
Denizi kaynatmaya çalışmayın. En önemli tek kullanıcı akışınızı seçin — çöktüğünde size para ya da güven kaybettireni. Ardından:
- Bağımlılıklarını haritalayın ve her birine açık bir zaman aşımı koyun.
- Yeniden denemelere dağınıklık içeren geri çekilme (backoff-with-jitter) ekleyin ve en az güvenilir bağımlılığınızın etrafına bir devre kesici koyun.
- O akışın nasıl kademe düşürmesi gerektiğine kâğıt üzerinde karar verin — ve yedek çözümü (fallback) inşa edin.
- İşe yaradığını kanıtlamak için bu ay 60 dakikalık bir oyun günü düzenleyin.
Dayanıklılık, ulaştığınız bir kilometre taşı değildir; sürekli çalıştırdığınız bir kastır. Bu hafta üzerinde düşünmeye değer soru şu: en kritik bağımlılığınız şu anda yavaşlasaydı — yok olmadı, sadece yavaşladı — kullanıcılarınız bunu fark eder miydi bile?