in

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.

Genişlet, geçir ve daralt adımlarıyla sıfır kesintili veritabanı şema değişikliği zaman çizelgesi

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_history tablosunda 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'in update modu üretim şemasını yönetemez (validate kalabilir).

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

ALTER TABLE musteri ADD COLUMN ad text; (nullable, varsayılansız)

Değişmez

Anlık; kilit yok

R2 — çift yazım

—

Yeni sütuna da yazar; okurken ad boşsa musteri_adi

Eski sürüm hâlâ eski sütuna yazıyor; sorun yok

R2 — backfill

Java migration: 10.000'lik partilerle UPDATE … WHERE ad IS NULL

Çalışmaya devam eder

Uzun sürebilir; kilit tutmaz

R3 — geçir

NOT NULL (aşağıdaki güvenli yolla)

Yalnızca ad okur ve yazar

Eski sütun artık kullanılmıyor

R4 — daralt

ALTER TABLE musteri DROP COLUMN musteri_adi;

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; volatile varsayılan (örn. now()) yine yeniden yazar → nullable ekle, backfill, sonra varsayılan

İndeks oluşturma

CREATE INDEX yazmaları kilitler

CREATE INDEX CONCURRENTLY; transaction dışında (aşağıda)

NOT NULL ekleme

Tam tablo taraması, kilit

ADD CONSTRAINT c CHECK (ad IS NOT NULL) NOT VALID → VALIDATE CONSTRAINT c → SET NOT NULL (PG 12+ taramayı atlar) → check'i düşür

Yabancı anahtar ekleme

İki tabloyu da kilitler

ADD CONSTRAINT … FOREIGN KEY … NOT VALID, sonra VALIDATE CONSTRAINT

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 SET lock_timeout = '3s'; alınamazsa migration başarısız olur, tekrar denenir

-- 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:

  1. Migration'ı uygulamadan ayrı bir adımda çalıştırın: CI/CD boru hattında flyway migrate ya da Kubernetes'te bir Job/init container; uygulama pod'ları yalnızca flyway validate yapar (spring.flyway.enabled=false + şema uyumu kontrolü).

  2. Migration uygulama dağıtımından önce koşar (genişlet adımları) ya da sonra (daralt adımları) — sıra, desenin gereğidir.

  3. 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çılmaz

Test stratejisi

  • Sıfırdan migration testi: Testcontainers ile boş PostgreSQL'e tüm migration'lar uygulanır; entity'ler validate ile 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

  1. Tek sürümde yeniden adlandırma: Eski pod'lar "column does not exist" ile düşer; dağıtım yarıda kalır.

  2. lock_timeout'suz DDL: Uzun bir rapor sorgusunun arkasında kuyruğa giren ALTER tüm uygulamayı bloklar.

  3. Transaction içinde CONCURRENTLY: PostgreSQL reddeder; script config ile transaction kapatılmalı.

  4. Uygulanmış migration'ı düzenlemek: Checksum hatası; başka ortamlarda farklı şema.

  5. Backfill'i tek UPDATE ile yapmak: Milyonlarca satırda dev transaction, WAL şişmesi, replika gecikmesi.

  6. 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.