<

Thundering Herd: Cache TTL'lerinin Senkron Ölümü

Grafiği ilk gören veritabanı yöneticimiz oldu ve ekran görüntüsünü "bu ne böyle, tarak mı?" mesajıyla attı. MySQL CPU grafiği gerçekten tarağa benziyordu: her saat başı, saniyesi saniyesine, 90 saniye süren yüzde 95'lik bir diş. Aradaki 58.5 dakika sakin. Uygulamada saat başı koşan hiçbir zamanlanmış iş yoktu. Trafik saat başlarında artmıyordu. Ama veritabanı, sanki kurmalı bir saat gibi, her tepe noktasında aynı nöbeti geçiriyordu.

Cevap, o sabah yaptığımız deploy'un masum görünen bir yan etkisindeydi. Deploy tüm uygulama süreçlerini 10:00'da yeniden başlatmıştı. Önbellek boştu; ilk istekler bütün cache anahtarlarını aynı birkaç dakika içinde doldurdu. Ve her anahtarın TTL'i aynıydı: 3600 saniye. Yani 10:00 civarında doğan binlerce anahtar, 11:00 civarında hep birlikte öldü. Hepsi aynı anda diriltilmek istedi, hepsi veritabanına koştu. Diriltilen anahtarlar yine aynı TTL'i aldı ve ölüm randevusu 12:00'ye kurulmuş oldu. Sürü, kendi senkronunu sonsuza dek taşıyordu.

Sürünün İçindeki Daha Sinsi Sürü

Yakından bakınca problemin iki katmanlı olduğunu gördük. Birinci katman az önceki: binlerce farklı anahtarın eş zamanlı ölümü. İkinci katman tek anahtarın içinde yaşanıyordu ve daha yıkıcıydı. En popüler anahtarımız — bütün araç listesi ekranının beslendiği bir sorgunun sonucu — saniyede yaklaşık 400 kez okunuyordu. O anahtar öldüğü anda, ilk isteğin sorguyu çalıştırıp önbelleği doldurması 1.2 saniye sürüyordu. O 1.2 saniyede gelen 400 küsur istek, önbellekte hiçlik bulup her biri kendi başına aynı ağır sorguyu veritabanına gönderiyordu. Yani tek bir anahtarın ölümü, aynı sorgunun 400 kopyasının aynı anda koşması demekti. Literatürde cache stampede denen şey tam olarak bu ve TTL senkronundan bağımsız olarak, yeterince popüler her anahtarın kaderi.

İlk İlaç: Randevuları Dağıtmak

Birinci katmanın ilacı utanç verici derecede basit: TTL'e rastgelelik katmak. 3600 sabitini bıraktık; her yazımda TTL, 3600 ± yüzde 15 aralığından rastgele seçiliyor. Böylece aynı dakikada doğan anahtarlar 54 ile 69 dakika arasına yayılarak ölüyor ve toplu ölüm randevusu kendiliğinden dağılıyor. Deploy sonrası ilk saat başında tarak dişi yüzde 95'ten 40'a indi. Tek satırlık değişiklik başına düşen kazanç sıralamasında bu, kariyerimde üst sıralarda.

Teşhis sürecinin kendisi de anlatmaya değer, çünkü ilk bakışta kimse önbelleği suçlamadı. Saat başı deseni önce zamanlanmış iş arattı; yoktu. Sonra müşterilerden birinin entegrasyonunu suçladık; değildi. Kilit ipucu, veritabanı sorgu loglarında geldi: diş ânında koşan sorgular çeşit olarak sıradan, adet olarak anormaldi ve neredeyse hepsi önbellekten servis edilmesi gereken sorgulardı. O an soru değişti: "kim bu sorguları koşturuyor" değil, "bu sorgular neden önbellekte değil". Doğru soruyu bulmak, cevabı bulmaktan uzun sürdü; genellikle de öyle oluyor.

İkinci İlaç: Mutfağa Tek Aşçı

Popüler anahtar problemi için single-flight kilidi kurduk. Bir istek anahtarı boş bulduğunda, doğrudan sorguya koşmak yerine önce Redis'te o anahtara özel kısa ömürlü bir kilit almaya çalışıyor (SET NX, 5 saniye ömürlü). Kilidi alan tek istek sorguyu çalıştırıp önbelleği dolduruyor; alamayanlar ise iki seçenekten birine düşüyor. Elimizde anahtarın bayat bir kopyası varsa onu servis ediyoruz — araç listesinin 30 saniye eski hâli, 1.2 saniyelik bir bekleyişten ve çökmüş bir veritabanından her zaman iyidir. Bayat kopya da yoksa kısa aralıklarla önbelleği yokluyorlar. Bunun için silmede anahtarı gerçekten silmek yerine "süresi geçmiş" işaretiyle bir süre daha tutuyoruz; stale-while-revalidate deseninin el yapımı hâli.

