Yayına çıkarken site neden kapanıyor? Sıfır kesintili dağıtım ve şema değişikliği
Kısa cevap: yayına çıkarken site kapanıyorsa sebep neredeyse her zaman üç yerden birindedir. Ya bir şema değişikliği tabloyu kilitliyor ve arkasında sorgu kuyruğu birikiyor, ya eski ve yeni kod birkaç dakika boyunca aynı anda çalışırken birbirinin varsayımını bozuyor, ya da yük dengeleyici henüz hazır olmayan veya çoktan kapanmaya başlamış bir sürece istek gönderiyor. Çözüm daha uzun bir bakım penceresi değil, her sürümü bir önceki sürümle yan yana çalışabilecek şekilde tasarlamak. Bunu yapan ekipler gün içinde yayına çıkıyor; yapmayanlar gece 03:00'ü bekliyor ve geri alma anında elinde plan bulamıyor. Aşağıdaki adımların hepsi, kurulu bir dağıtım hattının üzerine oturuyor.
Kademeli güncellemede iki sürüm aynı anda çalışır
Bu, geri kalan her şeyi belirleyen tek cümle. Kademeli güncelleme (rolling update) sırasında filonuzun bir kısmı yeni sürümü, kalanı eski sürümü çalıştırır. Bu pencere birkaç saniye de sürebilir, on dakika da; süresi önemli değil, var olması önemli. Aynı veritabanına, aynı kuyruğa ve aynı önbelleğe iki farklı kod sürümü yazıyor.
Buradan çıkan kural şu: bir sürüm, kendinden bir önceki sürümle uyumlu olmak zorunda. Kolonu silen bir migration ile o kolonu kullanmayı bırakan kodu aynı anda yayınlarsanız, eski sürecin bir sonraki sorgusu "column does not exist" hatası verir. Kullanıcı bunu 502 olarak görür. Uygulama kapanmadı, sadece kendisiyle çelişti.
Şema değişikliği aslında nerede kilitleniyor
PostgreSQL'de ALTER TABLE çoğu durumda ACCESS EXCLUSIVE kilidi ister. Bu kilit tek başına sorun değil, çünkü çoğu DDL milisaniyeler sürer. Sorun kuyruk davranışında: uzun süren bir raporlama sorgusu tabloyu tutuyorsa, DDL beklemeye geçer ve o andan itibaren tabloya gelen bütün yeni sorgular DDL'in arkasında sıraya girer. Üç saniyelik bir şema değişikliği, önündeki beş dakikalık sorgu yüzünden siteyi beş dakika dondurabilir. Uzun sorguların nereden geldiğini yavaş sorgu teşhisi yazımızda ayrıntılı anlattık.
Bunun tek satırlık panzehiri var: migration oturumunda lock_timeout ayarlayın. İki üç saniye tipik bir değer. Kilit o sürede alınamazsa komut hata verir, siz de birkaç saniye sonra tekrar denersiniz. Dağıtımın iptal olması, uygulamanın durmasından ucuzdur.
Sık yapılan işlerin güvenli halleri de biliniyor:
- Kolon ekleme. PostgreSQL 11'den itibaren sabit varsayılan değerle kolon eklemek tabloyu yeniden yazmaz; varsayılan katalogda tutulur ve okuma sırasında uygulanır. Daha eski sürümlerde aynı komut bütün tabloyu yeniden yazar.
- İndeks oluşturma.
CREATE INDEX CONCURRENTLYyazma işlemlerini engellemez. Karşılığında tabloyu iki kez tarar ve normalinden belirgin biçimde uzun sürer. Başarısız olursa geride geçersiz (invalid) bir indeks bırakır; bunu düşürüp tekrar denemek gerekir. Ayrıca transaction bloğu içinde çalışmaz, yani migration aracınız her migration'ı otomatik transaction'a sarıyorsa bu adımı ayrı çalıştırmalısınız. - NOT NULL yapma. Doğrudan
SET NOT NULLtabloyu baştan sona tarar ve bu sırada ACCESS EXCLUSIVE kilidi tutar. Bunun yerine önceCHECK (kolon IS NOT NULL) NOT VALIDkısıtı ekleyin, sonraVALIDATE CONSTRAINTçalıştırın (bu adım SHARE UPDATE EXCLUSIVE kilidi aldığı için yazmaları engellemez), en sonSET NOT NULLdeyin. PostgreSQL 12 ve sonrasında geçerli kısıt varlığı taramayı atlatır.
MySQL tarafında hikâye farklı. InnoDB'de kolon ekleme 8.0.12'den beri varsayılan olarak ALGORITHM=INSTANT ile yapılır ve yalnızca veri sözlüğünde meta veri değişikliğidir; 8.0.29'dan itibaren kolonu tablonun sonuna değil istediğiniz konuma ekleyebilirsiniz. Sınırları var: satır sürümü sayısı 64'ü geçemez, sonrasında tablonun COPY veya INPLACE ile yeniden inşa edilmesi istenir, ayrıca ROW_FORMAT=COMPRESSED tablolarda ve FULLTEXT indeksi olan tablolarda çalışmaz. INSTANT kapsamı dışına çıkan değişiklikler için gh-ost (GitHub) ve pt-online-schema-change (Percona) gölge tablo kurup veriyi kopyalar ve sonunda tabloyu takas eder. İkisi de kabul edilmiş araçlar, ama takas anını ve replika gecikmesini izlemeden çalıştırmayın.
Genişlet, taşı, daralt
Uyumsuz görünen her şema değişikliği üç ayrı sürüme bölünebilir. Kalıbın adı genişlet-daralt (expand and contract); Martin Fowler'ın yazdığı Parallel Change kalıbının veritabanına uygulanmış hali.
Diyelim ki telefon kolonunu serbest metinden E.164 biçimine geçireceksiniz. Yeniden adlandırma tek adımda yapılırsa eski kod anında kırılır. Bölünmüş hali:
- Genişlet.
telefon_e164kolonunu nullable olarak ekleyin. Şema değişti, davranış değişmedi, hiçbir kod bundan etkilenmedi. - Çift yazma. Yeni sürüm her iki kolona da yazsın, okumayı hâlâ eskisinden yapsın. Bu sürüm geri alınabilir, çünkü eski sürüm de çalışmaya devam eder.
- Geçmişi doldurun. Eski satırları partiler hâlinde yeni kolona taşıyın.
- Okumayı çevirin. Yeni kolondan okuyan sürümü yayınlayın. Sorun çıkarsa geri alma hâlâ ücretsiz, çünkü eski kolon dolu ve güncel.
- Daralt. Birkaç gün sonra çift yazmayı kaldırın, ardından ayrı bir sürümde eski kolonu düşürün.
Beş adım fazla gibi görünüyor. Karşılığında beşinin de geri alınabilir olması var, ve gerçek maliyet dağıtım sayısında değil, gece yarısı geri dönülemeyen bir migration'la baş başa kalmakta.
Geriye dönük doldurmayı tek UPDATE ile yapmayın
Milyonlarca satırı tek komutla güncellemek üç şeyi aynı anda bozar: uzun süren transaction kilitleri tutar, WAL üretimi patlar, replikalar geride kalır. Okuma trafiğini replikadan alan bir uygulamada bu, kullanıcıya "az önce kaydettiğim veri yok" olarak döner.
Doğru yol partili güncelleme: birincil anahtara göre birkaç bin satırlık dilimler halinde ilerleyin, her dilimi kendi transaction'ında commit edin, dilimler arasına kısa bir bekleme koyun ve replika gecikmesini izleyip eşiği aşınca yavaşlayın. Doldurma işini migration adımının içine değil, durdurulup kaldığı yerden devam edebilen ayrı bir görev olarak yazın. Bir saat süren doldurma işi normaldir; bir saat kilit tutan doldurma işi olay kaydıdır.
Trafik: hazır olmayan ve kapanmakta olan süreçler
Şema tarafını çözdükten sonra geriye kesintinin en sık ama en kolay sebebi kalıyor. Kubernetes'te bir Deployment'a strateji yazmazsanız varsayılan maxUnavailable ve maxSurge yüzde 25'tir; yani güncelleme sırasında kapasitenizin dörtte biri düşebilir. Sıfır kesinti isteyen üretim iş yüklerinde maxUnavailable: 0 ve küçük bir maxSurge (genelde 1) daha doğru: önce yeni pod ayağa kalkar, sonra eskisi çekilir.
Hazır olma yoklaması (readiness probe) tanımlı değilse Kubernetes konteyner başlar başlamaz pod'u hazır sayar. JVM'i ısınmamış, bağlantı havuzu kurulmamış bir sürece trafik gider. Yoklamanın uygulamanın gerçek bağımlılıklarını kontrol etmesi gerekir, boş bir 200 döndürmesi değil.
Kapanış tarafı daha sinsi. Pod silindiğinde iki şey aynı anda başlar: kubelet sonlandırmaya geçer ve servis uç noktası (EndpointSlice) listeden çıkarılır. Bu ikisi arasında sıra garantisi yoktur. Uç nokta güncellemesi kümeye yayılana kadar yük dengeleyici o pod'a istek göndermeye devam edebilir. Standart çözüm preStop kancasına birkaç saniyelik bir bekleme koymak: pod hemen ölmez, uç nokta yayılımı tamamlanır, sonra SIGTERM gelir. Dikkat edilmesi gereken nokta, preStop ile SIGTERM sonrası kapanışın aynı bütçeyi paylaşması; terminationGracePeriodSeconds varsayılanı 30 saniyedir ve sayaç sonlandırma başlarken çalışmaya başlar, SIGTERM'den sonra değil. 15 saniyelik bir bekleme koyduysanız uygulamanıza 15 saniye kalır.
Sanal makine tarafında karşılığı bağlantı boşaltma. AWS Application Load Balancer'da hedef grubun kayıt silme gecikmesi varsayılan 300 saniye, aralık 0 ile 3600 arasında. Hedef "draining" durumundayken yeni istek almaz ama devam eden istekleri tamamlar. Uygulamanız SIGTERM'i yakalayıp yeni bağlantı kabul etmeyi kesmiyorsa bu ayarın tek başına anlamı yok.
Mavi-yeşil, kanarya ve özellik bayrakları
Üçü aynı sorunu farklı yerlerden çözer, birbirinin alternatifi değildir.
Mavi-yeşil dağıtımda iki tam ortam vardır ve trafik yönlendirici seviyesinde tek hamlede çevrilir. En hızlı geri alma budur, çünkü eski ortam ayakta durur. Yakalanması gereken detay şu: veritabanı genelde ortak kalır. Yani şema, iki sürümle de uyumlu olmak zorunda. Genişlet-daralt yapmadan mavi-yeşil kurmak, geri alma düğmesini boyamaktan ibarettir.
Kanarya yayında yeni sürüme trafiğin küçük bir yüzdesi verilir, hata oranı ve gecikme karşılaştırılır, sonra kademeli artırılır. Kanaryanın değeri karşılaştırma metriğinde: hangi metriğin hangi eşiği aşınca otomatik geri alınacağını önceden yazmadıysanız, elinizde kanarya değil daha yavaş bir dağıtım vardır.
Özellik bayrakları ise dağıtımı yayına almadan ayırır. Kod üretimde durur, kapalıdır, siz açtığınızda özellik canlıya çıkar. Sorun çıktığında yeni sürüm çıkmaya gerek kalmadan bayrak kapatılır. Standart tarafında OpenFeature, CNCF'e Haziran 2022'de kabul edildi ve Kasım 2023'te incubating seviyesine geçti; sağlayıcı bağımsız bir API sunduğu için bayrak sisteminizi değiştirdiğinizde uygulama kodunuz aynı kalır. Bayrakların bilinen bedeli birikmeleri: her bayrak bir dallanmadır ve iki kod yolunu birden canlı tutar. Bayrağı açtıktan sonra temizleme işini de aynı sprint'e yazın.
Ön yüz de kesinti yaşar, farklı biçimde
Sunucu tarafı kusursuz olsa bile kullanıcının tarayıcısında açık duran eski sekme sorun çıkarır. Modern derleme araçları JavaScript paketini hash'li parçalara böler. Kullanıcı sayfayı yeni dağıtımdan önce açtıysa elinde eski index.html vardır ve tembel yüklenen bir rota istendiğinde artık var olmayan bir dosya adı ister. Sonuç, kullanıcının anlamlandıramadığı bir "chunk load error".
İki tarafı da çözmek gerekiyor: eski derleme çıktılarını CDN'de birkaç sürüm boyunca silmeyin (hash'li dosya adları çakışma yaratmaz), ve uygulamaya bir sürüm kontrolü koyun. Basit hali, sunucudan gelen sürüm etiketiyle istemcideki etiketi karşılaştırıp kullanıcıya sayfayı yenilemesini önermektir. API tarafında da aynı disiplin geçerli: alan eklemek güvenlidir, alan kaldırmak ve zorunlu hale getirmek değildir. Eski istemcilerin ne kadar süre yaşayacağını mobil uygulamalarda sürüm dağılımından, web'de oturum süresinden tahmin edebilirsiniz.
Arka plan işleri ve kuyruklar
Kesinti avında en çok atlanan yer burası. Bir işçi (worker) süreci SIGTERM aldığında elindeki işi bırakıp ölürse, kuyruk o mesajı yeniden dağıtır ve iş ikinci kez çalışır. Ödeme çekiyorsa iki kez çeker. Bu yüzden işçilerin kapanış davranışı yazılı olmalı: yeni mesaj almayı kes, elindekini bitir, sonra çık. Bitiremeyeceği kadar uzun işler için işin kendisini idempotent yapın, yani aynı anahtarla ikinci çalıştırma yeni bir etki üretmesin.
Mesaj biçimi değişiklikleri de tıpkı şema gibi davranır. Yeni bir zorunlu alan yayınlayan üreticiyi, o alanı anlamayan tüketicilerden önce yayına almayın. Tüketiciler tanımadığı alanları görmezden gelecek şekilde yazılırsa (tolerant reader) sıralama derdi büyük ölçüde kalkar.
Bakım penceresi ne zaman doğru cevaptır
Her sistemin sıfır kesintiye ihtiyacı yok. Kullanıcıları 09:00 ile 18:00 arasında çalışan, yurt içi tek bölgeden hizmet veren bir B2B uygulamada, gece yarısı planlı 20 dakikalık pencere, aylarca sürecek bir mühendislik yatırımından ucuza gelir. Karar ölçütü basit: kesintinin dakika başına iş maliyeti ile onu ortadan kaldırmanın mühendislik maliyetini karşılaştırın, üstüne kesinti ihtimalinin sıklığını koyun.
Ama pencereye karar verseniz bile genişlet-daralt disiplinini bırakmayın. O disiplinin asıl getirisi kesintisiz dağıtım değil, geri alınabilirlik. Pencere içinde de olsa geri dönemediğiniz bir migration, pencerenin bitiminde sizi hâlâ orada bırakır.
Nereden başlanır
Somut ilk üç adım şöyle: migration çalıştırıcınıza lock_timeout ve tekrar deneme mantığı ekleyin, servislerinize gerçek bağımlılıkları kontrol eden bir hazır olma yoklaması ve kapanışta SIGTERM ele alma yazın, sonraki şema değişikliğini genişlet-daralt adımlarına bölerek yayınlayın. Ölçüm tarafında DORA'nın değişiklik hata oranı (change fail rate) ve 2024'te eklenen dağıtım yeniden işleme oranı (deployment rework rate) metrikleri işe yarar, çünkü ikisi de "kaç dağıtım müdahale gerektirdi" sorusunu ölçer.
Bunlardan hiçbiri büyük bir proje değil, üçü birlikte ekibin dağıtıma bakışını değiştiriyor. Wedevit bu incelemeyi uzaktan yapıyor: son üç ayın migration dosyalarını, dağıtım yapılandırmasını ve kapanış davranışını okuyup kesintiyi nerede ürettiğinizi çıkarıyoruz. Sonrasında ölçüm tarafını izlenebilirlik ve hata bütçesi yazımızla, her dağıtımın taşıdığı riski düşürme kısmını test otomasyonu yazımızla birleştirebilirsiniz. Sıfır kesinti bir araç satın alma kararı değil; her değişikliği bir öncekiyle uyumlu tasarlama alışkanlığı.
Bu konuda yardıma mı ihtiyacınız var?