İçeriğe geç

Kesintisiz Şema Değişikliği — Genişlet, Taşı, Daralt

İleri 9 dk Sık karşılaşılır

Önce şunu oku: MVCC ve Kilitler — Eski Satır Sürümleri Nerede Yaşar?

30 saniyede özet

Yeni sürüm yavaş yavaş yayılırken bir süre eski ve yeni kod aynı tabloya bakar. Bir sütunun adını tek hamlede değiştirmek eski kodu düşürür. Değişikliği küçük, geri alınabilir adımlara bölmek kesintiyi önler.

Bir koridorun adını değiştirmek küçük bir iş gibi görünür. Eski haritayla gelenler için ise değildir.

  1. Bayt: Küçük bir iyileştirme yaptım: name sütununun adı artık full_name.

  2. Sen: Yeni kodu da aynı anda çıkardın mı?

  3. Bayt: Evet! Sürüm yayılırken bir şeyler ters gitti ve geri dönmek de imkânsızdı.

  4. Bayt: Koridorun adını değiştirirken eski haritayla gelenleri ne yapacağız?

Küçük bir iyileştirme: name sütununun adı full_name oldu. Yeni sürüm yayılırken bir şeyler ters gitti; üstelik eskisine dönmek de mümkün değildi.

Eski kod ve yeni tablo bir arada

rolling deployUygulama kopyalarını tek tek yeni sürüme geçiren dağıtım. Bir süre eski ve yeni kod aynı veritabanıyla birlikte çalışır.Sözlükte gör →, pod’ları tek tek yeni sürüme geçirir. Bir süre eski ve yeni kod yan yana çalışır. Migration uygulama açılırken (ör. Flyway ile) çalışıyorsa, şema ilk yeni pod’la birlikte değişir; eski pod’lar hâlâ trafik alırken.

Bu yüzden tek bir kural var: her migration, bir önceki kod sürümüyle de çalışmalıdır. Kolon eklemek bu kurala uyar; kolon silmek ya da adını değiştirmek uymaz.

Kafam karıştı, daha basit anlat

Yeni sürüme geçerken bir süre eski ve yeni kod aynı anda çalışır. Tablo ikisine de uymalıdır, yoksa eski kod birden hata vermeye başlar.

Hızlı kontrolOrta

Rolling deploy yapılan bir uygulamada, migration yazarken uyulması gereken temel kural hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Genişlet, taşı, daralt

Kolon adını tek bir ALTER TABLE … RENAME ile değiştirip yeni kodu aynı deploy'da çıkarıyorsun. Deploy sırasında ne olur? Cevabı göster

Eski pod’lar hata verir. İlk yeni pod migration’ı çalıştırdığı anda, hâlâ name kolonunu arayan eski pod’lar “column does not exist” alır. Yeni sürümden eskisine dönmek de imkânsızlaşır.

Levhayı bir gecede değiştirmek ya da yenisini eskisinin yanına asmak.
Adım adım oku
  1. Sütun tek hamlede yeniden adlandırılırsa, hâlâ çalışan eski pod'lar name sütununu bulamaz ve hata verir.
  2. Genişlet: yeni full_name sütunu eskisinin yanına eklenir; iki levha yan yana asılır.
  3. Taşı: yeni kod iki sütuna birden yazar, eski satırlardaki veri yeni sütuna kopyalanır.
  4. Daralt: bütün pod'lar yeni sürüme geçince eski sütun ayrı bir deploy'da kaldırılır.

genişlet-daraltUyumsuz bir şema değişikliğini, her biri önceki kodla da çalışan adımlara bölmek: yeni yapıyı ekle, veriyi taşı, kodu geçir, eski yapıyı kaldır.Sözlükte gör → değişikliği uyumlu adımlara böler:

  1. Genişlet: yeni kolonu ekle; kod iki kolona yazar, eskisinden okur.
  2. Doldur: eski satırları küçük parçalar hâlinde kopyala (backfillYeni eklenen bir kolonu ya da tabloyu mevcut satırlar için geriye dönük doldurmak. Büyük tablolarda küçük parçalar hâlinde yapılır.Sözlükte gör →).
  3. Okumaları taşı: kod yeni kolondan okur, hâlâ ikisine yazar.
  4. Daralt: eski kolonu okuyan kod kalmayınca yazmayı bırak, sonra kolonu sil.

Bir bankanın eski transfer tablosunda tutar double olarak saklanmış. Onu numeric’e taşımak, bu dört adımı gerçek bir tabloda izlemek için güzel bir fırsat.