Single-flight kilidinin de kendi tuzağı var, düşmeden söyleyeyim: kilidi alan süreç sorguyu bitiremeden ölürse ne olur? Kiralama süresi burada hayati — kilit 5 saniye sonra kendiliğinden düşüyor ve sıradaki istek nöbeti devralıyor. İlk taslakta kilidi süresiz almıştık; kod incelemesinde bir arkadaşın "bu süreç tam burada ölürse herkes sonsuza dek bayat veri mi yer?" sorusu bizi kurtardı. Dağıtık kilitlerde sahiplik her zaman kiralıktır; tapulu kilit, ölen sahibinin ardından sistemi rehin alır.

İki ilaç birlikte tarağı grafikten sildi. Saat başlarındaki p99 gecikmesi 4.2 saniyeden 210 milisaniyeye indi; veritabanının saat başı CPU dişi yüzde 95'ten 12'ye düştü.

Popüler anahtarlar için bir üçüncü tekniği de not edeyim: olasılıksal erken yenileme. Fikir şu — anahtarın ölmesini beklemek yerine, TTL'in sonuna yaklaşıldıkça küçük ama artan bir olasılıkla, isteklerden biri önbelleği daha süresi dolmadan arka planda tazeler. Böylece popüler anahtar teorik olarak hiç ölmez; yenilenme, son kullanıcı isteklerinin gölgesinde olur. Biz bunu yalnızca en üst 20 anahtarda kullanıyoruz çünkü kilit mekanizmasından daha incelikli ve yanlış ayarlanırsa gereksiz yenileme trafiği üretiyor. Sıcaklığı orta seviyedeki binlerce anahtar için single-flight kilidi hem yeterli hem de akıl yürütmesi kolay; her tekniği her anahtara uygulamak yerine anahtarları sıcaklığına göre katmanlamak, bakım yükünü ciddi azaltıyor.

Deploy'un Kendisi de Bir Stampede

Geriye bir senaryo kalıyordu: deploy anının kendisi. Ne kadar jitter eklersek ekleyelim, süreçler yeniden başladığında önbellek boş ve ilk dakikalar savunmasız. Bunun için iki küçük önlem aldık. En pahalı 20 anahtar için deploy script'inin sonunda bir ısıtma adımı koştuk: uygulama trafiğe açılmadan bu anahtarlar arka planda dolduruluyor. Ve süreçleri aynı anda değil, ikişer dakika arayla, dalga dalga yeniden başlatıyoruz; eski süreçlerin dolu önbelleği, yenilerin boşluğunu örtüyor. Deploy sonrası ilk 10 dakikanın hata oranı bu iki önlemle sıfıra yaklaştı.

Tarağı sildikten sonra aynı hastalığın başka semptomlarını aramaya başladık ve iki tane daha bulduk. Birincisi mobil taraftaydı: sunucu kısa bir kesinti yaşadığında binlerce mobil istemci bağlantıyı aynı anda kaybediyor ve hepsi aynı sabit aralıkla — 30 saniyede bir — yeniden deniyordu. Kesintiden dönüşün ilk saniyeleri, senkronlaşmış bir istek dalgasıyla karşılanıyordu; sunucu ayağa kalkar kalkmaz yeniden diz çöküyordu. Yeniden deneme aralığına üstel geri çekilme ve rastgele sapma ekledik; dalga düzleşti. İkincisi gece 00:00'daydı: farklı ekiplerin farklı zamanlarda yazdığı beş ayrı gece işi, kimse koordine etmediği hâlde hepsi yuvarlak saati seçtiği için üst üste biniyordu. Cron satırlarındaki sıfırları rastgele dakikalarla değiştirmek, gece yarısı disk grafiğindeki tepeyi ikiye böldü. Senkron, davet edilmeden gelen bir misafir; sabit bir sayı yazdığınız her yerden içeri sızıyor.

Bu vakanın bana öğrettiği genel ilke şu: bir sistemde birbirinden bağımsız görünen binlerce aktöre aynı sabiti verdiğinizde, onları farkında olmadan bir orkestraya çevirirsiniz ve orkestra en kötü parçayı hep birlikte çalar. TTL'ler, yeniden deneme süreleri, polling aralıkları, cron dakikaları — hepsi aynı hastalığa açık ve hepsinin aşısı aynı: rastgelelik. Grafiklerinizde açıklayamadığınız periyodik dişler varsa bir yazın; tarak desenini bir kez tanıyınca her yerde görmeye başlıyorsunuz.

📅 Yayınlanma:  ·  Yakup Zengin