Bir salı öğleden sonrası, bir bölgede iki saatlik bir baz istasyonu kesintisi yaşandı. Yaklaşık 3 bin araç takip cihazı çevrimdışı kaldı. Cihazlar akıllıydı: bağlantı yokken konumları hafızalarına biriktirdiler. Saat 16:20'de şebeke döndü ve 3 bin cihaz, ikişer saatlik birikmiş veriyi aynı anda boşaltmaya başladı. RabbitMQ kuyruğumuz 20 dakikada 6.2 milyon mesaja şişti, broker bellek alarmına girdi ve yayıncı bağlantılarını duraklattı. Asıl felaket buydu: kesintiden hiç etkilenmemiş 27 bin cihazın canlı verisi de artık kuyruğa giremiyordu. Bir bölgenin gecikmiş verisi, bütün ülkenin canlı haritasını dondurdu.
O gün backpressure kavramını kitaptan değil, meydan savaşından öğrendik.
İlk Refleks: Daha Çok Tüketici
Panik ânının klasik hamlesi: consumer sayısını artırmak. 4 tüketiciyi 16'ya çıkardık. Kuyruk erime hızı neredeyse değişmedi. Çünkü tüketiciler zaten dar boğaz değildi; her mesaj tek tek MySQL'e INSERT ediliyordu ve veritabanı diski saniyede yaklaşık 3.500 yazmada doyuyordu. 16 tüketici aynı dar kapıya daha kalabalık bir kuyruk oluşturdu, satır kilitleri yüzünden toplam verim yüzde 8 arttı, o kadar. Backpressure problemlerinde ilk soru "kaç işçim var" değil, "zincirin en yavaş halkası hangi hızda akıtıyor" olmalı. Bizim zincirde en yavaş halka diskti ve işçi eklemek diski hızlandırmıyordu.
Gerçek Çözümün Birinci Ayağı: Toplu Yazım
Tüketiciyi tek mesaj işleyen bir döngüden, 500 mesaj biriktirip tek multi-row INSERT atan bir yapıya çevirdik; prefetch değerini de bu tampona uyacak şekilde 1000'e ayarladık. Ack'ler batch commit'ten sonra topluca gitti. Veritabanına giden ifade sayısı 500'de 1'e indi ve aynı disk, saniyede 3.500 satır yerine 41 bin satır alır oldu. Boşaltma testinde 6 milyon mesajlık kuyruk 5.5 saat yerine 22 dakikada eridi. Yıllardır bilinen bir teknik; acısı, bunu felaket günü değil tasarım günü hatırlamak gerektiği.
Toplu yazıma geçerken bir tuzağa dikkat etmek gerekiyor; biz de içine bir kez düştük. 500'lük batch'in içinden tek bir bozuk mesaj — hatalı koordinat, parse edilemeyen paket — bütün batch'in INSERT'ini düşürüyordu ve tüketici aynı batch'i sonsuz döngüde yeniden deniyordu. Kuyruk erimek yerine tek bir zehirli mesajın etrafında dönüyordu. Çözüm iki katmanlı: batch düşerse ikiye bölüp yeniden dene (binary split), tek mesaja kadar inildiğinde hâlâ hata varsa o mesajı dead-letter kuyruğuna at ve yola devam et. Dead-letter kuyruğunu da haftalık gözden geçiriyoruz; oraya düşenler çoğu zaman yeni bir cihaz firmware'inin protokolü kırdığının ilk habercisi.
İkinci Ayak: Her Mesaj Eşit Doğmaz
Toplu yazım debiyi çözdü ama adalet sorununu çözmedi: geçmişe ait 6 milyon paket sırada beklerken canlı paketler onların arkasına diziliyordu. Oysa iki verinin aciliyeti taban tabana zıt. Canlı haritadaki müşteri aracını şimdi görmek ister; iki saat önceki güzergâh ise raporda lazım olur. Trafiği ikiye ayırdık: cihaz protokolündeki zaman damgasına bakıp 5 dakikadan eski paketleri backfill kuyruğuna, tazeleri live kuyruğuna yönlendirdik. Live kuyruğunun tüketicileri her zaman öncelikli; backfill tüketicileri veritabanı yazma gecikmesi bir eşiği aşınca kendilerini otomatik yavaşlatıyor. Böylece sistemin cevabı "herkes eşit boğulsun" olmaktan çıkıp "canlı akış her koşulda nefes alsın" hâline geldi.
Bu ayrımın görünür bir faydası daha oldu: kuyruk metrikleri nihayet anlam kazandı. Eskiden tek kuyruğun uzunluğu, canlı akışla geçmiş verinin toplamı olduğu için hiçbir soruya net cevap vermiyordu. Şimdi live kuyruğunun yaşı — en eski mesajın kaç saniyedir beklediği — tek başına sistemin sağlık göstergesi: 5 saniyeyi aşarsa bir şeyler yanlış demektir ve alarm eşiği bu kadar net olunca nöbetçinin hayatı kolaylaşıyor. Backfill kuyruğunun uzunluğu ise alarm değil, kapasite planlama verisi. Aynı sayının iki ayrı kuyrukta iki ayrı anlama kavuşması, tasarımın doğru yerinden ayrıldığının işaretiydi.
Üçüncü Ayak: Boğulacaksan Nerede Boğulacağını Seç
Backpressure tasarımının özü şu soruya dürüst cevap vermek: sistem kapasitesinin üzerinde veri geldiğinde fazlalık nerede bekleyecek, kim yavaşlayacak, ne feda edilecek? Cevapsız bırakırsanız bu kararı sizin yerinize en kötü yer verir; bizde broker'ın bellek alarmı vermişti. Biz kararları açıkça yazdık. Kuyruklar lazy moda alındı ki milyonlarca mesaj RAM yerine diskte beklesin. Kuyruk uzunluklarına sınır ve taşma politikası tanımlandı. Canlı harita beslemesi için ayrıca bir gerçek zamanlı katman ayrıldı: her aracın yalnızca en güncel konumu anahtar bazlı ezilerek tutuluyor; harita 30 saniyelik aralıksız her noktaya değil son duruma bakar, ham kayıt tarafı ise her noktayı diskte saklamaya devam eder.
Kapasite hesabını da nihayet kabaca yazılı hâle getirdik. Sistemin sindirme hızı saniyede 41 bin satırsa ve olası en kötü kesinti senaryosu 30 bin cihazın 4 saatlik birikimi ise, boşaltma süresi aşağı yukarı bellidir ve bu sürenin kabul edilebilir olup olmadığı bir mühendislik değil, işletme kararıdır. Bu hesabı müşteri tarafına da taşıdık: büyük kesintilerden sonra "geçmiş veriler en geç şu kadar dakikada tamamlanır" diyebilmek, destek hattının yükünü gözle görülür azalttı. Belirsizlik, gecikmeden daha çok şikâyet üretiyor.
Bir de üretici tarafına dokunduk. Cihazların firmware'inde birikmiş veri gönderimi, bağlantı kurulur kurulmaz tam gaz başlıyordu. Yeni konfigürasyonda backfill gönderimi paketler arasına küçük gecikmeler koyuyor ve sunucudan aldığı yanıt yavaşlarsa temposunu düşürüyor. Akış kontrolünün en ucuz yeri kaynaktır; veri merkezinize girmiş 6 milyon mesajı yönetmek, hiç bu hızda girmemesini sağlamaktan her zaman pahalıdır.
Olaydan altı hafta sonra aynı senaryoyu bilerek yeniden yaşattık; buna içeride yangın tatbikatı diyoruz. Staging ortamında 3 bin sanal cihazı iki saat susturup aynı anda saldık. İlk tatbikatta yeni mimarinin bir açığı çıktı: backfill tüketicileri kendilerini yavaşlatıyordu ama canlı kuyruğun tüketicileriyle aynı veritabanı bağlantı havuzunu paylaştıkları için, yavaşlamış backfill işlemleri havuzdaki bağlantıları işgal edip canlı tarafı dolaylı yoldan boğuyordu. Havuzları ayırdık. Bu tür bağımlılıklar — ortak bağlantı havuzu, ortak thread havuzu, ortak disk — kâğıt üzerindeki izolasyonu sessizce deler ve ancak yük altında görünür. Tatbikatsız backpressure tasarımı, hiç prova edilmemiş tiyatro gibidir; prömiyerde öğrenirsiniz.
Alarm Eşiği Değil, Eğim
Son ders izlemeyle ilgili. Eski alarmımız "kuyrukta 100 binden çok mesaj varsa öt" diyordu; felaket günü öttüğünde iş işten geçmişti. Yeni alarm mutlak sayıya değil dengeye bakıyor: beş dakikalık pencerede üretim hızı tüketim hızını aşıyorsa ve fark büyüyorsa, kuyruk daha 50 bindeyken haber veriyor. Kuyruk uzunluğu bir fotoğraf; üretim-tüketim farkı ise filmin gidişatı. Boğulmakta olan bir sistemi fotoğraftan değil filmden tanırsınız. Kendi kuyruk mimarinizde "taşınca ne olacak" sorusunun cevabı yazılı değilse, iletişim sayfasından yazın; o cevabı sakin bir günde birlikte yazmak, alarmlı bir gecede yazmaktan çok daha ucuz.