Sertaç Yıldırım saha notları

Ana sayfa → Teknik

Şema Değişikliği: Cuma Gecesi Değil, İki Aşamada

Cuma 23:00. Migration başladı, “beş dakika sürer” demiştik. 23:40’ta hâlâ devam ediyordu ve o 40 dakika boyunca sipariş tablosuna kimse yazamadı. Sebebi basit: test veritabanında 80 bin satır vardı, canlıda 47 milyon.

Özet
  • Şema değişikliği bir an değil, bir penceredir. Deploy süresince kodun iki sürümü aynı anda canlıdır; şema ikisini de aynı anda mutlu etmeli.
  • Beş adım, beş ayrı sürüm. Ekle → çift yaz → doldur → okumayı çevir → sil. Her adım tek başına geri alınabilir.
  • RENAME diye bir işlem yok. İki değişikliğin kılık değiştirmiş hâli: ekle ve sil. Aradaki her şey senin sorunun.
  • Backfill tek UPDATE değildir. Parti parti, kesilebilir, replica gecikmesi izlenerek.
  • Asıl soru “eski kolonu ne zaman sileceğiz”. Çoğu ekipte cevap “hiç”; bunu da tarih yazarak çözdük.
  • Gece yapılan migration, gözden geçirilmemiş migration demektir. Gündüz yapılamayan değişiklik, henüz güvenli değil demektir.

Sahadan: 80 bin satır ile 47 milyon satır

Migration’ı yazan arkadaş her şeyi doğru yapmıştı. Değişiklik gözden geçirildi, test ortamında çalıştı, süre ölçüldü: 4,2 saniye. Cuma gecesi seçildi ki trafik düşük olsun. Kitapta yazan her şey yapılmıştı.

Kaçırdığımız tek şey şuydu: test veritabanı canlının küçültülmüş bir kopyası değildi, başka bir veritabanıydı. 80 bin satırda tablo yeniden yazmak saniyeler sürer, 47 milyon satırda dakikalar. Ölçtüğümüz sayı doğruydu ama ölçtüğümüz şey yanlıştı.

İkinci hata benim. “Cuma gecesi yapalım” dediğimde kendimi güvende hissettim; oysa o cümle riski azaltmıyor, sadece tanığı azaltıyor. Gece yapılan bir değişiklik daha az kişinin izlediği, daha yorgun kafayla düzeltilen bir değişikliktir. Riski düşürmek istiyorsan işlemi küçültmen gerekir, saati değil.

Gece yapılmak zorunda olan bir değişiklik, henüz güvenli hâle getirilmemiş bir değişikliktir. Saat değiştirmek, adımı küçültmenin yerine geçmez.

Neden tek hamlede olmuyor

Sebep veritabanında değil, deploy’da. Sürümü canlıya çıkarırken eski pod’lar bir anda kaybolmuyor; yeni sürüm ayağa kalkarken eski sürüm hâlâ istek karşılıyor. O pencere birkaç saniye de olabilir, kademeli çıkışta saatler de (sürüm yönetimi yazısında anlattığım düzende günler).

Yani şu an canlıda iki farklı kod sürümü aynı tabloya yazıyor. Şemayı tek hamlede yeni sürüme göre değiştirirsen, eski sürüm o an hata almaya başlıyor. Kural buradan çıkıyor: her şema değişikliği, hem eski hem yeni kodla çalışmak zorunda.

Expand / contract: beş adım

“Telefon numarasını tek alandan ülke kodu + numara olarak ayıralım” gibi basit bir istek, sahada şöyle görünüyor:

Bes adim, bes ayri surum
1) EXPAND   : yeni kolonu ekle, NULL kabul etsin
              ALTER TABLE musteri ADD COLUMN ulke_kodu varchar(5) NULL;
              kod degismedi. geri alma: kolonu birak, zararsiz.

2) CIFT YAZ : kod yeni kayitta HER IKI kolona da yazar
              okuma hala eski kolondan. bu surum tek basina cikar.
              geri alma: onceki surume don, yeni kolon bos kalir.

