- Postgres şema migration işlemleri; kilitler, tablo yeniden yazmaları ve replikasyon gecikmeleri operasyonel kesintilere dönüşebildiği için, büyük OLTP ortamlarında özellikle yüksek risk taşır
- Risk;
DEFAULT ile NOT NULL’ın aynı anda eklenmesi, CONCURRENTLY olmadan indeks oluşturulması, kolonların hemen silinmesi, güvenli olmayan tip değişiklikleri ve doğrulamasız foreign key eklenmesi gibi tam tarama ve uzun kilitler doğuran işlemlerde yoğunlaşır
- PostgreSQL 11’den itibaren bazı kolon ekleme maliyetleri azalmış olsa da, indeksler için
CREATE INDEX CONCURRENTLY, foreign key’ler içinse NOT VALID ardından VALIDATE CONSTRAINT gibi operasyonel etkiyi düşüren prosedürler gerekir
- Büyük ölçekli değişiklikler küçük batch’lere bölünmeli; okuma replikaları, replikasyon gecikmesi, bağımlı nesneler ve mevcut uygulama instance’larının kolona referans verip vermediği de birlikte kontrol edilmelidir
- Production ölçeğindeki veride önceden test edilmeli; yıkıcı işlemler ise çok aşamalı dağıtım ve doğrulanmış bir rollback planı hazırlandıktan sonra yürütülmelidir
Şema migration’ının ön kabulleri
- Burada DB migration, DBMS değişimini değil DB şeması değişikliğini ifade eder
- Hedef değişikliklerin üç niteliği vardır
- Her değişiklik için benzersiz bir tanımlayıcı ve otomatik uygulanma prosedürü bulunan sürüm kontrollü değişiklikler
- Production’a uygulandıktan sonra değiştirilmeden yalnızca yeni değişiklikler eklenen değişmez değişiklikler
- Veritabanı şemasının aşamalı olarak evrildiği artımlı değişiklikler
- Odak, mobil ve web uygulamaları gibi OLTP kullanım senaryolarıdır; 1 saniyeyi aşan sorgu çalıştırma süresi genellikle fazla yavaş kabul edilir
- Küçük veritabanlarında ve düşük aktivite seviyesinde bazı sorunlar pek görünmeyebilir; ancak yaklaşık 10TiB ölçek ve saniyede 10⁴~10⁵ transaction yükünde sorunların çoğu ortaya çıkabilir
- Database Lab Engine, ince klonlarla geliştirme ve test için kullanılır; 10TiB’lik bir veritabanını 10 saniyenin altında klonlayarak şema değişikliği risklerini dağıtımdan önce kontrol etmeyi sağlar
- GitLab Migration Style Guide, çok sayıda Postgres şema değişikliğini otomatik olarak dağıtma deneyimini içeren bir referans kaynaktır
Kolon ekleme ve tablo yeniden yazma
DEFAULT ve NOT NULL’ı aynı anda içeren bir kolon eklemek, özellikle eski PostgreSQL sürümlerinde risklidir
- PostgreSQL 11 öncesinde tüm tablonun yeniden yazılması gerekir
- Büyük tablolarda bu işlem saatler veya günler sürebilir ve bu süre boyunca yazma kilidi oluşur
- Riskli bir örnek şöyledir
ALTER TABLE users ADD COLUMN status text DEFAULT 'active' NOT NULL;
- Daha güvenli prosedür; kolon eklemeyi, veri güncellemeyi ve constraint eklemeyi ayrı adımlara bölmektir
- Önce kolonu
NOT NULL olmadan ekleyin
- Gerekirse mevcut satırları güncelleyin
- Ardından
NOT NULL constraint’ini ekleyin
ALTER TABLE users ADD COLUMN status text DEFAULT 'active';
-- UPDATE users SET status = 'active' WHERE status IS NULL;
ALTER TABLE users ALTER COLUMN status SET NOT NULL;
- PostgreSQL 11 ve üzeri sürümlerde, volatile olmayan
DEFAULT değerine sahip kolon eklemek artık tablo yeniden yazması gerektirmez
İndeks oluşturma ve foreign key ekleme
CONCURRENTLY olmadan indeks oluşturulduğunda standart indeks oluşturma işlemi tablo üzerinde exclusive lock alır
- İndeks oluşturma bitene kadar tüm yazmalar ve bazı okumalar engellenebilir
- Riskli bir örnek şöyledir
CREATE INDEX idx_users_email ON users(email);
- Çalışan sistemlerde
CREATE INDEX CONCURRENTLY kullanmak daha güvenlidir
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CONCURRENTLY için bazı kısıtlar vardır
- Daha uzun sürer ama tablo erişimini engellemez
- Transaction bloğu içinde kullanılamaz
- Başarısız olursa silinmesi gereken geçersiz bir indeks bırakabilir
- Büyük bir tabloya foreign key constraint’ini doğrudan eklemek, mevcut veriyi doğrulamak için tüm tabloyu tarar ve uzun kilitlere yol açar
- Daha güvenli prosedür, önce constraint’i
NOT VALID ile ekleyip ardından trafiğin düşük olduğu bir zamanda doğrulamaktır
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user_id
FOREIGN KEY (user_id) REFERENCES users(id)
NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user_id;
Kolon silme ve tip değiştirme
- Production’da bir kolonu doğrudan silmek, uygulama kodu hâlâ o kolona referans veriyorsa uygulama hatalarına neden olabilir
- Kolon silme çok aşamalı yapılmalıdır
- Önce ilgili kolonu kullanmayan uygulama kodunu dağıtın
- Eski uygulama instance’larının tamamı değiştirilene kadar bekleyin
- Kolonu ayrı bir migration’da silin
- Kolon tipi değiştirme tablo yeniden yazması veya uyumluluk sorunları yaratabilir
- Downtime, veri kaybı ve uygulama hatalarıyla sonuçlanabilir
- Sorunlu örnekler şöyledir
ALTER TABLE users ALTER COLUMN id TYPE bigint;
ALTER TABLE users ALTER COLUMN email TYPE varchar(100);
integer’dan bigint’e geçerken yeni bir kolon kullanan çok aşamalı prosedür gerekir
varchar uzunluğunu azaltırken önce veriler kontrol edilmeli ve değişikliğin gerçekten gerekli olup olmadığı değerlendirilmelidir
Büyük değişiklikler, replikasyon ve bağımlı nesneler
- Çok fazla veriyi tek bir transaction içinde değiştiren migration’lardan kaçınılmalıdır
- Kilit çekişmesi ve bellek kullanımı artar
- Sorun çıktığında toparlanma süresi uzar
- Replikasyon gecikmesi büyüyebilir
- Büyük ölçekli veri migration’larını küçük batch’lere bölmek daha güvenlidir
- Migration’ın okuma replikaları ve replikasyon gecikmesi üzerindeki etkisi de birlikte değerlendirilmelidir
- Büyük migration’lar ciddi replikasyon gecikmesi yaratabilir
- Okuma replikalarının performansını etkileyebilir
- Değiştirilecek kolon veya tabloya bağımlı nesneler de kontrol edilmelidir
- View, function, trigger gibi bağımlı nesneler gözden kaçırılırsa zincirleme hatalar veya ek manuel müdahale gerekebilir
Test ve rollback planı
- Migration’ı yalnızca küçük geliştirme veri setlerinde test etmek, büyük veri setlerinin performans karakteristiklerini görmeyi zorlaştırır
- Production ölçeğinde veri klonları üzerinde test yapılmalıdır; Database Lab Engine gibi araçlar kullanılabilir
- Sorun çıktığında migration’ı geri alma yolu yoksa production sorunu uzun süreli downtime’a dönüşebilir
- Özellikle yıkıcı işlemler için doğrulanmış bir rollback planı gerekir
- Güvenli şema değişikliğinin temeli şunlardır
- Production ölçeğindeki veri üzerinde test etmek
- Riskli işlemlerde çok aşamalı yaklaşım kullanmak
CONCURRENTLY ve NOT VALID gibi PostgreSQL özelliklerinden yararlanmak
- Performans ve replikasyon etkilerini izlemek
- Her zaman rollback planı hazırlamak
1 yorum
Hacker News yorumları
Postgres'i gerçekten çok seviyorum ama bu yazının büyük kısmı kaçınılabilir ve dikkat edilmesi gereken şeylerden oluşuyor. Yine de bence Postgres'in en kötü yanı rol yönetimi
Özellikleri güçlü; iyi kullanılırsa harika olabilir, ama bunu gerçekten çalışır hale getirme süreci kara büyü gibi hissettiriyor. Arayüzün her köşesi, beklediğiniz gibi çalışıp çalışmayacağı belli olmayan anlaşılmaz büyüler gibi ve bu kadar önemli bir şeyi yönetmek için korkunç bir yöntem
Bu bölümün kılavuzu da zayıf; sadece dar kullanım senaryolarında aşağı yukarı nasıl çalışması gerektiğini anlatıyor. Beklendiği gibi olmazsa deneme yanılmayla neyi yanlış yaptığınızı bulmanız gerekiyor ve doğru yöntemin ne olduğu hâlâ netleşmiyor. Karmaşık kullanıcı izinlerine sahip bir DB'yi migrate etmek gerçekten çok zor
Sanırım bir ay kadar ayırıp bir cookbook yazmam gerekecek. Bir kişinin bile ona bakıp ağlayarak uykuya dalmasını engelleyebilirse buna değer
Bir tabloda
SELECTyapmak için Database üzerindeCONNECT, Schema üzerindeUSAGEgerekir ve bunlar Schema sahibine örtük olarak verilir. Ayrıca Table üzerindeSELECTgerekir ve bu da tablo sahibine örtük olarak verilirİzinleri görmek için
grantee=privilege-abbreviation[]/grantor:biçimindeki ACL girdilerini anlamanız gerekir. Database izinleri\l+, Schema izinleri\dn+, Table izinleri\dp+ile görülebilirİzin listesi burada) bulunuyor. Örneğin
user=arwdDxt/postgres,postgresrolünün kullanıcıya tüm izinleri verdiği anlamına gelirBir nesnenin
granteesütunu boşsa bu, varsayılan sahip izinleri yani tüm izinler anlamına gelebilir ya da var olan tüm rolleri ifade edenPUBLICrolüne verilmiş izinleri gösterebilir. Örnek:=r/postgrespublicSchema kullanılırsa iş daha da karışır. Schema üzerindeCREATEizni bulunduğundan, veriyi sorgulayan aynı kullanıcıyla tablo oluşturursanız sahiplik izinleri varsayılan olarak eklenir ve tablo hemen sorgulanabilirPostgres rolleri hakkında ciddi bir cookbook yazıp bunun için Kickstarter benzeri bir şey açarsanız ilk destek verenlerden biri olurum
Ama bir kez çalışınca gerçekten büyü gibi hissettirdi ve ilgili mekanizmaların kendisi şaşırtıcı derecede basitti
Üretim ortamında Schema migration çalıştırıyorsanız
lock_timeoutkullanmalısınızDış anahtarlı bir tabloyu silmek ya da bir dış anahtarı kaldırmak gibi görünüşte zararsız ve testte neredeyse anında biten değişiklikler bile, trafiği yoğun üretim DB'sinde mevcut transaction'lar veya autovacuum yüzünden kilit çakışmalarına yol açabilir
O
ALTER, ilk transaction'ın kilidini beklerkenACCESS EXCLUSIVEkilidi almaya çalışır ve sonra kilitli tabloya yönelik tüm sorgular bloke olurBelli ölçekte Postgres işletiyorsanız bu tür çakışmalar sadece zaman meselesidir.
lock_timeoutayarlanırsa migration, diğer tüm sorguları bekleterek sonsuza kadar kilit beklemek yerine süre dolunca başarısız olurstatement_timeout, kilit bekleme süresini de kapsadığı için yoğun kullanılan tablolara etkisini daha iyi tahmin etmenizi sağlarZaman aşımını 5 saniye yaparsanız toplam kesintinin en fazla 5 saniye olacağını bilirsiniz ve sonrasında transaction'lar devam eder. Yalnızca
lock_timeoutkullanırsanız kilidi aldıktan sonra işlemin ne kadar süreceğini kontrol edemezsiniz; eşzamanlı trafik yüzünden hızlı da olabilir yavaş daSorguyu analiz edip ne tür bir kilit alacağını söyleyen iyi bir yöntem olup olmadığını merak ediyorum. Emin olmadığımda hep belgeleri yeniden okumaya dönüyorum
ACCESS EXCLUSIVEkilidi alıp beklemiyor; kilit kuyruğu yüzünden bekliyorALTER,ACCESS EXCLUSIVE'den daha düşük bir kilidin serbest kalmasını bekliyorALTERhiç çalışmayabilir. İlgili tabloda yeterince trafik varsa bu olabilirBöyle durumlarda, uygulama toparlanabiliyorsa,
ALTER'i engelleyen diğer aktif sorguları öldürmenin en iyi seçenek olduğunu düşünüyorumFly.io'nun Safe Migrations in Ecto rehberine haftada birkaç kez bakıyorum. Ecto, Elixir'in DB adaptörü
Varsayılan migration'ın yeterli olup olmadığını ya da daha karmaşık bir sürece gerek duyulup duyulmadığını hızlıca anlamak için çok faydalı bir başvuru kaynağı
https://fly.io/phoenix-files/safe-ecto-migrations/
Postgres indekslerinde acemiyken beni en çok şaşırtan şey, UNIQUE indeksin ek kilitler nedeniyle eşzamanlı sorgu sonuçlarını etkileyebilmesiydi
INSERT INTO foo (bar) (SELECT max(bar) + 1 FROM foo);gibi bir sorgu varsayılan modda aynı anda çalıştırılırsa, yinelenenbardeğerleri ekleyebilir. Bunun nedeni, bir transaction’ın başka bir transaction’ın oluşturduğu yeni en büyük değeri göremeyebilmesidirUNIQUE indeks eklendiğinde “kaybeden” transaction’ın constraint hatası alacağını düşünürdüm, ama gerçekte iki transaction da başarılı oluyor ve race condition da ortadan kalkıyor
=# INSERT INTO foo (bar) (SELECT max(bar) + 1 FROM foo);ERROR: duplicate key value violates unique constraint "foo_bar_idx"DETAIL: Key (bar)=(2) already exists.CONCURRENTLYile oluşturup doğrulanmamış bir UNIQUE constraint yaratarak bunu kesintisiz yapmak mümkünBu constraint yalnızca yeni
INSERT/UPDATEişlemlerine uygulanır. Sonrasında constraint üzerindeVALIDATEçalıştırılırsa tam bir UNIQUE constraint haline gelirBunun yaygın bir durum olduğuna katılıyorum, ancak sorun Postgres’ten çok genel olarak yazılım geliştirmede
Bu tuzaklar yüzünden, kesintisiz schema migration otomasyonunu hedefleyerek Reshape [0]’ı geliştirdim
Tüm sorunlardan kaçındığını söyleyemem, ama bunu hedefleyen yeni bir ürün geliştiriyorum. Bu alanla, özellikle de Postgres’le ilgileniyorsanız iletişime geçmek isterim: fabian@reshapedb.com
[0] https://github.com/fabianlindfors/reshape
Sık gördüğüm bir başka hata da bir tabloyu kopyalarken indeksleri atlamak
CREATE TABLE SELECT * FROM WHERE <>bu şekilde çalışmaz. İnsanlar yedek tablo oluştururken ya da toplu silme yapmaya çalışırken bunu sık yapıyorAmaç, DB yedeğinden ve WAL’dan geri dönmek zorunda kalmamak için, muhtemelen hiç kullanılmayacak ama anında hazır bir veri kopyasına sahip olmaktır. İndeks oluşturmak sunucu zamanı ve disk alanı israfıdır
İşler sarpa sararsa ya da gerçekten gerekirse, o indeksler daha sonra oluşturulabilir
“Case 2.
IF [NOT] EXISTSkötü kullanımı” bölümü iyi bir kötü kullanım örneği göstermiyorAyrıca pratikte bunu kullanmak gerçekten doğru. Temiz, basit ve gizli tuzakları yok. Yalnızca birkaç tablo varsa schema migration aracı gereksiz yük olur
Kötü verinin üstüne yara bandı yapıştırmak sorunu çözmez, sadece gizler. Sorunun türüne bağlı olarak ileride beklenmedik bir şekilde, en kötü zamanda patlayabilir
Bu durumda “kötü veri”, var olması gerekirken olmayan ya da olmaması gerekirken var olan tablo, kolon ve view’lerdir. Henüz var olmaması gereken bir tablo neden var? Silme işlemi mi başarısız oldu? Mevcut tablonun schema’sı doğru mu? Aynı migration yanlışlıkla iki kez mi çalıştı?
Her migration’dan sonra schema tam olarak doğru durumda olmalıdır. Eğer migration içinde
IF [NOT] EXISTSvarsa, bu önceki migration’dan sonra schema’nın tam doğru durumda kalmadığı anlamına gelir. Schema durumundan emin olmamak iyi değildirZaten var olan bir tablonun kolonu, migration’ın oluşturmaya çalıştığından farklıysa ne olacak?
IF EXISTSmigration’ın başarılı görünmesini sağlar ama schema kötü durumda kalır. Böyle durumlarda migration’ın hızlıca başarısız olması daha iyidirint4’ü vekil birincil anahtar olarak kullanma kısmına küçük bir itirazım varÖnemli olan tablo boyutu değil de indeks boyutu değil mi? Tablo boyutunda zaten 23 baytlık header ve hizalama padding’i var, bu yüzden 4 baytlık farkın etkisi az. Ama belleğe daha fazla indeks sığdırılabiliyorsa bunun faydası olabilir. İndeks girdilerinde 8 baytlık bir header var
Ayrıca örnekteki 1 milyar satır
int4’ün maksimum değerine fazla yakın, bu da tedirgin ediciYine de yazı harika
Bu, diskteki 8KB’lık sayfanın RAM’de 8KB’dan büyük olabileceği anlamına mı geliyor?
Bunun yalnızca tablo satırı verisinin çalışma belleği üzerindeki etkisiyle ilgili olduğunu düşünüyorum. Yine de önemli. Özellikle Postgres, satırlar rastgele sırada olduğu için range query yerelliğinde korkunç. Ama bunun belirleyici bir içgörü olduğunu da düşünmüyorum
DB ile ilgili sorunlardan büyük ölçüde korunmuş bir geliştiriciyim. Django içinde migration oluşturmayı, model tabloları yaratmayı ve ORM ile sorgu yapmayı biliyorum ama arka planda olan pek çok şey kara büyü gibi geliyor
Şimdi bir şirket kurarken bu tür sorunlarla karşılaşıp bunları tek başıma çözmek zorunda kalmaktan endişe ediyorum. Geliştirme ortamında ne yapılması gerektiğini öğrenmek için buna nasıl yaklaşmalıyım?
Postgres’i seviyorum ama yerleşik bir toplu update/delete yöntemi olmamasından gerçekten nefret ediyorum
En sinir bozucu tarafı bu ve duvara tosladığım her seferde neredeyse her ay batcher’ı yeniden yazmak zorunda kalıyorum