Derinleş · Transfer tutarı: double'dan numeric'e dört adımda 4 dosya · ~69 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/resources/db/migration/ V41__expand_transfer_amount.sql Genişlet: yeni numeric kolon eklenir; eski pod'ların kullandığı hiçbir şey değişmez.

src/main/resources/db/migration/V41__expand_transfer_amount.sql
-- Expand. Old pods keep reading and writing "amount"; nothing they use changes.
SET LOCAL lock_timeout = '5s'; -- give up rather than queue every query behind a long transaction
ALTER TABLE transfer ADD COLUMN amount_exact numeric(19,2); -- nullable: no rewrite, brief lock
-- A later release stops writing the old column; it must not be NOT NULL by then.
ALTER TABLE transfer ALTER COLUMN amount DROP NOT NULL;

src/main/java/com/bank/transfer/ Transfer.java Kod iki kolona birden yazar, okumayı şimdilik eski kolondan yapar.

src/main/java/com/bank/transfer/Transfer.java
@Entity
@Table(name = "transfer")
class Transfer {
@Id
private Long id;
private String fromIban;
private String toIban;
private String currency;
@Column(name = "amount") // legacy double precision column, still read by old pods
private Double legacyAmount;
@Column(name = "amount_exact", precision = 19, scale = 2)
private BigDecimal amount;
// Expand phase: write both columns so old and new pods see the same transfer.
void setAmount(BigDecimal value) {
this.amount = value.setScale(2, RoundingMode.UNNECESSARY); // refuses fractions of a kuruş
this.legacyAmount = this.amount.doubleValue();
}
// Reads the old column until the backfill is done; the next release reads amount_exact.
BigDecimal amount() {
return BigDecimal.valueOf(legacyAmount).setScale(2, RoundingMode.HALF_EVEN);
}
}

src/main/java/com/bank/transfer/ TransferAmountBackfill.java Doldur: eski satırlar küçük parçalar hâlinde, her parça kendi kısa transaction'ında kopyalanır.

src/main/java/com/bank/transfer/TransferAmountBackfill.java
@Component
class TransferAmountBackfill {
private static final int BATCH = 5_000;
private final JdbcTemplate jdbc;
TransferAmountBackfill(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
// Runs once every pod writes both columns. No surrounding transaction:
// each UPDATE commits on its own, so row locks are short.
void run() {
long maxId = jdbc.queryForObject("SELECT coalesce(max(id), 0) FROM transfer", Long.class);
for (long from = 0; from < maxId; from += BATCH) {
jdbc.update("""
UPDATE transfer
SET amount_exact = round(amount::numeric, 2)
WHERE id > ? AND id <= ? AND amount_exact IS NULL
""", from, from + BATCH);
}
}
// It could also be one UPDATE over the whole table. On a large table that is one
// long transaction: every touched row stays locked until the end, and all the
// dead row versions pile up at once.
}

src/main/resources/db/migration/ V44__contract_transfer_amount.sql Daralt: eski kolonu kullanan sürüm kalmayınca, ayrı bir deploy'da kolon silinir.

src/main/resources/db/migration/V44__contract_transfer_amount.sql
-- Contract. Ships in a later deploy, once no running version reads or writes "amount".
SET LOCAL lock_timeout = '5s';
ALTER TABLE transfer DROP COLUMN amount; -- catalog-only: a brief exclusive lock, no rewrite
-- Making amount_exact NOT NULL is its own small step on a big table:
-- CHECK (amount_exact IS NOT NULL) NOT VALID, then VALIDATE CONSTRAINT in a separate
-- migration, then SET NOT NULL, which PostgreSQL 12+ proves from that CHECK without a scan.
Kafam karıştı, daha basit anlat

Bir kolonun adını tek hamlede değiştirme. Önce yenisini ekle, veriyi taşı, kod yenisini kullansın. Eskisini en sonda, kimse kullanmadığında sil.

Hızlı kontrolİleri

`price` kolonunun tipi integer kuruştan decimal'e geçecek. Kesintisiz yapmak için doğru sıra hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her şema değişikliğini, bir önceki kod sürümüyle uyumlu olup olmamasına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Geriye uyumlu

Eski kod fark etmeden çalışmaya devam eder.

    Uyumsuz

    Eski kod hata verir ya da yanlış veri yazar.

      Kendin gör

      Trafik varken şema değiştirmek

      Tohum 363703
      • pod-1: v1
      • pod-2: v1
      • pod-3: v1

      Oynat ya da adımla.

      Hız
      Adım 0

      Şu an ne oldu?

      customers.name → customers.full_name

      Üç pod rolling update ile güncellenecek. Deploy sırasında eski ve yeni kod aynı veritabanına bakacak.