3) BACKFILL : eski satirlari parti parti doldur (asagida)
              kod degismedi. istedigin zaman durdur, devam et.

4) OKUMAYI CEVIR : kod yeni kolondan okumaya baslar
              hala her ikisine yaziyor. bu en kritik surum:
              geri alma tek adimda mumkun cunku eski kolon guncel.

5) CONTRACT : once cift yazmayi kaldir (bir surum),
              sonra eski kolonu sil (ayri bir surum).
              ALTER TABLE musteri DROP COLUMN telefon_eski;
              bu adim GERI ALINAMAZ. en az bir hafta beklenir.

Beş adım fazla gibi görünüyor ama her biri sıradan bir sürüm; toplam iş, tek hamlelik migration’dan uzun sürmüyor. Fark şurada: hiçbir adımda kilit yok, hiçbir adımda “geri dönüş yok” anı yok. 4. adıma kadar her şey tek komutla geri alınıyor.

Dördüncü adımın neden kritik olduğunu kaçırmayalım: okumayı çevirdiğinde hâlâ çift yazıyorsun. Bu yüzden bir sorun çıkarsa okumayı eskiye geri almak yeterli — eski kolon güncel kalmaya devam ettiği için veri kaybı yok. Çift yazmayı okumayla aynı sürümde kaldıran ekipler, geri dönüşü olmayan bir kapıdan geçiyor.

Backfill: tek UPDATE değil

Üçüncü adımın klasik hatası şu tek satır:

Boyle yapma / boyle yap
# BOYLE YAPMA - 47 milyon satiri tek islemde kilitler
UPDATE musteri SET ulke_kodu = substring(telefon_eski, 1, 3);

# BOYLE YAP - parti parti, kesilebilir, izlenebilir
son_id = 0
while True:
    parti = "UPDATE musteri SET ulke_kodu = substring(telefon_eski,1,3)
             WHERE id > {son_id} AND ulke_kodu IS NULL
             ORDER BY id LIMIT 5000
             RETURNING id"
    satirlar = calistir(parti)
    if not satirlar: break
    son_id = max(satirlar)          # nerede kaldigini KAYDET
    bekle(200)                      # replica'ya nefes aldir
    if replica_gecikmesi() > 5:     # saniye
        bekle(5000)                 # yavasla, durma

Üç ayrıntı önemli: nerede kaldığını kaydet (backfill yarıda kesilir, devam edebilmeli), replica gecikmesini izle (backfill’in en sık yan etkisi okuma replica’larının geride kalması ve bunun kullanıcıya “kaydım görünmüyor” diye yansıması), partiler arasında bekle (kesintisiz yazmak diğer sorguları aç bırakır).

Hangi işlem güvenli, hangisi değil

İşlemDurumNe yapmalı
Kolon ekle (NULL kabul eden)GüvenliDoğrudan yapılabilir
Kolon ekle (NOT NULL + varsayılan)Motora bağlıEski sürümlerde tablo yeniden yazılır; önce NULL ekle, doldur, sonra kısıtı koy
Kolon adını değiştirTehlikeliYapma. Ekle + sil olarak beş adıma böl
Kolon tipini değiştirTehlikeliYeni kolon aç, çift yaz, doldur, çevir, sil
Kolon silGeri alınamazSadece contract adımında, en az bir hafta sonra
Index ekleKilitleyebilirEşzamanlı (concurrently) seçeneğiyle, işlem dışında
NOT NULL kısıtı ekleTabloyu tararÖnce doğrulanabilir kısıt, sonra doğrula, sonra sıkılaştır

Tablonun asıl mesajı şu: tek satırlık komutların çoğu tek adım değil. RENAME bir kelime ama iki değişiklik. NOT NULL iki kelime ama 47 milyon satırlık bir tarama.

Kolon adı değiştirmek diye bir işlem yoktur. Ekleme ve silme vardır, arada da bir pencere — ve o pencere senin sorumluluğundadır.

Eski kolonu ne zaman sileceğiz?

