· kaynak dev.to (home feed)
Postgres saha notları: milisaniyelerle ölçülen ALTER TABLE ifadeleri neden yine de kesintiye yol açar
Bir dev.to saha rehberi, milisaniyelerle ölçülen bir ALTER TABLE'ın bir API'yi nasıl devirebileceğini ve lock_timeout ile expand/contract migrations'ın bunu nasıl önlediğini anlatıyor.

Bir API'yi deviren bir milisaniyelik migration
dev.to'daki bir saha notları yazısı, birçok ekibin tanıdığı bir production arızasını adım adım anlatıyor: staging'de göz açıp kapayana kadar çalışan, tek bir nullable kolon ekleyen bir migration, production'da donup kaldı ve API yanıt vermeyi bıraktı. Yazarın ana noktası şu: sorun asla kolonun kendisi değildi — sorun, onun arkasında kuyruğa giren her şeydi.
Yazıya göre ALTER TABLE, Postgres'in sunduğu en kısıtlayıcı kilit modu olan ACCESS EXCLUSIVE kilidini gerektirir ve bu kilit, düz bir SELECT'in aldığı ACCESS SHARE dahil diğer tüm kilitlerle uyumsuzdur. Kilit istekleri geliş sırasına göre verilir ve sonra gelen daha zayıf istekler, önceden beklemeye girmiş daha güçlü bir isteği geçemez. Dolayısıyla bir ALTER TABLE, tamamlanmasını beklediği uzun süreli bir transaction'a takıldıysa, o tablodaki sonraki her okuma ve yazma onun arkasında sıraya girer. Milisaniyeler süren bir catalog güncellemesi, tabloyu halihazırda tutan şey ne kadar sürerse o kadar süren bir kilitlenmeye dönüşür.
Yazı üç tekrarlanan suçlu sayıyor: uzun süreli bir analitik sorgusu, transaction açıp hiç commit etmeyen bir web isteği ve normal autovacuum'un aksine geri adım atmayan anti-wraparound modundaki autovacuum. Teşhis için yazar, pg_stat_activity'yi pg_blocking_pids() ile join etmeyi öneriyor; blocking listesi boş olmayan her satır bekliyor demektir ve o listedeki PID, asıl çözmeniz gereken oturumu gösterir.
lock_timeout ile hızlı başarısız olma
Ana öneri, her DDL öncesi kısa bir lock_timeout belirlemek — birkaç saniye. lock_timeout bir ifadenin kilit elde etmek için ne kadar bekleyeceğini sınırlarken, statement_timeout kilidi elde ettikten sonra ne kadar çalışacağını sınırlar. Üç saniyelik bir üst sınırla, kilidini alamayan bir migration SQLSTATE 55P03 ile iptal olur ve kuyruğu boşaltır; böylece kuyruk en fazla üç saniye içinde temizlenir. İptal olmak sorun değil: doğru hamle tekrar denemektir ve birkaç an sonra yapılan deneme genellikle uzun sorgular arasında bir boşluk bulur. Yazı ayrıca migrations'ları deploy'lara paketlemek yerine ayrı bir pipeline adımı olarak çalıştırmayı tavsiye ediyor.
Genişlet, backfill yap, daralt
Rolling ve serverless deploy'lar eski ve yeni uygulama sürümlerini aynı anda tek bir veritabanına karşı çalıştırdığı için şema, ikisiyle de uyumlu kalmak zorunda. Expand/contract bunu üç deploy'la başarır: yeni yapıyı eskisinin yanına ekleyin ve çift yazım yapın; mevcut satırları toplu işlerle backfill edin ve okumaları yeni yapıya geçirin; son olarak eski kolona yazmayı durdurun ve ancak o zaman onu düşürün. Bir kolon yeniden adlandırma tuzağı örneklendirir — RENAME COLUMN anlık, yalnızca metadata üzerinde yapılan bir işlemdir, yine de eski adı seçmeye devam eden her eski instance, commit edildiği anda hata vermeye başlar. Yazarın kuralı şu: her deploy iki şeyden birini yapmalı — isteğe bağlı bir şey eklemeli ya da çalışan hiçbir instance'ın bağımlı olmadığı bir şeyi kaldırmalı — ama asla ikisini aynı anda yapmamalı. Bir kolonu düşürmek anlıktır ama fiilen geri alınamaz, bu yüzden dizinin sonunda, kendi başına yapılmalıdır.
Ucuz DDL ile tam tablo yeniden yazımı karşılaşması
Yeniden yazım, ACCESS EXCLUSIVE tutarken her satırı yeni dosyalara kopyalar — büyük bir tabloda bu, üst sınırı olmayan bir kilitlenmedir. Postgres 11'den beri NOT NULL sabit bir default ile ADD COLUMN yalnızca catalog düzeyindedir, ama gen_random_uuid() gibi volatile bir default her satırı yeniden yazar. varchar'dan text'e genişletme yalnızca metadata düzeyindedir; int'den bigint'e genişletme tam bir yeniden yazımdır. CREATE INDEX inşanın tamamı boyunca yazmaları engeller, bu yüzden yazı CREATE INDEX CONCURRENTLY öneriyor; bu bir transaction bloğu içinde çalışamaz ve dolayısıyla kendi başına bir adım olarak çalıştırılmalıdır — ve başarısız bir deneme, tekrar denemeden önce düşürülmesi gereken geçersiz bir index bırakır.
Constraint'ler için yazar iki adımlı bir yaklaşıma yaslanıyor: bir foreign key veya CHECK constraint'i NOT VALID olarak eklemek anlıktır ve yalnızca yeni yazmaları doğrular; ardından gelen VALIDATE CONSTRAINT, mevcut satırları ne okumayı ne de yazmayı engelleyen daha zayıf SHARE UPDATE EXCLUSIVE kilidi altında tarar. Aynı biçim, engelleyici bir tarama olmadan bir kolonu NOT NULL yapar — Postgres 12'den beri doğrulanmış bir CHECK constraint kanıt olarak hizmet eder ve SET NOT NULL kendi taramasını atlar.
Neden önemli
Migration kesintilerinin çoğu yavaş DDL'den kaynaklanmaz; anlık bir ifadenin kilit kuyruğunun başında takılmasından kaynaklanır. Bu arıza modunu önlemek ucuzdur. Retry'larla kısa bir lock_timeout, sınırsız bir kesintiyi birkaç başarısız denemeye dönüştürür ve expand/contract, şema değişikliklerini rolling deploy'larla uyumlu tutar. Bunlar altyapı yatırımları değil süreç değişiklikleridir ve canlı bir API arkasında Postgres çalıştıran her ekip için geçerlidirler.
- #postgres
- #database
- #migrations
- #devops
- #backend