Flyway Migration Rehberi: Sıfır Kesintili Şema Değişikliği
Dağıtımın en riskli parçası kod değil, şemadır: 'ALTER TABLE tabloyu sekiz dakika kilitledi' cümlesini duymamak için şema değişikliği hem eski hem yeni uygulama sürümüyle çalışmalıdır. Flyway ile sürümlü migration'ları, genişlet-geçir-daralt desenini, PostgreSQL'de kilit almayan DDL alternatiflerini, toplu backfill'i ve çok replikalı dağıtımda güvenli çalıştırmayı anlatıyoruz.
Dağıtımın en riskli parçası kod değil, şemadır. Kod geri alınabilir; 40 milyon satırlık tabloya yanlış eklenen bir sütun ya da sekiz dakika kilit tutan bir ALTER TABLE geri alınamaz, yalnızca yaşanır. Üstelik modern dağıtımlar kademelidir: birkaç dakika boyunca eski ve yeni uygulama sürümü aynı anda çalışır ve aynı veritabanına bakar. Şema değişikliği ikisiyle de uyumlu değilse "sıfır kesinti" yalnızca bir slogandır.
Bu yazıda Flyway ile sürümlü migration disiplinini, genişlet-geçir-daralt (expand/migrate/contract) desenini, PostgreSQL'de kilit almayan DDL alternatiflerini, toplu backfill'i, çok replikalı dağıtımda migration'ı güvenle çalıştırmayı, test stratejisini ve sahada gördüğümüz hataları anlatıyoruz.
Temel disiplin
Şema, kod gibi sürümlenir: Her değişiklik depoda bir migration dosyasıdır (
V12__musteri_ad_ekle.sql); elle çalıştırılan SQL yoktur. Flyway hangi sürümün uygulandığınıflyway_schema_historytablosunda tutar.Uygulanan migration değişmez: Checksum farkı üretimi durdurur; hata varsa yeni bir migration ile düzeltilir.
Yalnızca ileri: "Down" migration yazılmaz; geri alma, şemayla uyumlu bir önceki uygulama sürümüne dönmektir. Şemanın önceki sürümle uyumlu tasarlanması bunu mümkün kılar.
Küçük ve tek amaçlı: Bir migration bir değişiklik; büyük dosyalar yarıda kalınca durumu belirsizleştirir.
ddl-autoüretimde kapalı: Hibernate'inupdatemodu üretim şemasını yönetemez (validatekalabilir).
Genişlet – geçir – daralt
Sıfır kesintinin tek kuralı: her migration, hem şu an çalışan hem de dağıtılmakta olan uygulama sürümüyle uyumlu olmalıdır. Bu, her yıkıcı değişikliğin birkaç sürüme yayılması demektir. Örnek: musteri.musteri_adi sütununu ad olarak yeniden adlandırmak.
Sürüm | Şema (migration) | Uygulama | Not |
|---|---|---|---|
R1 — genişlet |
| Değişmez | Anlık; kilit yok |
R2 — çift yazım | — | Yeni sütuna da yazar; okurken | Eski sürüm hâlâ eski sütuna yazıyor; sorun yok |
R2 — backfill | Java migration: 10.000'lik partilerle | Çalışmaya devam eder | Uzun sürebilir; kilit tutmaz |
R3 — geçir |
| Yalnızca | Eski sütun artık kullanılmıyor |
R4 — daralt |
| Değişmez | En az bir sürüm sonra; geri alma penceresi kapandıktan sonra |
Aynı desen sütun tipini değiştirmek (yeni sütun + backfill + geçiş), tabloyu bölmek ve ilişki değiştirmek için de geçerlidir. Yavaş görünür; ama her adım tek başına güvenli ve geri alınabilirdir.
Tehlikeli DDL ve güvenli karşılıkları (PostgreSQL)
İşlem | Risk | Güvenli yol |
|---|---|---|
Varsayılan değerli sütun ekleme | PG 11 öncesi tüm tabloyu yeniden yazar | PG 11+'da sabit varsayılan anlıktır; |
İndeks oluşturma |
|
|
NOT NULL ekleme | Tam tablo taraması, kilit |
|
Yabancı anahtar ekleme | İki tabloyu da kilitler |
|
Sütun yeniden adlandırma / tip değiştirme | Eski sürüm kırılır | Genişlet-geçir-daralt |
Sütun/tablo silme | Eski sürüm hâlâ kullanıyor olabilir | Önce kod bırakır, bir sürüm sonra şema siler |
Uzun kilit bekleme | Kilit kuyruğu tüm sorguları bloklar | Her migration'ın başında |
-- V12__musteri_ad_ekle.sql (R1: genişlet)
SET lock_timeout = '3s';
SET statement_timeout = '30s';
ALTER TABLE musteri ADD COLUMN ad text;-- V14__musteri_ad_indeks.sql (transaction dışında çalışmalı)
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_musteri_ad ON musteri (ad);
-- Aynı adlı V14__musteri_ad_indeks.sql.conf dosyasında:
-- executeInTransaction=false// V13__MusteriAdBackfill.java (R2: partili backfill; kilit tutmaz, kaldığı yerden devam eder)
public class V13__MusteriAdBackfill extends BaseJavaMigration {
@Override
public void migrate(Context ctx) throws Exception {
var jdbc = new JdbcTemplate(new SingleConnectionDataSource(ctx.getConnection(), true));
int guncellenen;
do {
guncellenen = jdbc.update("""
UPDATE musteri SET ad = musteri_adi
WHERE id IN (SELECT id FROM musteri WHERE ad IS NULL ORDER BY id LIMIT 10000)
""");
Thread.sleep(50); // replikasyon ve diğer sorgulara nefes payı
} while (guncellenen > 0);
}
}Çok replikalı dağıtımda migration'ı kim çalıştırır?
Spring Boot varsayılanı, uygulama açılırken Flyway'in çalışmasıdır. Tek örnekte sorunsuz; üç replika aynı anda açıldığında Flyway'in kilit mekanizması (flyway_schema_history üzerinde kilit; spring.flyway.lock-retry-count) yarışı önler, ama uzun bir backfill üç pod'un startup probe'unu tüketir ve Kubernetes hepsini yeniden başlatır. Daha sağlam düzen:
Migration'ı uygulamadan ayrı bir adımda çalıştırın: CI/CD boru hattında
flyway migrateya da Kubernetes'te bir Job/init container; uygulama pod'ları yalnızcaflyway validateyapar (spring.flyway.enabled=false+ şema uyumu kontrolü).Migration uygulama dağıtımından önce koşar (genişlet adımları) ya da sonra (daralt adımları) — sıra, desenin gereğidir.
Migration'ı çalıştıran veritabanı kullanıcısı DDL yetkilidir; uygulamanın kullanıcısı yalnızca DML — en az yetki ilkesi ve yanlışlıkla şema değişikliğine karşı koruma.
# application.yaml (uygulama pod'u)
spring:
flyway:
enabled: false # migration ayrı Job'da; uygulama şemayı değiştirmez
jpa:
hibernate:
ddl-auto: validate # entity ↔ şema uyumsuzluğunda açılmazTest stratejisi
Sıfırdan migration testi: Testcontainers ile boş PostgreSQL'e tüm migration'lar uygulanır; entity'ler
validateile eşleşir. Her CI koşusunda.Önceki sürümden migration testi: Üretimin son sürümü şemasından (dump) yeni migration'lar uygulanır; gerçek veri şekliyle backfill sınanır.
Uyumluluk testi: Yeni şema üzerinde önceki uygulama sürümünün entegrasyon testleri koşar — kademeli dağıtımın simülasyonu.
Süre provası: Büyük tablolarda migration üretim boyutundaki bir kopyada zamanlanır; "3 saniye" ile "40 dakika" dağıtım planını değiştirir.
Sahada sık yapılan hatalar
Tek sürümde yeniden adlandırma: Eski pod'lar "column does not exist" ile düşer; dağıtım yarıda kalır.
lock_timeout'suz DDL: Uzun bir rapor sorgusunun arkasında kuyruğa giren ALTER tüm uygulamayı bloklar.Transaction içinde CONCURRENTLY: PostgreSQL reddeder; script config ile transaction kapatılmalı.
Uygulanmış migration'ı düzenlemek: Checksum hatası; başka ortamlarda farklı şema.
Backfill'i tek UPDATE ile yapmak: Milyonlarca satırda dev transaction, WAL şişmesi, replika gecikmesi.
Geri alma = yedekten dönüş sanmak: Yedekten dönüş veri kaybıdır; geri alma, uyumlu şema üzerinde eski uygulama sürümüne dönmektir.
Sık sorulan sorular
Flyway mı Liquibase mi?
İkisi de aynı disiplini uygular. Flyway düz SQL ve sadelik; Liquibase XML/YAML changelog, veritabanından bağımsız soyutlama ve daha gelişmiş geri alma araçları. Tek veritabanı motoruyla çalışan Spring Boot ekiplerinde Flyway daha az sürtünme yaratır; birden çok motor destekleyen ürünlerde Liquibase'in soyutlaması değerlidir.
Kiracı başına şema kullanan SaaS'ta migration nasıl çalışır?
Şema listesi üzerinde döngüyle (Flyway.configure().schemas(kiraci)) ve her şemada ayrı geçmiş tablosu; dağıtım tüm şemalar aynı sürüme gelene kadar "bitmiş" sayılmaz. Ayrıntı için multi-tenant mimari yazımıza bakın.
MySQL'de aynı kurallar geçerli mi?
Desen aynıdır; araçlar farklı. MySQL 8'de birçok DDL çevrimiçidir (ALGORITHM=INSTANT/INPLACE), ama tip değişiklikleri ve bazı indeksler tabloyu yeniden yazar; büyük tablolarda gh-ost ya da pt-online-schema-change gibi araçlar kullanılır.
Sonuç
Sıfır kesintili şema değişikliği bir araç değil, bir disiplindir: sürümlü ve değişmez migration'lar, her adımı hem eski hem yeni uygulamayla uyumlu tutan genişlet-geçir-daralt deseni, kilit almayan DDL, partili backfill, uygulamadan ayrı çalıştırılan migration ve önceki sürümden test. Bu disiplinle 40 milyon satırlık tablo da gündüz vakti değiştirilir.
Veritabanı dağıtım sürecinizi sıfır kesintiye taşımak ya da riskli bir şema değişikliğini planlamak için bakım ve destek ile kurumsal uygulama geliştirme hizmetlerimize göz atabilirsiniz.