      Görevler0/3

      • Deploy sırasında eski pod'ların hata vermesine yol açaçık

        İpucu

        Kolonu tek adımda yeniden adlandır.

      • Bir index yüzünden sipariş yazmalarını durduraçık

        İpucu

        Düz CREATE INDEX.

      • Kolon adını hiçbir isteği düşürmeden ve geri dönüşü açık tutarak değiştiraçık

        İpucu

        Genişlet, doldur, okumaları taşı, daralt.

      Olay günlüğü (0)

      Henüz olay yok. Oynat veya adımla.

      1. Varsayılanla oynat. Tek adımda yeniden adlandırma: eski pod’lar düştü, geri dönüş kapandı.
      2. Yöntemi “kesintisiz” yap. Dört adım, üç deploy, sıfır hata.
      3. Değişikliği index yap, “tek adımda”. Yazmalar durdu, istekler timeout aldı.
      4. “Kesintisiz” seç. Index yazmaları engellemeden oluştu.
      Hızlı kontrolİleri

      Simülatörde tek adımlık yeniden adlandırma sonrası 'geri dönüş kapalı' çıktı. Neden?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tabloyu kilitleyen değişiklikler

      Bazı şema değişiklikleri tabloyu kısa ya da uzun süre kilitler. Büyük tablolarda online karşılıklarını kullan:

      PostgreSQLSQL ServerOracle
      Online indexCREATE INDEX CONCURRENTLYWITH (ONLINE = ON)CREATE INDEX … ONLINE
      NotTransaction bloğu içinde çalışmazSürüme göre değişir (Enterprise, Azure SQL)Enterprise Edition özelliği

      Kısa kilit alan bir ALTER TABLE bile, uzun bir transaction’ın arkasında beklerken arkasındaki bütün sorguları bekletir. PostgreSQL’de migration’dan önce SET lock_timeout ile beklemeye bir üst sınır koy.

      Kafam karıştı, daha basit anlat

      Bazı değişiklikler tabloya “bekleyin” tabelası asar. Büyük bir tabloda bu tabela çok uzun kalabilir, bu yüzden tabelasız yapılan online yöntemleri seç.

      Hızlı kontrolİleri

      PostgreSQL'de milisaniyede bitmesi gereken bir `ALTER TABLE orders ADD COLUMN note text` çalıştırıldı ve bütün sipariş sorguları dakikalarca bekledi. Neden?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Bu Flyway migration'ı PostgreSQL'de hata veriyor. Hatalı satır hangisi?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Hatalı satıra dokun, sonra kontrol et.

      V12__orders_customer_index.sql
      SQLUTF-8LF

      Tuzaklar

      Tek dev UPDATE ile doldurmak. Milyonlarca satırı tek transaction’da güncellemek uzun kilitler, şişme ve dev bir geri alma demektir. Parçalara böl.

      CONCURRENTLY transaction içinde. Flyway’de o migration’ı transaction dışında çalışacak şekilde ayarla; başarısız olursa geride kalan INVALID index’i temizle.

      Daraltmayı unutmak. Yazılmayan ama silinmeyen kolonlar birikir ve bir gün biri onları yeniden kullanır.

      Geri alınamaz migration’ı geri dönüşle birlikte düşünmemek. Kolon silindikten sonra eski koda dönmek yalnızca veriyi geri yükleyerek mümkündür.

      Hızlı kontrolİleri

      Yeni full_name kolonunu 50 milyon satır için doldurmak gerekiyor. Hangisi en güvenli yaklaşım?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Kendini sına

      Şimşek turu1/5

      Rolling deploy sırasında eski ve yeni kod bir süre aynı veritabanını kullanır.

      Soru 1/2İleri

      Beş pod aynı anda açılıyor ve her biri başlangıçta Flyway çalıştırıyor. Aynı migration beş kez mi uygulanır?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 Yeni sürüm yayılırken eski ve yeni kod aynı veritabanına bakar. Her şema değişikliği bir önceki kod sürümüyle de çalışabilmelidir.
      2. 2 Ad ve tip değişikliği tek adımda yapılmaz: genişlet, doldur, okumaları taşı, daralt. Her adımda geri dönüş açık kalır.
      3. 3 Büyük tablolarda kilit alan değişiklikleri online çalıştır (CONCURRENTLY, ONLINE = ON) ve kilit beklemesine bir üst sınır koy.
      Sonraki kapı Bağlantı havuzunu büyüttün ve sistem daha da yavaşladı. Nasıl olur? Bağlantı Havuzu — Büyük Havuz Hızlı Havuz Değildir · 8 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.