1 puan yazan GN⁺ 2024-02-18 | 1 yorum | WhatsApp'ta paylaş

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 bigserial birincil anahtar türünü, 170'ında serial4 tü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, deployments ve epics gibi 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 de text türlerini kullanır, ancak text türünü daha sık kullanır.
  • text türünün bir uzunluk kısıtlaması yoktur; uzunluk kısıtlaması CHECK kullanı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_case kurallarını takip eder.

Zaman Damgalarında Saat Diliminin Kullanımı

  • GitLab hem timestamp with timezone hem de timestamp without timezone türlerini kullanır.
  • Sistem işlemleri için timestamp without timezone, kullanıcı işlemleri için timestamp with timezone kullanı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_logs gibi 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 jsonb veri türünü kullanır.

Diğer İpuçları

  • Değiştirilebilir tablolarda updated_at gibi denetim alanları kullanılır; değiştirilemeyen günlük tablolarında ise kullanılmaz.
  • Enumlar, alan tüketimini azaltmak için character varying yerine smallint olarak 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

 
GN⁺ 2024-02-18
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 + iid kararı, dahili ID tahminini engellemekten çok sorgu performansı gereksinimlerinden kaynaklanmış gibi görünüyor.

    • Doğru, ancak ID’nin tahmin edilebilir olması bir güvenlik açığını korkunç hale getirebilir ya da çok daha kötüleştirebilir.
      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.
    • Yazıda da geçtiği gibi, bu güvenlikten çok rekabet istihbaratı meselesine yakın. Basit otomatik artan ID, tablodaki toplam kayıt sayısını veya büyüme oranını açığa çıkarır.
      Issue tablosunun birincil anahtarı olan id dış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.
    • Güvenlik tiyatrosu ifadesi fazla kötüye kullanılıyor. Güvenlik birden çok katmandan oluşabilir ve oluşmalıdır; kimlik doğrulama gibi bir katman kırıldı diye geri kalan her şeye kolayca erişilememelidir.
      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.
    • Erişim denetiminde de hatalar olur. Tahmin edilemeyen ID’ler, bu hatalardan bazılarının suistimal edilmesini çok daha zor hale getirir.
      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.
    • Küçük bir terim farkı ama bu, genelde şirket içi veri kullanımını ifade eden “business intelligence”tan ziyade rekabet istihbaratı olarak adlandırılmalı gibi görünüyor. https://en.wikipedia.org/wiki/Competitive_intelligence
  • 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.

    • Şu anda bile 362.107.148 depo ve 818.516.506 benzersiz issue ve pull request ile hâlâ sınırın altındalar.
      https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
    • GitHub’ın Rails’ten uzaklaşma kararında ActiveRecord’un büyük bir eksiğinin, yani bileşik birincil anahtar desteği olmamasının da rolü olduğunu düşünüyorum.
      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.
    • GitLab Cloud’un çok kiracılı veritabanı mı kullandığını, yoksa kullanıcı/müşteri/organizasyon bazlı veritabanları mı kullandığını kesin olarak bilen var mı merak ediyorum.
      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.
    • O saatli bomba, 11 saniyelik bir migrasyonla etkisiz hale getirilebilen bir bomba.
    • Birincil anahtarı int’ten bigint’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 bigserial kullanıp anahtarı şifrelemek de var: https://github.com/abevoelker/gfc64
    Ancak 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

    • Sorun yalnızca o tek sütunun boyutu değil; o id’nin yabancı anahtar olarak kullanıldığı tüm yerler ve bu yabancı anahtar sütunları için gereken indeks boyutu da sorun
      Kullanıcı ID’si gibi, veritabanı genelinde onlarca, yüzlerce yabancı anahtarın referans verebileceği bir değeri düşünmek yeterli
    • Sorun, diğer 5 sütunun indekslenmemiş olması
      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
    • Veritabanının birincil anahtarını 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ır
      x86-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
    • UUIDv7 de her derde deva değil. Çoğu durumda bir kaynağın oluşturulma zamanını sızdırmak istemezsiniz
      Örneğin bir videoyu halka açılmadan bir ay önce yükleyip izleyicilerin bunu bilmemesini isteyebilirsiniz
    • Bu yaklaşımın başka varyasyonları da var: https://pgxn.org/dist/permuteseq/
      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 text ile varchar bölümüne değinmek istiyorum
    Yazar, 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 text kullanılmasını açıkça söylüyor; dokümantasyon[2] da “birçok amaç için character 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ğunlukla text kullanması, 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...

    • Asıl, saklanan dizge uzunluğundaki değişime göre şemayı migrate etmeniz gerektiğinde ciddi bir performans farkı ortaya çıkar
      varchar(300)’ü varchar(200) yapmak için tüm satırların yeniden yazılması gerekir; oysa text sü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 da text tipi ve CHECK kısıtı kullanıldığında, uzunluk kontrolü varsa şema evriminin character varying veya varchar(n) kullanımına göre daha kolay olduğu söyleniyor
  • Yabancı 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.

    • Birkaç yıl önce GitLab kullandığımda büyük pull request’lerde istemci tarafı performans sorunları çok ciddiydi. GitHub da ideal değil ama yine de kabul edilebilir düzeyde idare ediyor.
    • GitHub’ı karşılaştırmak, Chrome’u diğer tarayıcılarla, hatta Chromium tabanlı tarayıcılarla karşılaştırmaya benziyor.
      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 F ile 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.
    • GitHub, geçen yılki iki kesinti dışında genel olarak istikrarlı ve çoğu zaman oldukça hızlı. Öyle olmasaydı klavye kısayollarını kullanmazdım.
      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_IID ve CI_MERGE_REQUEST_IID iç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 int32 ile int64 arasında seçim yapmamız epey garip geliyor. Yaklaşık 1 trilyon kardinaliteyi destekleyen 5 baytlık bir tamsayı tipi olmalıymış gibi.

    • Değerleri bit bit sıkı paketlemiyorsanız, 2’nin kuvveti olmayan bir boyut seçmek mantıklı değil.
  • 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?

    • Proje bazında bir şey yaptığınızda zaten gereken bilgi elinizde oluyor. Ayrıca her projenin issue’larının 2 trilyon 700 milyar 300 milyon 5 milyon 717 bin 325 gibi bir sayıdan başlamasındansa 1’den başlaması kullanıcı açısından daha dostça.
  • PostgreSQL’in yerel UUID v4 tipini bigserial yerine 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?

    • UUIDv4 tamamen rastgeledir; B-tree indeksleri ise makul bir sıralamaya sahip, “sağa doğru eğilimli” değerler bekler.
      Bu yüzden UUIDv4 sütununu indeksleme işi yavaşlar; UUIDv6 ve UUIDv7’nin geliştirilme motivasyonu da budur.
    • Boyuttaki %25 artış doğru, ama satır başına 8 baytlık küçük ve öngörülebilir doğrusal bir artış. Satırdaki diğer verilerle karşılaştırıldığında çok endişe edilecek bir şey değil.
      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.
    • Bunun B-tree’den kaynaklandığını düşünüyorum. B-tree ve sayfalar, çoğunlukla yalnızca son sayfa kullanıldığında daha iyi çalışır.
      UUID, sıralı olmayan çok sayıda yazma üretir ve bu da sayfa şişmesine yol açar.
    • Sıralama düzeni rastgele dağıldığında B-tree’nin önbellek yerelliği kötüleşir. Ekleme son sayfaya gitmek yerine oraya buraya dağılır.
      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.