Expand/contract deseniyle kesintisiz şema değişikliklerini öğrenin: sütun ekleyin, kademeli backfill yapın, uyumlu kod deploy edin ve eski yolları güvenle kaldırın.

Veritabanı değişikliklerinden kaynaklanan kesinti her zaman temiz, açık bir erişim kaybı şeklinde olmaz. Kullanıcılar için bir sayfanın sonsuza kadar yüklenmesi, bir ödemenin başarısız olması veya uygulamanın aniden “bir şeyler yanlış gitti” göstermesi şeklinde görünebilir. Ekipler için ise uyarılar, artan hata oranları ve temizlenmesi gereken başarısız yazma kuyruğu olarak kendini gösterir.
Şema değişiklikleri risklidir çünkü veritabanı uygulamanızın çalışan her sürümü tarafından paylaşılır. Bir sürümdeki release sırasında genellikle eski ve yeni kod aynı anda çalışır (rolling deploy’lar, birden fazla örnek, arka plan işler). Görünüşte doğru olan bir migration yine de bu sürümlerin birini kırabilir.
Yaygın hata durumları şunlardır:
Kod doğru olsa bile sürümlerin zamanlaması ve uyumluluğu yüzünden yayınlar tıkanır.
Kesintisiz şema değişikliklerinin özünde bir kural var: her ara durum hem eski hem yeni kod için güvenli olmalı. Veritabanını, mevcut okumaları ve yazmaları bozmadan değiştirirsiniz; hem eski hem yeni yapılarına dayanabilecek kod gönderirsiniz; ve eski yolun hiç kimse tarafından kullanılmadığından emin olana kadar onu kaldırmazsınız.
Gerçek trafiğiniz, katı SLA’larınız veya çok sayıda uygulama örneğiniz ve worker’ınız varsa bu ekstra çaba kesinlikle değer. Çok az trafiğe sahip küçük bir iç araçsa planlı bakım penceresi daha basit olabilir.
Veritabanı işleri kaynaklı çoğu olay, uygulamanın veritabanının anında değişeceğini varsayıp işlerken veritabanının değişimin zaman aldığını fark etmemesinden doğar. Expand/contract deseni bunu, tek riskli değişikliği daha küçük, güvenli adımlara bölerek önler.
Kısa bir süre için sisteminiz iki “diyalektiği” aynı anda destekler. Önce yeni yapıyı tanıtırsınız, eski olanı çalışır tutarsınız, veriyi kademeli taşır, sonra temizlersiniz.
Desen basittir:
Bu yaklaşım rolling deploy’larla iyi çalışır. 10 sunucuyu teker teker güncelliyorsanız, kısa süreliğine eski ve yeni sürümler birlikte çalışır. Expand/contract, bu örtüşme sırasında veritabanının her iki sürüme de uyumlu kalmasını sağlar.
Ayrıca rollback’leri daha az korkutucu kılar. Yeni sürümde bir hata varsa uygulamayı geri alabilirsiniz; çünkü expand penceresi sırasında eski yapılar hâlâ mevcut olduğu için veritabanını geri almanıza gerek kalmaz.
Örnek: PostgreSQL’de full_name sütununu first_name ve last_name olarak bölmek istiyorsunuz. Yeni sütunları eklersiniz (expand), hem eski hem yeni şekilleri okuyup yazabilen kod gönderirsiniz, eski satırları backfill edersiniz, sonra full_name hiçbir yerde kullanılmadığından emin olunca silersiniz (contract).
Expand aşaması yeni seçenekler eklemekle ilgilidir; eski olanı kaldırmakla değil.
Yaygın ilk adım yeni bir sütun eklemektir. PostgreSQL’de genelde en güvenli yol sütunu nullable ve varsayılan değersiz eklemektir. Varsayılanlı ve NOT NULL bir sütun eklemek tablo yeniden yazımı veya daha ağır kilitler tetikleyebilir; Postgres versiyonunuza ve yaptığınız değişikliğe bağlıdır. Daha güvenli sıra genelde: önce nullable ekle, toleranslı kod deploy et, backfill yap, sonra NOT NULL uygulamaya koy.
İndeksler de dikkat ister. Normal indeks oluşturma beklediğinizden daha uzun süre yazmaları engelleyebilir. Mümkünse concurrent olarak indeks oluşturun; daha uzun sürer ama release’i durduracak kilitleri önler.
Expand ayrıca yeni tablolar eklemeyi de kapsayabilir. Tek sütundan many-to-many ilişkiye geçiyorsanız, yeni bir join tablosu ekleyip eski sütunu yerinde tutabilirsiniz. Eski yol çalışırken yeni yapı veri toplamaya başlar.
Pratikte expand genelde şunları içerir:
Expand sonrası, eski ve yeni uygulama sürümleri aynı anda sürpriz olmadan çalışabilmelidir.
Çoğu sürüm sıkıntısı ortada ortaya çıkar: bazı sunucular yeni kodu çalıştırırken diğerleri hâlâ eski kodu çalıştırıyor ve veritabanı zaten değişiyordur. Hedefiniz basit: rollout sırasında hangi sürüm olursa olsun hem eski hem genişletilmiş şema ile çalışabilmeli.
Yaygın yaklaşım dual-write’tir. Yeni bir sütun eklediğinizde yeni uygulama hem eskiye hem yenisine yazar. Eski uygulama sürümleri yalnızca eskiye yazmayı sürdürür; çünkü o sütun hâlâ vardır. Yeni sütunu başta opsiyonel tutun ve tüm yazarlar yükselene kadar katı kısıtlamaları erteleyin.
Okumalar genelde yazmalardan daha dikkatli değiştirilir. Bir süre eski sütunda (tam dolu olduğundan emin olduğunuz) okumaları tutun. Backfill ve doğrulamadan sonra okumaları yeni sütunu tercih edecek şekilde değiştirin; yeni eksikse eskiye geri dönün.
Ayrıca veritabanı değişirken API çıktınızı sabit tutun. İçsel bir alan ekleseniz bile tüketiciler hazır olana kadar (web, mobil, entegrasyonlar) yanıt biçimlerini değiştirmekten kaçının.
Geri alınması kolay bir rollout genelde şöyle görünür:
Ana fikir: ilk geri döndürülemez adım eski yapıyı düşürmektir; bunu sona bırakın.
Backfill, birçok “kesintisiz şema değişikliği”nin ters gittiği yerdir. Yeni sütunu mevcut satırlar için doldurmak istiyorsunuz; uzun kilitler, yavaş sorgular veya ani yük zirveleri olmadan.
Batching önemlidir. Hızlı biten (saniyeler, dakikalar değil) batchler hedefleyin. Her batch küçükse işi duraklatıp devam ettirebilir, ayarlama yapabilir ve release’leri engellemezsiniz.
İlerlemeyi takip etmek için stabil bir cursor kullanın. PostgreSQL’de bu genelde primary key’tir. Satırları sıra ile işleyin ve tamamladığınız son id’yi depolayın ya da id aralıkları ile çalışın. Bu, job yeniden başladığında pahalı full-table scan’leri önler.
Basit bir desen:
UPDATE my_table
SET new_col = ...
WHERE new_col IS NULL
AND id > $last_id
ORDER BY id
LIMIT 1000;
Update’i koşullu yapın (örneğin, WHERE new_col IS NULL) böylece job idempotent olur. Tekrar çalıştırmalar sadece hâlâ işi gereken satırları etkiler ve gereksiz yazmaları azaltır.
Backfill sırasında yeni verilerin gelmesini planlayın. Genel sıralama şöyledir:
İyi bir backfill sıkıcıdır: dengeli, ölçülebilir ve veritabanı ısındığında kolayca duraklatılabilir.
En riskli an yeni sütunu ekmek değil; ona güvenebileceğinize karar verdiğiniz andır.
Contract’a geçmeden önce iki şeyi kanıtlayın: yeni veri eksiksiz ve üretimde güvenle okunuyor.
Hızlı ve tekrarlanabilir tamamlanma kontrolleriyle başlayın:
Dual-write yapıyorsanız sessiz hataları yakalamak için bir tutarlılık kontrolü ekleyin. Örneğin, old_value <> new_value dönen satır sayısını saatlik kontrol edip sıfır değilse alarm kurun. Bu genelde hangi yazıcının hâlâ sadece eski sütunu güncellediğini çabucak gösterir.
Migrasyon çalışırken temel üretim sinyallerini izleyin. Sorgu süresi veya kilit beklemeleri yükselirse, güvenli olduğunu düşündüğünüz doğrulama sorguları bile yük ekliyor olabilir. Yeni sütunu okuyan kod yolları için deploy’lardan hemen sonra hata oranlarını izleyin.
Her iki yolu ne kadar tutmalısınız? En az bir tam release döngüsünü ve bir backfill yeniden çalıştırmasını atlatacak kadar. Birçok ekip 1–2 hafta veya hiçbir eski uygulama sürümünün çalışmadığından emin olana kadar bekler.
Contract ekipleri genellikle gerginleştirir çünkü geri dönüşü olmayan bir nokta gibi hissettirir. Eğer expand doğru yapıldıysa, contract çoğunlukla temizliktir ve yine de küçük, düşük riskli adımlarla yapılabilir.
Zamanlamayı dikkatle seçin. Backfill hemen bitince bir şeyleri silmeyin. En az bir tam release döngüsü kadar bekleyin ki gecikmiş işler ve kenar durumlar ortaya çıksın.
Güvenli bir contract sırası genelde şöyle olur:
Mümkünse contract’ı iki release’e bölün: biri kod referanslarını kaldıran (ek logging ile), diğeri veritabanı nesnelerini silen. Bu ayrım rollback ve sorun giderme sürecini kolaylaştırır.
PostgreSQL spesifikleri burada önemlidir. Bir sütunu düşürmek çoğunlukla metadata değişikliğidir, ama kısa bir ACCESS EXCLUSIVE kilidi gerektirebilir. Sessiz bir zaman planlayın ve migration’ı hızlı tutun. Ek indeksler oluşturduysanız, yazmaları engellememek için DROP INDEX CONCURRENTLY kullanmayı tercih edin (bu işlem bir transaction bloğu içinde çalıştırılamaz; dolayısıyla migrasyon aracınızın bunu desteklemesi gerekir).
Kesintisiz migrasyonlar veritabanı ve uygulama neyin izinli olduğu konusunda anlaşmayı bıraktığında başarısız olur. Desen yalnızca her ara durum eski kod ve yeni kod için güvenliyse işe yarar.
Bu hatalar sık görülür:
Gerçekçi bir senaryo: API’den full_name yazılmaya başlanır, ama kullanıcı oluşturan bir arka plan işi hâlâ sadece first_name ve last_name ayarlıyor. Gece çalışır, full_name = NULL olan satırlar eklenir ve sonrasında full_name her zaman varmış gibi varsayan kod sorun çıkarır.
Her adımı günlerce çalışabilecek bir release gibi değerlendirin:
Tekrar edilebilir bir kontrol listesi, sadece tek bir veritabanı durumunda çalışan kodu göndermenizi engeller.
Deploy etmeden önce veritabanında genişletme parçalarının (yeni sütunlar/tablolar, düşük kilitli oluşturulmuş indeksler) zaten mevcut olduğunu doğrulayın. Sonra uygulamanın toleranslı olduğunu doğrulayın: eski şekil, genişletilmiş şekil ve yarı-backfill durumunda çalışmalı.
Kısa bir kontrol listesi:
Bir migrasyon, okumalar yeni veriyi kullandığında, yazmalar artık eski veriyi tutmadığında ve en az bir basit doğrulama (count veya örnekleme) ile backfill’in doğrulandığında tamamlanmış sayılır.
Diyelim PostgreSQL’de customers tablonuz var ve phone sütunu çeşitli formatlarda ve bazen boş tutuluyor. Bunu phone_e164 ile değiştirmek istiyorsunuz ama release’leri engellemek veya uygulamayı kapatmak istemiyorsunuz.
Temiz bir expand/contract sırası şöyle olur:
phone_e164 sütununu nullable, varsayılan değersiz ekleyin ve henüz ağır kısıtlamalar koymayın.phone hem phone_e164 yazacak şekilde güncelleyin, ancak kullanıcılar için hiçbir şey değişmemesi adına okumaları phone üzerinde tutun.phone_e164’yi oku, NULL ise phone’a geri dön şeklinde bir fallback kullanın.phone_e164 kullandığından emin olunca fallback’i kaldırın, phone’u silin ve gerekiyorsa sıkı kısıtlamaları ekleyin.Her adım geriye uyumlu kaldığı sürece rollback basit olur. Okuma değişikliği sorun çıkarırsa uygulamayı geri alırsınız ve veritabanında her iki sütun da duruyor. Backfill yük yükseltirse işi duraklatır, batch boyutunu azaltır ve sonra devam edersiniz.
Ekip uyumunu korumak için planı tek bir yerde dokümante edin: kesin SQL, hangi release’in okumaları çevireceği, tamamlanmayı nasıl ölçeceğiniz (ör. phone_e164’nin yüzde kaçının NULL olmadığı) ve her adımın sahibi kim olduğu.
Expand/contract en iyi şekilde rutin haline geldiğinde çalışır. Ekibinizin her şema değişimi için yeniden kullanabileceği kısa bir runbook yazın; ideal olarak bir sayfa ve yeni birinin takip edebileceği kadar spesifik olsun.
Pratik bir şablon şunları kapsar:
Sorumluluğu baştan belirleyin. “Herkesin bir başkası yapacak sanması” eski sütunların ve feature flag’lerin aylarca kalmasının sebebidir.
Backfill çevrimiçiyse bile, bunu daha düşük trafikli zamanda planlayın. Partileri küçük tutmak, DB yükünü izlemek ve gecikme arttığında hızla durmak daha kolaydır.
Eğer Koder.ai (koder.ai) ile planlama ve deploy yapıyorsanız, Planning Mode migrasyon aşamalarını ve kontrol noktalarını dokümante etmek için faydalı olabilir. Uyumluluk kuralları yine geçerlidir; ancak adımları yazılı hale getirmek, kesintilere yol açan “sıkıcı” adımların atlanmasını zorlaştırır.
Çünkü veritabanınız uygulamanın çalışan tüm sürümleri tarafından ortak kullanılır. Rolling deploy’lar ve arka plan işleri sırasında eski ve yeni kod aynı anda çalışabilir; bir migrasyon isim değişikliği yaparsa, sütun silerse veya kısıtlama eklerse, o anda çalışan sürümlerden biri bu yeni şema durumuna hazır olmayabilir.
Her ara veritabanı durumunun hem eski hem de yeni kod için çalışacak şekilde tasarlanması demektir. Yeni yapıları önce eklersiniz, her iki yolu bir süre çalıştırırsınız, sonra hiçbir bağlılık kalmadığı kesinleşince eski yapıları kaldırırsınız.
Expand, yeni sütunlar, tablolar veya indeksler ekleyerek mevcut uygulamanın ihtiyaç duyduğu hiçbir şeyi kaldırmaz. Contract ise yeni yolun tamamen çalıştığı kanıtlandıktan sonra eski sütunları, eski okuma/yazma yollarını ve geçici senkronizasyon mantığını temizleme aşamasıdır.
Genelde en güvenli başlangıç, varsayılan değeri olmayan nullable bir sütun eklemektir; bu eski kodun çalışmasını bozmadan ağır kilitlere yol açmaz. Sonra uygulamayı bu sütunun eksik veya NULL olmasına toleranslı şekilde deploy edin, kademeli olarak backfill yapın ve ancak daha sonra NOT NULL gibi kısıtlamaları sıkılaştırın.
Geçiş boyunca yeni uygulama sürümünün hem eski alanı hem de yeni alanı yazdığı durumlarda kullanılır. Bu, eski uygulama örnekleri ve işler hala sadece eski alanı güncelliyor olsa bile verinin tutarlı kalmasını sağlar.
Küçük, hızlı biten partiler halinde backfill yapın ve her partiyi idempotent olacak şekilde tasarlayın; böylece tekrar çalıştırıldığında sadece hâlâ ihtiyaç duyan satırlar güncellenir. Sorgu sürelerini, kilit beklemelerini ve replikasyon gecikmesini izleyin; veritabanı ısınırsa işi duraklatmaya veya batch boyutunu küçültmeye hazır olun.
Önce tamamlanmışlığı kontrol edin: yeni sütunda beklenmeyen NULL kalmadığından emin olun. Ardından örnekleme veya sürekli karşılaştırmalarla eski ve yeni değerlerin tutarlı olduğunu doğrulayın. Deploylardan hemen sonra yeni sütunu okuyan yolların hata oranlarını izleyin.
Çok erken NOT NULL eklemek, büyük bir işlemi tek bir transaksiyonda backfill yapmak, varsayılanların masrafsız olduğunu varsaymak, yazmalar henüz yeni sütunu doldurmadan okumaları yeni sütuna geçirmek ve cron işler, raporlama sorguları gibi diğer yazıcı/okuyucuları unutmak sık yapılan hatalardır.
Eski alan yazmayı durdurduktan, okumaları yeni alana geçirdikten ve yeterli süre bekledikten sonra güvenlidir. Birçok ekip bunu ayrı bir sürüm olarak ele alır: önce uygulama içi referansları kaldırmak (ek logging ile), daha sonra veritabanı nesnelerini silmek. Bu ayrım geri alma ve hata ayıklamayı kolaylaştırır.
Eğer düşük trafikli ve bakım penceresini kaldırabiliyorsanız tek seferlik bir pencere yeterli olabilir. Ancak gerçek kullanıcı trafiğiniz, çoklu uygulama örnekleri, arka plan işleriniz veya sıkı SLA’lar varsa expand/contract genellikle ekstra adımları hak eder çünkü rollout ve rollback daha güvenli olur. Koder.ai Planning Mode içinde aşamaları ve kontrolleri önceden yazıya dökmek, atlanan “sıkıcı” adımları engellemeye yardımcı olur.