Bu sorunun ekiplerdeki gerçek cevabı “hiç”. Beşinci adım hiç gelmiyor, çünkü kimse acil değil. Bizde aynısı oldu: bir yıl sonra bakıldığında üç tabloda “eski” ekli kolonlar duruyordu; ikisine hâlâ çift yazılıyordu ve kimse neden olduğunu bilmiyordu.

Çözüm, feature flag’lerde işe yarayan kuralın aynısı oldu: karar, işi başlatırken verilir. Migration dosyasının başına bir satır koyduk ve CI bu tarihi izliyor:

Migration basligi
# migration: 2026_04_12_ulke_kodu_ekle
# expand-contract: EVET
# eski kolon    : musteri.telefon_eski
# silme tarihi  : 2026-05-20      <-- CI bu tarihten sonra uyarir
# sahip         : tek isim

Tarih geçince CI uyarı üretiyor, iki hafta sonra derlemeyi kırıyor. Sert ama tek işleyen yöntem bu; uyarıyla yetindiğimiz dönemde hiçbir kolon silinmedi.

Ne izlemeli?

NeNeden
En uzun kilit süresi (migration başına)Ortalama yalan söyler; tek bir 40 dakika bütün ayı bozar
Backfill sırasında replica gecikmesiKullanıcıya “kaydım yok” diye yansıyan tek şey
Tamamlanmamış expand/contract sayısıYarım kalmış her geçiş, çift yazma borcudur
Gece yapılan migration oranıYüksekse süreç değil, cesaret kullanıyorsun

Bende işe yaramayanlar

  • Test veritabanını büyütmek. “Canlı kadar veri koyalım” dedik; üç hafta sonra kimse o ortamı güncel tutmuyordu. İşe yarayan şey, migration’ı canlının bir kopyasında (restore edilmiş yedek) çalıştırmak ve süreyi oradan ölçmek oldu.
  • Migration’ı uygulama deploy’undan ayırmamak. Aynı pipeline’da çalıştırdığımız sürece, migration yavaşlayınca deploy da takılıyordu ve geri alma karmaşıklaşıyordu. Ayırdık: şema değişikliği kendi başına çıkar, uygulama ayrı.
  • “Bakım penceresi” ilan etmek. İlk refleks buydu. Ayda bir yarım saat kesinti planlamak, expand/contract öğrenmemek için ödediğimiz bedeldi — ve o pencere hep dolu geçti, hiçbir zaman yetmedi.

Kontrol listesi

Migration çıkmadan önce
  • Bu değişiklik kodun eski sürümüyle de çalışıyor mu?
  • Süreyi canlı boyutundaki bir kopyada mı ölçtüm, test ortamında mı?
  • Kilit alıyor mu? Alıyorsa kaç saniye, hangi tablo?
  • Adımlardan hangisi geri alınamaz — ve o adım ayrı bir sürümde mi?
  • Backfill parti parti mi, kesilirse kaldığı yerden devam eder mi?
  • Replica gecikmesini kim izliyor, eşiği ne?
  • Eski kolonun silme tarihi yazılı mı?
  • Bunu gündüz yapabiliyor muyum? Yapamıyorsam neden?

Sonuç

O cuma gecesi 40 dakika kaybettik ve ertesi hafta aynı değişikliği yeniden yaptık — bu sefer beş adımda, gündüz, kimse uyanık beklemeden. Toplam süre daha uzundu: dört gün. Kesinti süresi ise sıfırdı.

Şema değişikliğini zorlaştıran şey veritabanı değil, eski ve yeni kodun bir süre birlikte yaşamak zorunda olması. Bunu kabul ettiğin anda çözüm kendiliğinden geliyor: değişikliği, her adımı tek başına doğru olan küçük parçalara böl.

Test şu: bu migration’ı Salı öğlen, trafik en yüksekken çalıştırabilir miyim? Cevap hayırsa gece yapmak onu güvenli hâle getirmiyor — sadece kimsenin izlemediği bir saate taşıyor.