Ekibimde herkesin bildiği bir travma tarihi var (ben o gün henüz burada değildim ama hikâye kurumsal hafızaya kazınmış): iki yıl önce bir salı öğleni, sipariş tablosuna masum görünen bir ALTER TABLE çalıştırıldı. Metadata lock zinciri kurudu, yazma işlemleri dört dakika boyunca kuyrukta bekledi ve o dört dakikada platformda tek bir sipariş oluşmadı. Dört dakika, yoğun saatte binlerce sipariş demek. O günden beri "küçük bir kolon ekleyeceğiz" cümlesi bizde refleks olarak alarm zilleri çaldırır. Geçen ay aynı tabloya yine dokunmamız gerekti: orders artık 1,4 milyar satır ve 780 GB; iptal akışının yeniden tasarımı için yeni bir kolon ve üstüne bir index gerekiyordu. Bu kez operasyon 31 saat sürdü ve kimse fark etmedi. Fark edilmemesi, ayları bulan bir disiplinin sonucuydu.
INSTANT Yazısına Fazla Güvenmek
İlk yanlış hipotez ekip içinden geldi: "MySQL 8 var, ALGORITHM=INSTANT deriz, saniyeler sürer." Kağıt üstünde doğru; kolon ekleme çoğu durumda instant DDL kapsamında. Ama bizim değişiklik paketi yalnızca kolon değildi: kolonun üstüne secondary index ve bir kolonda tip genişletmesi vardı; ikisi de instant kapsamı dışında, tablo kopyalı rebuild gerektiriyor. Üstelik instant kolon eklemenin de kendi sınırları var: row format kısıtları, sürüm eşiği ve tabloya instant eklenmiş kolon sayısının limiti. Staging'de birebir kopya üzerinde denedik; INSTANT reddedildi, INPLACE ise online görünmesine rağmen replikaya tek dev transaction olarak akıp saatlerce replication lag üreteceği için elendi. Replika lag'i bizim için soyut bir metrik değil: raporlama, arama beslemesi ve bazı okumalar replikalardan döner. Ders bir: online DDL kararı dokümandan değil, prod kopyası üzerinde yapılmış provadan çıkar.
Prova dediğim şey de öylesine bir deneme değil. Gecelik snapshot'tan production ile aynı disk ve instance tipinde bir kopya ayağa kaldırıyoruz; üzerine binlog replay ile gerçek yazma trafiğinin kaydedilmiş bir kesitini bindiriyoruz ki tablo migration sırasında sessiz durmasın. Bu düzenekte üç şeyi ölçtük: chunk boyutuna göre kopyalama hızı, yazma trafiği altında binlog uygulama gecikmesi ve tahmini toplam süre. İlk denemede 1.000 satırlık chunk ile başladık; IO'yu rahat bırakıyordu ama toplam süre 50 saati aşıyordu. 2.500'e çıkınca replika lag'i tehlikeli titredi. Karar 1.500'de kaldı ve production'daki gerçek süre, provanın tahmininden yalnızca %8 saptı. O %8'lik isabet, gece 04:00'te "acaba" diye ter dökmemekle aynı şey.
Binlog'u Dinleyen Hayalet
Tercihimiz gh-ost oldu. pt-online-schema-change ile aramızdaki tarihsel mesele trigger'lar: pt-osc orijinal tabloya üç trigger takar ve her yazma işlemine senkron maliyet bindirir; yoğun yazma alan bir sipariş tablosunda bu maliyet en kötü anda, en yüksek trafikte ödenir. gh-ost ise değişiklikleri binlog'dan, yani asenkron olarak okur; tablonun kendisine hiçbir şey takmaz. Yük kontrolü de sürücü koltuğundadır: biz max-lag-millis'i 1.500'e çektik, ayrıca kendi işaret tablomuz üzerinden bir throttle bayrağı tanımladık. Akşam 18:00-23:00 arasındaki sipariş tepesinde ve öğlen yemek saatinde kopyalama otomatik duraksadı; gece boyunca chunk chunk ilerledi. 1,4 milyar satırın gölge tabloya kopyalanması, bu duraklamalarla birlikte 29 saat sürdü; binlog'dan gelen canlı değişiklikler eş zamanlı olarak gölgeye uygulandı.
Bir gotcha'yı ucuz atlattık: disk. Gölge tablo, orijinalin tam bir kopyası demek; 780 GB'lık tablo için operasyon boyunca en az bir o kadar boş alan lazım, binlog büyümesi de cabası. Volume %58 doluydu; operasyondan önce eski partition'ları arşivleyip %41'e çektik. Bunu yapmasak 25. saatte disk alarmıyla her şeyi geri sarmak zorunda kalırdık ve gh-ost'ta geri sarmak kolay olsa da 25 saat çöpe giderdi.
gh-ost'un çalışma düzeninde iki ayrıntı daha operasyonu şekillendirdi. Birincisi binlog formatı: satır bazlı (ROW) binlog şart ve biz zaten replikasyon için bu moddaydık; değilseniz bunun kendisi ayrı bir geçiş projesidir, migration sabahı keşfedilecek bir şey değil. İkincisi, gh-ost'u binlog'u master yerine bir replikadan okuyacak şekilde bağladık; master'ın üstündeki tek yük chunk kopyalama yazmaları oldu, değişiklik akışını dinleme maliyeti replikaya taşındı. İzleme tarafında da migration'a özel bir Grafana panosu açtık: kopyalanan satır sayısı, tahmini kalan süre, binlog gecikmesi, throttle durumu. 31 saat boyunca bu panoya bakan herkes operasyonun nabzını tuttu; "ne durumda?" sorusunu Slack'te bir kez bile sormadık.
Kesişme Anı
En gergin bölüm cut-over: gölge tablonun orijinalin adını devraldığı atomik rename. gh-ost'un güzelliği bu anı sizin seçmeniz: postpone bayrağıyla operasyonu hazır bekletip, düşük trafikli pencerede tetikledik. Gece 04:10'da cut-over 2,3 saniyelik bir yazma duraklamasıyla tamamlandı; uygulama tarafında bu, birkaç yüz isteğin retry ile telafi ettiği kısa bir gecikme tepesi olarak göründü. Cut-over öncesi bir güvenlik adımı daha vardı: gölge tablo ile orijinal arasında örneklemeli satır karşılaştırması yapan bir doğrulama script'i, rastgele seçilmiş 100 bin satırı kolon kolon eşleştirdi ve tek bir fark bulamadı; bulaydı cut-over o gece iptal olacaktı. Grafana'daki sipariş oluşturma grafiğinde o ana zoom yapmazsanız hiçbir şey göremezsiniz. İki yıl önceki dört dakikalık düz çizginin yanında, bu 2,3 saniye bize zafer gibi geldi.
Şema Değişikliği Bir Deployment'tır
Asıl dönüşüm araçta değil, süreçte oldu. Şema değişikliklerini artık kod gibi ele alıyoruz: her değişiklik bir migration PR'ı, üzerinde en az iki review, staging kopyasında zorunlu prova ve tahmini süre/disk raporu. Uygulama tarafında expand-contract deseni şart: önce yeni kolon eklenir ve kod her iki şemayla da çalışacak hale getirilir, çift yazım açılır, tarihsel veri Kafka üzerinden batch backfill ile doldurulur, okumalar yeni kolona alınır ve ancak haftalar sonra eski kolon ayrı bir operasyonla emekli edilir. Rollback planı "ALTER'ı geri al" değildir; kod her iki şemayla çalıştığı için rollback çoğu zaman sadece feature flag kapatmaktır.
Backfill bahsi ayrıca anlatmaya değer, çünkü insanların "kolonu ekledik, bitti" sandığı yerde işin yarısı başlıyor. Yeni kolonu tarihsel veriyle doldurmak için tek bir dev UPDATE çalıştırmak, ALTER kadar tehlikeli bir kilit ve replikasyon fırtınası demek. Biz backfill'i Kafka üzerinden yürüttük: bir producer job eski kayıtların anahtarlarını 5.000'lik partiler halinde bir topic'e yazdı, consumer'lar her partiyi kısa transaction'larla güncelledi ve replika lag'i 1 saniyeyi aşınca kendi kendine yavaşladı. 1,4 milyar satırın backfill'i bu tempoda dört güne yayıldı; acele etmemek burada bir tasarım tercihiydi. Dört gün boyunca kimsenin fark etmediği bir işlem, dört dakikada herkesin fark ettiği bir işlemden her zaman daha hızlıdır.
Bir de ölçtüğümüz şeyi söyleyeyim: bu operasyonun tamamında sipariş oluşturma başarı oranı %99,99'un altına inmedi, replika gecikmesi 1,5 saniyeyi aşmadı, müşteri tarafına yansıyan tek bir incident kaydı açılmadı. 31 saatlik bir açık kalp ameliyatı için fena bir skor değil. Büyük tablonuza dokunmanız gerekiyor ve içinizde iki yıl önceki bizim salı öğleni korkusu varsa, buradan yazın; prova checklist'imizi paylaşmaktan memnuniyet duyarım. Korkunun ecele faydası yok ama provası çok.