Organizasyonlar mikro hizmet mimarilerini ölçeklendirdikçe, veritabanı yönetimi için geleneksel monolitik yaklaşım önemli bir darboğaza dönüşür. Her hizmetin kendi veri deposuna sahip olduğu merkezi olmayan bir sistemde, hizmet kesintisi olmadan şemaları evrimleştirmenin zorluğu katlanarak artar. Senkron, monolitik bir veritabanından asenkron, dağıtık bir ekosisteme geçiş, hassas bir şekilde yönetilmezse veri bozulmasına veya hizmet bozulmasına yol açabilecek karmaşık eşzamanlılık çatışmalarını beraberinde getirir.
Bu yazı, sıfır kesintiyle şema evrimini uygulamak için gereken mimari desenleri ve otomatik stratejileri incelemektedir. Bağımsız veri depoları genelinde şema değişikliklerini nasıl yöneteceğimizi, özellikle de geçiş penceresi sırasında eşzamanlı okuma ve yazma sorunlarını ele alacağız.
Dağıtık Şema Evriminin Zorlukları
Mikro hizmet ortamında, bir hizmet tarafından başlatılan bir veritabanı değişikliği, diğerlerine otomatik olarak yayılmaz. Tek bir geçiş betiğinin tüm sistemi yönetebildiği monolitik bir uygulamadan farklı olarak, dağıtık sistemler birden fazla bağımsız veritabanı arasında senkronize bir şema değişikliği dansı gerektirir. Birincil risk, geçiş sırasında veritabanının "ara durumundadır". Hizmet A yeni bir şema sürümü dağıtırken Hizmet B hala eski sürümü çalıştırıyorsa, her ikisi aynı anda uyumsuz veri yapılarını okumaya veya yazmaya çalışabilir.
Ayrıca, yüksek eşzamanlılık ortamlarında, aynı hizmetin birden fazla örneği aynı geçiş mantığını çalıştırmaya çalıştığında veya uygulama kodu veritabanıyla şemanın geçici durumunu ihlal edecek şekilde etkileşime girdiğinde yarış durumları sıkça meydana gelir. Güçlü otomasyon olmadan, bu çatışmalar veri kaybına veya sistem kesintisine yol açar.
Ekle ve Geçiş Deseni
Sıfır kesintiyle şema evrimi için endüstri standardı, genelde genişleme-küçülme evresi olarak adlandırılan "Ekle ve Geçiş" (Add-and-Migrate) desenidir. Bu strateji, geriye uyumluluk ilkesine dayanır. Mevcut bir sütunu asla silmez veya değiştirmezsiniz; bunun yerine yeni yapıyı eklerken eskisini korursunuz.
Süreç üç aşamadan oluşur:
- Genişletme: Yeni sütun(lar)ı veritabanı şemasına, bunları doldurmadan ekleyin. Hem eski hem de yeni kod sürümleri yan yana var olabilir; yeni kod yeni sütunlara yazarken, eski kod onları görmezden gelir.
- Geri Doldurma: Yeni sütunları eski sütunlardaki verilerle doldurun. Tabloyu kilitlememek için bu işlem asenkron olarak gerçekleşebilir.
- Küçültme: Tüm hizmetler yeni kod sürümüne geçtikten sonra, eski sütunları kaldırın.
Geri doldurma aşaması sırasında eşzamanlılık çatışmalarını yönetmek için, uygulama mantığının hem eski hem de yeni veriyi içeren bir satır durumunu doğru şekilde ele almasını ve geçiş betiğinin devam eden yazma işlemleriyle çakışmamasını sağlamalıyız.
İyimser Kilitleme ile Otomatik Çatışma Algılama
Merkezi olmayan veri depolarında, veritabanı seviyesindeki kilitler (örneğin `SELECT ... FOR UPDATE`) kullanmak performans bozulmasına ve ölümcül çakışmalara (deadlock) yol açabilir. Daha ölçeklenebilir bir yaklaşım iyimser kilitlemedir. Satırlara bir sürümlendirme mekanizması veya özel bir çatışma belirteci ekleyerek, geçiş penceresi sırasında verinin başka bir işlem tarafından değiştirilip değiştirilmediğini tespit edebiliriz.
İşte SQL tabanlı bir dağıtık depoda çatışma yönetimi ile bir geçiş şemasını yapılandırmanın pratik bir örneği:
-- Adım 1: Genişletme - Varsayılan değer ve sürümlendirme ile yeni sütun ekle
ALTER TABLE users ADD COLUMN updated_at_version INT DEFAULT 1;
ALTER TABLE users ADD COLUMN legacy_status VARCHAR(50); -- Uyumluluk için eski sütun korunur
-- Adım 2: Eşzamanlılık Kontrolü - Geçiş durumunu izlemek için özel bir sütun kullan
-- Bu, geri doldurma sırasında birden fazla hizmetin birbirinin işini üzerine yazmasını engeller
ALTER TABLE users ADD COLUMN migration_version INT DEFAULT 0;
-- Adım 3: Geri Doldurma Mantığı (Uygulama tarafı için sahte kod)
-- Uygulama yazmadan önce migration_version'u kontrol etmelidir
BEGIN;
SELECT * FROM users WHERE user_id = 123 FOR KEY SHARE;
IF migration_version < 2 THEN
UPDATE users
SET new_status = status,
migration_version = 2
WHERE user_id = 123 AND migration_version = 0;
END;
COMMIT;
Bu senaryoda, uygulama kodu güncelleme yapmadan önce `migration_version`u kontrol eder. Başka bir örnek zaten satırı geçiş yaptıysa, güncelleme atlanır ve "yazma kayması" (write skew) anormalliği önlenir. Bu yaklaşım, yüksek eşzamanlılık altında bile şema evrim sürecinin tutarlı kalmasını sağlar.
Şema Güvenliği için Kod Olarak Altyapı
Manuel geçiş betikleri insan hatasına açıktır ve karmaşık ortamlarda nadiren ölçeklenebilir. En etkili strateji, şema evrimini kod olarak işlemektir. Şema değişikliklerini CI/CD kanalınıza entegre ederek, eşzamanlılık simülasyonlarının yeni şema üzerinde çalıştırıldığı sıkı test ortamlarını zorunlu kılabilirsiniz.
Flyway veya Liquibase gibi araçlar, geçiş betiklerini sürümlü artefaktlar olarak tanımlamanıza olanak tanır. Ancak mikro hizmetler için, veritabanı ile etkileşime giren kodun hangi sürümünü kontrol eden bir "özellik bayrağı" (feature flag) sistemi uygulamanız gerekir. Bu, şemanın dağıtımını mantığın dağıtımından ayırır. Yeni şemayı trafiğin %10'una yayabilir, çatışmalar için izleyebilir ve eşzamanlılık metrikleri stabil hale gelene kadar ilerleyebilirsiniz.
Sonuç
Mikro hizmet mimarisinde sıfır kesintiyle şema evrimini başarmak sadece bir veritabanı görevi değil; uygulama mantığı, altyapı ve veri modelleme arasında koordinasyon gerektiren sistemsel bir zorluktur. Ekle ve Geçiş desenine bağlı kalarak, iyimser kilitleme stratejileri uygulayarak ve tüm yaşam döngüsünü kod olarak altyapı ile otomatikleştirerek, mühendislik ekipleri merkezi olmayan veri depolarının karmaşıklıklarını yönetebilir.
Amaç, geçişi son kullanıcı için görünmez kılmak ve veri bütünlüğünü sağlamaktır. Sisteminiz büyüdükçe, eşzamanlılık çatışmalarının maliyeti artar; bu da otomatik, sağlam şema evrim stratejilerini sadece bir en iyi uygulama değil, iş sürekliliği için bir zorunluluk haline getirir.