GitLab'ın Postgres Şema Tasarımı Üzerine Notlarım (2022)
(shekhargulati.com)GitLab Postgres Şema Tasarımı Üzerine Notlarım
- GitLab'in Postgres şemasını inceleyerek, kendi tasarladığınız şemayla karşılaştırmak ve GitLab'in şema tanımından iyi uygulama örnekleri öğrenmek istedim.
- GitLab, açık kaynaklı bir DevOps platformudur ve GitHub'a bir alternatiftir; kendi başına barındırma imkanı sunar.
Doğru Birincil Anahtar Türü Kullanmak
- Veritabanı küçükken fark edilmese de, büyüyünce birincil anahtarlar depolama alanını, yazma hızını ve okuma hızını etkiler.
- GitLab, 573 tablodan 380'inde
bigserialbirincil anahtar türünü, 170'ındaserial4türünü, kalan 23'ünde ise bileşik birincil anahtarları kullanır.
İç ve Dış Kimliklerin Kullanımı
- Birincil anahtarları dış dünyaya açmamak iyi bir uygulamadır.
- GitLab,
issues,ci_pipelines,deploymentsveepicsgibi tablolarda hem dahili kimlik (id) hem de harici kimlik (iid) kullanır.
text Veri Tipi ve CHECK Kısıtlama Kullanımı
- GitLab şeması hem
character varying(n)hem detexttürlerini kullanır, ancaktexttürünü daha sık kullanır. texttürünün bir uzunluk kısıtlaması yoktur; uzunluk kısıtlamasıCHECKkullanılarak tanımlanır.
Adlandırma Kuralları
- Tüm tablolar çoğul adlandırılır ve ad alanı sağlamak için modül önekleri kullanılır.
- Tablo ve sütun adları
snake_casekurallarını takip eder.
Zaman Damgalarında Saat Diliminin Kullanımı
- GitLab hem
timestamp with timezonehem detimestamp without timezonetürlerini kullanır. - Sistem işlemleri için
timestamp without timezone, kullanıcı işlemleri içintimestamp with timezonekullanılır.
Yabancı Anahtar Kısıtları
- GitLab, çoğu tabloda yabancı anahtar kısıtları kullanır; ancak
audit_events,abuse_reports,web_hooks_logs,spam_logsgibi bazı tablolarda kullanılmaz.
Büyük Tablolar için Bölümleme
- GitLab, sorgu performansını artırmak için boyutu büyüyebilecek tabloları bölümlemeye tabi tutar.
Trigramlar ve LIKE Aramayı Desteklemek için gin_trgm_ops Kullanımı
- GitLab, verimli arama yapmak için GIN (Generalized Inverted Index) dizinini kullanır.
jsonb Kullanımı
- GitLab şeması, birçok tabloda
jsonbveri türünü kullanır.
Diğer İpuçları
- Değiştirilebilir tablolarda
updated_atgibi denetim alanları kullanılır; değiştirilemeyen günlük tablolarında ise kullanılmaz. - Enumlar, alan tüketimini azaltmak için
character varyingyerinesmallintolarak depolanır.
GN⁺'in Yorumu:
- GitLab'in şema tasarımı, veritabanı tasarımı hakkında önemli içgörüler sunar ve özellikle büyük ölçekli sistemler için şema optimizasyonu konusunda kritik dersler içerir.
- GitLab'in açık kaynak olması, bu şema tasarım kararlarının diğer geliştiricilere kendi projelerinde uygulayabilecekleri pratik örnekler sunmasını sağlar.
- GitLab şemasından öğrenilebilecek en önemli nokta, veri türü seçimi, dizinleme stratejisi, bölümleme ve yabancı anahtar kısıtlarının kullanımı gibi veritabanı performansı ile bakımını doğrudan etkileyen unsurların dikkatlice değerlendirilmesi gerektiğidir.
1 yorum
Hacker News yorumları
Birincil anahtarları dışarıya açmama pratiğinin neden gerekli olduğu konusunda şüpheliyim. İsteklerin zaten kimlik doğrulamasından geçmesi gerekiyorsa, ID tahmin etmeyi engellemenin ne değer kattığını da bilmiyorum.
Yalnızca tahmin edilen bir ID ile kimlik doğrulama/yetkilendirme olmadan faydalı bir şey yapılabiliyorsa, zaten başka bir yer ciddi biçimde bozulmuş demektir; şemaya gereksiz karmaşıklık eklemek yerine o kısma odaklanmak gerekir. Rakiplerin müşteri sayısını tahmin etmesini engellemek gibi rekabet istihbaratını gizleme bir ölçüde değerli olabilir, ama GitLab’ın bunu çok dert ettiğini sanmıyorum. GitLab’ın
id + iidkararı, dahili ID tahminini engellemekten çok sorgu performansı gereksinimlerinden kaynaklanmış gibi görünüyor.Kullanıcıya UUID açtıysanız, aynı açık olsa bile saldırganın UUID’yi tutturması gerekeceğinden iş çok daha zorlaşır ve ikincil bir kaynağa ihtiyaç duyulabilir. Veri sızıntısı olsa bile müdahale için zaman kazanılır ve sızıntı miktarı da hesaplanabilir. Buna karşılık sıralı ID kullanılıyorsa sorun anında tam kapsamlı bir sızıntıya dönüşebilir ve kişisel veri denetim kurumlarına zorunlu bildirim gerektiren büyük bir olay haline gelebilir. Bunlar işe yaramaması gereken derinlemesine savunma önlemleri, ama pratikte bu şekilde kırılmış berbat yazılımlar var.
Issue tablosunun birincil anahtarı olan
iddışarıya açılırsa, bir projede issue oluştururken 1’den başlamadığı için GitLab genelinde ne kadar issue olduğunu kolayca tahmin edebilirsiniz.Elbette yalnızca tahmin edilmiş bir ID ile kimlik doğrulama/yetkilendirme olmadan bir şey yapılabiliyorsa bu büyük bir sorundur. Ama o noktada başka katman yoksa oyun zaten bitmiş demektir. Hatalar önceden haber vermez; özellikle de incelikli hatalar. Böyle bir hata ortaya çıktığında en azından ID’leri tahmin edilemez hale getirdiyseniz, sistemdeki tüm kullanıcı hesaplarına kolayca erişilmesini önlediğiniz için buna şükredersiniz.
Elbette öncelik doğru erişim denetimini güvenceye almak olmalı; ancak tahmin edilemeyen ID’ler korkunç bir felaket ile kıl payı atlatılmış bir olay arasındaki farkı yaratabilir. UUID uygun değilse otomatik artan veritabanı ID’si kullanıp bunu şifrelemek de mümkün; uygun bir yazılım katmanı varsa şifrelenmiş ID’ler de neredeyse otomatik çalışır.
128 milyon herkese açık deponun büyük çoğunluğu başka depoların fork’u ve yalnızca ana depoya pull request açmak için var; dolayısıyla yanlışlık yapılmadıkça issue’ları olmayacak gibi.
Küçük oyuncak projelerde veya kısa sürede terk edilen projelerde de hiç issue olmayabilir ya da çok az olabilir. Yüzlerce, binlerce issue’su olan projeler kesinlikle vardır; ancak 128 milyon deponun genel ortalaması epey düşük olup 2 milyar sınırının altında kalması muhtemel. Yine de o tabloda 4 baytlık tip, daha doğrusu 31 bit kullanmanın github.com dâhil bazı organizasyonlar için saatli bomba olduğuna katılıyorum.
https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
PRIMARY KEY(repo_id, issue_id)gibi temel bir gereksinim ActiveRecord içinde gereksiz yere karmaşıklaşıyor; tek bir birincil anahtar kolonu isteyen ActiveRecord’a uymak için benzersiz anahtar ile ayrı bir birincil anahtarı birlikte kullanan geçici çözümler gerekiyor. UUID birincil anahtar da bir geçici çözüm gibi görünüyor, ancak(repo_id, issue_id)çifti için benzersiz kısıt yine de gerektiğinden veritabanı boyutu ve ek yük artıyor. Daha geniş açıdan bakınca Ruby on Rails’in tekil model-controller-view katmanlarına dayanan monolitik MVC yapısının da uygulama büyüdükçe ölçeklenebilirlik ve sürdürülebilirlik sorunları yarattığını; MVC’nin modüler veya bileşen tabanlı yapılara daha uygun olduğunu düşünüyorum.Hem self-hosted hem de bulut sunan ürünler çoğu zaman müşteri bazlı veritabanlarını tercih ederdi. Çünkü barındırma türünden bağımsız olarak aynı sorguları kullanacak şekilde kod tabanını paylaşmak çok daha basit hale gelir. Müşteri bazlı veritabanında bu tür kullanım sınırlarına yaklaşmak neredeyse hiç olmaz; yaklaşılıyorsa self-hosted daha uygun olacaktır.
int’tenbigint’e geçirmek mümkün. Biraz hazırlık ve özel kod gerekiyor ama kesintisiz yapılabiliyor.Büyük migrasyonları genel olarak şu prosedürü izleyerek yönetiyoruz ve biraz değiştirerek kullanıyoruz: http://zemanta.github.io/2021/08/25/column-migration-from-in...
Yabancı anahtarlar, indeksler ve kısıtlar genel olarak süreci zorlaştırıyor ama imkânsız değil. Benim durumumda veri migrasyonu birkaç saat sürdü, ama hızlı olması gerekmiyordu. Bildiğim kadarıyla GitLab’ın, sürüm yükseltme sırasında herhangi bir noktada çalışacak şekilde yükseltme sonrası işleri yürüten araçları var.
UUID sütunlarının depolama boyutuna dair argüman pek ikna edici değil. Tabloda 5 başka sütun varsa 128 bit ile 64 bit arasındaki fark büyük sayılmaz
Daha önemli endişe performans. UUIDv4 yaygın biçimde destekleniyor, ancak tamamen rastgele olduğu için indeks performansı açısından ideal değil. UUIDv7[0], Snowflake[1]’e daha yakın; zamansal yerellik taşıyor ama uygulamaları daha az yaygın. Ayrı bir yaklaşım olarak
bigserialkullanıp anahtarı şifrelemek de var: https://github.com/abevoelker/gfc64Ancak bu yöntemde 1) gizli değer döndürülemez ve 2) bir kez sızarsa herkes tablo boyutunu Fermi tahminiyle çıkarabilir. Genel ID ile dahili ID’yi ayırmak zahmetli; genel ID UUIDv4 ise performanstan da ödün verilir. UUIDv7’nin en çok koşulu karşılayan çözüm olduğunu düşünüyorum
[0]: https://uuid7.com/
[1]: https://en.wikipedia.org/wiki/Snowflake_ID
id’nin yabancı anahtar olarak kullanıldığı tüm yerler ve bu yabancı anahtar sütunları için gereken indeks boyutu da sorunKullanıcı ID’si gibi, veritabanı genelinde onlarca, yüzlerce yabancı anahtarın referans verebileceği bir değeri düşünmek yeterli
Veritabanı performansında üç aşama var. 1) Hem indeksler hem veriler belleğe sığar. 2) İndeksler belleğe sığar ama veriler sığmaz. 3) Ne indeksler ne de veriler belleğe sığar. 1 iyidir; ama değilse 2’yi ne pahasına olursa olsun korumak gerekir. İndeks boyutunun iki katına çıkması bunu daha zorlaştırır
typedef void*gibi temel bir işaretçi olarak düşünebilirsiniz. Boyutu; bellek ve disk kullanımını, işleme hızı darboğazlarını, join ve lookup işlemlerinin en derin iç döngülerinde anahtar karşılaştırmaya harcanan CPU zamanını etkileyerek genel performansa yansırx86-64 CPU’lar ilk çıktığında 64 bit işaretçilere geçişin performans etkisi o kadar büyüktü ki x32/ilp32 ortaya çıktı; .NET’in hâlâ “prefer 32-bit” seçeneğini varsayılan tutmasının nedeni de bu. Veritabanı birincil anahtarı olarak 128 bit UUID kullanmak korkunç bir hata
Örneğin bir videoyu halka açılmadan bir ay önce yükleyip izleyicilerin bunu bilmemesini isteyebilirsiniz
URL’lerde, e-postalarda vb. gösterirken değeri şifrelemek de mümkün: https://wiki.postgresql.org/wiki/Pseudo_encrypt
Böylece sıralı indekslerin avantajlarının çoğu korunurken anahtar değiştirmek de mümkün olur. Ancak anahtarı değiştirirseniz yer imleri kırılır, geçmiş e-postalarda gönderilmiş bağlantılar geçersiz olur ve fiilen her şeyi yeniden adlandırmışsınız gibi bir etki kalır
Küçük bir ayrıntı gibi görünebilir ama
textilevarcharbölümüne değinmek istiyorumYazar, var olmayan bir performans farkını kanıtlamaya çalışmak için epey söz sarf ettikten sonra “iki tip arasında büyük bir performans farkı yok” sonucuna varıyor. Bu konu uzun zaman önce kapanmış bir konu; mesele “büyük değil” değil, “yok”. PostgreSQL wiki’si[1] çok iyi bir neden olmadıkça
textkullanılmasını açıkça söylüyor; dokümantasyon[2] da “birçok amaç içincharacter varying,textüzerinde bir domain gibi davranır” diyor ve yeşil Tip kutusunda “bu üç tip arasında performans farkı yoktur” ifadesini kullanıyor. Dolayısıyla GitLab’in çoğunluklatextkullanması, dokümanları okuyup şemasını yarım yamalak “taşınabilir” bir şema yerine PostgreSQL’e göre tasarladığı anlamına geliyor gibi görünüyor[1] https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use...
[2] https://www.postgresql.org/docs/current/datatype-character.h...
varchar(300)’üvarchar(200)yapmak için tüm satırların yeniden yazılması gerekir; oysatextsütunundaki kısıtı güncellemek temelde neredeyse bedavadır ve yalnızca mevcut değerlerin yeni kısıtı karşılayıp karşılamadığını doğrulayan tam tablo taraması gerekir. Yazıda datexttipi veCHECKkısıtı kullanıldığında, uzunluk kontrolü varsa şema evriminincharacter varyingveyavarchar(n)kullanımına göre daha kolay olduğu söyleniyorYabancı anahtarlar pahalıdır sözü sık tekrarlanır ama bunun benchmark’ı nadiren görülür
Yanlış uygulamanın birçok yolu var; ama sonuçta yığının bir yerinde bütünlük zaten zorlanıyor. Bunu yeniden uygulamak yerine veritabanından yararlanmak bilgi ve deney gerektirir ve çoğu zaman büyük kazaları önler
GitLab ile GitHub arasındaki performans farkını birinin derleyip derlemediğini ya da buna dikkat edip etmediğini merak ediyorum.
İkisi de Rails tabanlı uygulamalar ama genel sayfa yükleme süreleri açısından GitLab bana GitHub’a kıyasla berbat geliyor.
Chrome ve GitHub, kullanıcı deneyimini mahvetseler bile her türlü numaraya başvurur. Örneğin şirket GitHub’ında bir merge diff’ini açıp
Ctrl File arama yaptığımda sonuç çıkmayınca sonrakine geçerek Git geçmişini elle didik didik ettiğimi, ancak 100. diff’te en önemli dosyanın derinlerde saklı olduğunu fark ettiğimi hatırlıyorum. Muhtemelen birinin sayfa yükleme metriklerini tutturup terfi almasını kolaylaştırdığı içindi.GitLab’ın kültürünü ve performansı hafife alışını anlamaya yardımcı olabilecek eski bir çalışanın yazısı burada: https://news.ycombinator.com/item?id=39303323
Ben GitLab’ı performans sorunlarını hissedecek kadar yoğun kullanmıyorum ama bu yazının faydalı olabileceğini düşünüyorum.
CI değişkenleri
CI_PIPELINE_IIDveCI_MERGE_REQUEST_IIDiçindeki fazladan I harfinin ne anlama geldiğini hep merak etmiştim.Bunun veritabanıyla ilgili bir tercih olduğunu tahmin ediyordum; bu yazı bunu doğruluyor.
“1 kentilyon, 1.000.000.000 milyara eşittir” ifadesini görünce genelde yalnızca
int32ileint64arasında seçim yapmamız epey garip geliyor. Yaklaşık 1 trilyon kardinaliteyi destekleyen 5 baytlık bir tamsayı tipi olmalıymış gibi.Otomatik artan ID kullanmamak makul olabilir, ama iç kullanım ve dış kullanım için 2 ayrı ID tutmanın avantajından emin değilim.
Sütun ve indeks sayısı artıyor, her zaman önce sorgu yapmak gerekiyor; ayrıca iç anahtarı değiştirip dış anahtarı değiştirmeyeceğiniz bir güvenlik senaryosu da aklıma gelmiyor. Kaçırdığım bir şey mi var?
PostgreSQL’in yerel UUID v4 tipini
bigserialyerine kullanmanın tablo boyutunu %25 artırdığı ve ekleme oranınıbigserialın %25’ine düşürdüğü söyleniyor; UUIDv4’ün neden bu kadar kötü olduğunu merak ediyorum.UUID dediğimiz şey sadece 128 bitlik bir sayı değil mi? Üretme maliyeti aşırı mı yüksek, yoksa ne oluyor?
Bu yüzden UUIDv4 sütununu indeksleme işi yavaşlar; UUIDv6 ve UUIDv7’nin geliştirilme motivasyonu da budur.
Daha büyük sorun ekleme oranı. UUID’de ekleme oranı kullanılabilir RAM miktarıyla sınırlanır. Otomatik artan tamsayıda böyle değildir. Tamsayı zamanla korelasyonludur ama UUID4 rastgeledir; ölçek büyüdükçe performans özellikleri temelden değişir. Küçük tablolarda ekleme cezası neredeyse ihmal edilebilir, ancak B-tree indeksinin boyutu bellek sınırına dayandığında PostgreSQL UUID B-tree’sinin tamamını bellekte tutamaz ve disk sayfası değiştirmeye dayanır. Otomatik artan tamsayılarda zamansal olarak yakın satırlar aynı indeks sayfasını kullandığından aynı yük altında diske gitmeye gerek kalmaz. Bu ölçeğe ulaşıldığında fark sabit bir %25 düşüş değil, 25 katlık bir performans uçurumudur; şema migrasyonu dışında tek çare daha fazla RAM almaktır.
UUID, sıralı olmayan çok sayıda yazma üretir ve bu da sayfa şişmesine yol açar.
Toplu eklemelerin yerelliği de daha sonra sorgulama sırasında kötüleşir; ilgili kayıtları rastgele bulmak gerekir. Sonuçta maliyet hem ekleme anında hem de sonraki seçimler sırasında ödenir.