Kasım 2020'de bir müşteri, 250 araçlık filosunun tüm yılına ait sefer raporunu istedi. Rapor motoru işe koyuldu ve dört dakika boyunca, aynı monolitin içinde yaşadığı için, canlı harita API'siyle aynı PHP-FPM worker havuzunu kemirdi. O dört dakikada 5.000 aracın canlı takibi p99'da 8 saniyeye çıktı. Bir kişinin yıllık raporu, herkesin canlı ekranını rehin almıştı. Bu olay ilk değildi; ama "rapor motorunu bu gövdeden ayırıyoruz" kararının imzalandığı olay bu oldu.
Yazının başlığındaki "ameliyat" kelimesi süs değil. Beş yıllık, 400 bin satırlık, her köşesi birbirine değen bir PHP monolitinden parça koparmak cerrahi bir iş ve biz bu ameliyatı hasta uyanıkken, sistem günde 40 milyon istek işlerken yaptık.
Neden Rapor Motoru? Kesik Çizgileri Aramak
Monolit parçalama hevesine kapılan herkese ilk sorum şudur: neden o parça? Bizim gerekçelerimiz üç maddeydi ve üçü de ideolojik değil, ölçülebilirdi. Kaynak profili uyumsuzdu: API istekleri 15 ms'lik, bellek-hafif işlerken raporlar dakikalar süren, 500 MB'a şişen canavarlardı; aynı FPM havuzunda bu ikisi, ambulansla çöp kamyonunu aynı şeride sokmaktı. Ölçekleme ihtiyacı farklıydı: rapor yükü ay sonlarında 10 kat artıyor, API yükü sabit seyrediyordu. Ve değişim hızı farklıydı: rapor şablonları haftada birkaç kez değişiyor, her seferinde koca monolit deploy ediliyordu — o dönem her deploy'un FPM reload'ı, preload yüzünden zorunluydu üstelik.
Bir gerekçe daha vardı ki itiraf etmesi zor: rapor kodu, monolitin en bakımsız mahallesiydi. Ayırma kararı, o mahalleyi yeniden yazma fırsatıydı. Ama burada kendimize bir kural koyduk: taşıma ve yeniden yazma aynı anda yapılmaz. Önce davranışı birebir koruyarak taşı, sonra yerinde iyileştir. İkisini birleştiren herkes, "yeni sistem eskisiyle aynı sonucu vermiyor" çıkmazında aylarca debelenir — bunu 2018'de başka bir modülde yaşamıştık, tekrarı yoktu.
Strangler İnceliği: Kapıyı Değil, Trafiği Taşımak
Kopuş planı strangler yaklaşımıydı: eski kapı yerinde durur, yeni servis arkada büyür, trafik dilim dilim el değiştirir. İlk somut adım, monolitin içine bir cephe (facade) çekmek oldu: rapor üretimine giden tüm çağrıları — 14 farklı controller'dan, iki cron'dan ve bir e-posta zamanlayıcısından geliyorlardı — tek bir ReportGateway sınıfının arkasında topladık. Bu, hiçbir davranış değiştirmeyen, üç haftalık sıkıcı bir refactor'dı ve ameliyatın en kritik adımıydı: kesik çizgisi artık kodda fiziksel olarak vardı.
Sonra gateway'e bir bayrak öğrettik: rapor tipine ve müşteri ID'sine göre isteği ya eski iç koda ya da HTTP üzerinden yeni servise yönlendiriyor. Yeni servis — PHP 7.4, kendi FPM havuzu, kendi makinesi; işlem modeli senkron değil, kuyruklu: istek bir job üretir, worker üretir, sonuç nesne deposuna yazılır, kullanıcıya "raporunuz hazır" bildirimi düşer. İlk taşınan rapor, en basit olanıydı (günlük kilometre özeti) ve iki hafta boyunca gölge modda çalıştı: her istek iki sistemde de üretildi, çıktılar otomatik karşılaştırıldı. 40 bin karşılaştırmada 6 uyuşmazlık çıktı; altısı da eski koddaki bir yuvarlama tutarsızlığıydı. Kararı müşteriler değil, bu diff raporu verdi.
Veritabanı: Ayrılığın En Zor Maddesi
Servis ayrımının ders kitabı hali "her servisin kendi veritabanı" der. Biz demedik. Rapor motoru, konum verisinin sahibi olamazdı — o veri telemetri hattının malı — ve 1,9 milyar satırlık tabloyu kopyalamak da çözüm değildi. Pragmatik çizgimiz şu oldu: yeni servis ana veritabanına yalnızca okuma replikası üzerinden ve yalnızca kendine tahsis edilmiş kullanıcıyla bağlanır; o kullanıcının grant'i beş tabloyla sınırlıdır. Yazdığı her şey (rapor meta verisi, iş kuyruğu durumu, hazır dosyalar) kendi ayrı şemasındadır. Buna "dağıtık monolit" diyen çıkacaktır; kısmen haklıdır da. Ama sınırı grant'lerle çizmek, sınırı hiç çizmemekten fersah fersah iyidir ve ileride gerçek bir API sınırına evrilmenin yolunu kapatmaz. Nitekim bir yıl içinde o beş tablonun ikisi, telemetri servisinin sunduğu bir iç API'yle değiştirildi. Replika kullanımı bir yan faydayı bedavaya getirdi: en ağır rapor sorgusu bile artık yazma hattına kilit koyamıyor, koysa koysa replikasyon gecikmesini büyütüyor — o da rapor ekranındaki tazelik ibaresine yansıyıp kendini ele veriyor.
Kopuşun Bilançosu ve Yara İzleri
Aralık ortasında, yani karardan altı hafta sonra, rapor trafiğinin yüzde 80'i yeni servisteydi. Ölçümler: API p99'u ay sonu rapor sağanağında bile 180 ms'nin altında sabitlendi (eskiden 8 saniyeyi görüyordu); rapor deployment'ları API'ye dokunmadan günde birkaç kez yapılır oldu; rapor makinesi ay sonunda dikey büyütülüp ay başında küçültülüyor. Ve beklemediğimiz kazanç: kuyruklu modele geçince raporların yüzde 60'ının gece saatlerine kendiliğinden kayması — kullanıcı "hazır olunca haber ver" düğmesini sevdi, sistem de gece boş kapasiteyi.
Geri dönüş planını da anlatayım, çünkü cesaretimizin asıl kaynağı oydu. Gateway'deki yönlendirme bayrağı müşteri bazında ve anlık değiştirilebilir durumdaydı; yeni serviste ilk hafta çıkan bir bellek sızıntısında (PDF kütüphanesinin büyük tablolarda sayfa başına 30 MB tutması) tek satır konfigürasyonla ilgili rapor tipini eski yola çevirdik, sızıntıyı sakin kafayla düzeltip iki gün sonra geri açtık. Kullanıcılar hiçbir şey fark etmedi. Geri dönüş düğmesi olmayan geçiş, geçiş değil kumardır.
Yara izleri de kayıtlara geçsin. Bir: dağıtık sistem vergisini ödedik — eski dünyada stack trace tek parçaydı, şimdi bir raporun hikâyesi iki servisin loglarına bölünüyor; korelasyon ID'sini ilk günden koymadığımız için iki haftalık log arkeolojisi yaşadık. İki: zaman dilimi hatası — yeni servisin makinesi UTC, monolit Europe/Istanbul kuruluydu; gölge mod bunu yakaladı ama yakalamasaydı gece yarısı sınırındaki tüm günlük raporlar üç saat kaymış olacaktı. Üç: cephe sınıfına topladığımız 14 çağrı noktasından biri, bir müşterinin özel entegrasyonu için yazılmış unutulmuş bir kod yoluydu ve onu ancak müşteri arayınca bulduk. Envanterin yüzde 93'ü, yüzde 100'ü değildir.
Ekip tarafında da beklenmedik bir etki oldu. Rapor servisi ayrı bir depo, ayrı bir deploy hattı olunca iki kişilik bir "rapor ekibi" fiilen kendiliğinden oluştu; kod sahipliği netleşti, review kuyrukları kısaldı. Conway yasasını tersinden işletmiş olduk: mimariyi böldük, organizasyon kendini ona göre hizaladı. Küçük şirketlerde bunun kıymeti az anlatılır — 400 bin satırın tamamından "herkes sorumluyken" aslında kimse sorumlu değildi.
Mikroservis furyasının tepe yaptığı bir dönemde yazıyorum bunu, o yüzden açık konuşayım: monoliti parçalamak başlı başına bir erdem değil. Bizim monolitimizin geri kalanı yerinde duruyor ve gayet iyi hizmet veriyor; sıradaki kopuş adayı (bildirim altyapısı) da ancak kendi ölçülebilir gerekçelerini biriktirdiğinde ameliyata girecek. Parça koparmanın bedeli her seferinde ağ, izleme, deployment ve zihin yükü olarak ödeniyor; bedeli karşılayan tek şey, uyumsuz kaynak profilleri ve farklı değişim hızları gibi somut basınçlar. Kendi monolitinizde ilk kesiği nereden atacağınızı tartışmak isterseniz buradan ulaşın; neşteri nereye vurmayacağınız, çoğu zaman daha önemli bir karar.