Meine Notizen zum GitLab-Postgres-Schema-Design
(shekhargulati.com)Meine Notizen zum GitLab-Postgres-Schema-Design
- Indem ich mir das Postgres-Schema von GitLab anschaue, kann ich es mit meinem eigenen Schema vergleichen und Best Practices aus GitLabs Schemadefinition ableiten.
- GitLab ist eine Open-Source-DevOps-Plattform, eine Alternative zu GitHub, die selbst gehostet werden kann.
Richtige Verwendung von Primärschlüsseltypen
- Wenn eine Datenbank klein ist, fällt das kaum auf, mit dem Wachstum wirken sich Primärschlüssel jedoch auf Speicherplatz, Schreibgeschwindigkeit und Lesegeschwindigkeit aus.
- Von 573 Tabellen verwendet GitLab
bigserialals Primärschlüsseltyp in 380 Tabellen,serial4in 170, und die übrigen 23 nutzen zusammengesetzte Primärschlüssel.
Verwendung interner und externer IDs
- Es ist eine gute Praxis, Primärschlüssel nicht nach außen hin offenzulegen.
- GitLab verwendet in Tabellen wie
issues,ci_pipelines,deploymentsundepicssowohl interne IDs (id) als auch externe IDs (iid).
Verwendung von text-Datentypen und CHECK-Einschränkungen
- Das GitLab-Schema verwendet sowohl
character varying(n)als auchtext, wobei estexthäufiger nutzt. - Der Datentyp
texthat keine Längenbeschränkung; GitLab definiert die Längenbeschränkung stattdessen mitCHECK.
Namenskonventionen
- Alle Tabellen sind im Plural benannt und nutzen Modulnamenpräfixe, um Namespaces bereitzustellen.
- Tabellen- und Spaltennamen folgen der
snake_case-Konvention.
Zeitzonennutzung bei Timestamps
- GitLab verwendet sowohl
timestamp with timezoneals auchtimestamp without timezone. - Für Systemvorgänge wird
timestamp without timezoneverwendet, für Benutzeraktivitätentimestamp with timezone.
Fremdschlüssel-Constraints
- GitLab nutzt Fremdschlüssel-Constraints in den meisten Tabellen, in einigen wie
audit_events,abuse_reports,web_hooks_logsundspam_logsjedoch nicht.
Partitionierung großer Tabellen
- GitLab partitioniert Tabellen, die im Laufe der Zeit groß werden können, um die Abfrageleistung zu verbessern.
Unterstützung von LIKE-Suchfällen mit Trigrammen und gin_trgm_ops
- GitLab nutzt GIN(Generalized Inverted Index)-Indizes, um eine effiziente Suche bereitzustellen.
Einsatz von jsonb
- Das GitLab-Schema verwendet den Datentyp
jsonbin mehreren Tabellen.
Weitere Tipps
- Änderbare Tabellen haben Audit-Felder wie
updated_at, unveränderliche Log-Tabellen dagegen nicht. - Enums werden als
smallintstattcharacter varyinggespeichert, um Platz zu sparen.
GN⁺-Meinung:
- Das GitLab-Schema-Design liefert Einblicke in die Datenbankgestaltung und beinhaltet wichtige Lehren, insbesondere zur Schema-Optimierung für Großsysteme.
- Da GitLab Open Source ist, liefern solche Schema-Entscheidungen praktische Beispiele, die andere Entwickler in ihren eigenen Projekten übernehmen können.
- Was man aus dem GitLab-Schema lernen kann, ist, dass Datentypauswahl, Indexierungsstrategie, Partitionierung und der Einsatz von Fremdschlüssel-Constraints zu Aspekten beitragen, die die Datenbankleistung und Wartbarkeit maßgeblich beeinflussen.
1 Kommentare
Hacker-News-Kommentare
Ich frage mich, warum die Praxis nötig ist, Primärschlüssel nicht nach außen offenzulegen. Wenn Requests ohnehin authentifiziert werden müssen, sehe ich auch nicht, welchen Wert es hat, das Erraten von IDs zu verhindern.
Wenn man allein mit einer erratenen ID ohne Authentifizierung und Autorisierung etwas Nützliches tun kann, ist an anderer Stelle bereits etwas ernsthaft kaputt; darauf sollte man sich konzentrieren, statt dem Schema unnötige Komplexität hinzuzufügen. Competitive Intelligence in dem Sinne, dass Wettbewerber die Kundenzahl nicht abschätzen können, kann einen gewissen Wert haben, aber ich glaube nicht, dass GitLab sich darum besonders stark gekümmert hat. Bei GitLabs Entscheidung für
id + iiddürften eher Anforderungen an die Query-Performance als der Schutz vor dem Erraten interner IDs ausschlaggebend gewesen seinWenn man den Nutzern UUIDs offengelegt hat, müsste ein Angreifer trotz derselben Schwachstelle erst die UUID treffen, was viel schwieriger ist und möglicherweise eine zweite Quelle erfordert. Selbst wenn Daten abfließen, gewinnt man Zeit für eine Reaktion, und die Menge des Abflusses lässt sich einschätzen. Bei sequenziellen IDs hingegen wächst das Problem sofort zu einem vollständigen Leak an und kann zu einem großen Vorfall werden, der den Datenschutzaufsichtsbehörden gemeldet werden muss. Das ist eine Defense in Depth, die eigentlich nicht nötig sein sollte, aber es gibt tatsächlich schreckliche Software, die genau auf diese Weise kompromittiert wurde
Wenn man den Primärschlüssel
idder Issue-Tabelle offenlegt, beginnt er beim Erstellen von Issues in einem Projekt nicht bei 1; dadurch lässt sich leicht abschätzen, wie viele Issues es insgesamt in GitLab gibtNatürlich ist es ein großes Problem, wenn allein mit einer erratenen ID ohne Authentifizierung und Autorisierung etwas möglich ist. Aber wenn es an diesem Punkt keine weiteren Schichten gibt, ist das Spiel schon vorbei. Bugs kündigen sich nicht vorher an, insbesondere subtile Bugs nicht. Wenn so ein Bug auftritt und wenigstens die IDs nicht erratbar sind, wird man dankbar sein, dass nicht alle Nutzerkonten des Systems leicht zugänglich geworden sind
Natürlich sollte man sich zuerst darauf konzentrieren, korrekte Zugriffskontrolle sicherzustellen, aber nicht erratbare IDs können den Unterschied zwischen einer schrecklichen Katastrophe und einem Beinaheunfall ausmachen. Wenn UUIDs nicht passen, kann man auch Auto-Increment-Datenbank-IDs verwenden und sie verschlüsseln; mit einer geeigneten Softwareschicht funktionieren verschlüsselte IDs nahezu automatisch
Von 128 Millionen öffentlichen Repositories ist die Mehrheit Forks anderer Repositories und existiert nur, um Pull Requests an das Haupt-Repository zu stellen; sofern niemand einen Fehler macht, dürften sie keine Issues haben.
Kleine Spielzeugprojekte oder schnell aufgegebene Projekte haben wahrscheinlich ebenfalls keine oder nur sehr wenige Issues. Es gibt sicher Projekte mit Hunderten oder Tausenden Issues, aber der Durchschnitt über alle 128 Millionen Repositories dürfte ziemlich niedrig sein und wahrscheinlich unter der 2-Milliarden-Grenze bleiben. Trotzdem stimme ich zu, dass die Verwendung eines 4-Byte-Typs – genauer gesagt von 31 Bit – für diese Tabelle für einige Organisationen einschließlich github.com eine Zeitbombe ist
https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
Eine grundlegende Anforderung wie
PRIMARY KEY(repo_id, issue_id)wird in ActiveRecord unnötig kompliziert, und um zu ActiveRecord zu passen, das eine einzelne Primärschlüsselspalte verlangt, braucht man den Umweg über einen Unique Key plus einen separaten Primärschlüssel. UUID-Primärschlüssel wirken ebenfalls wie ein Workaround, aber die Unique-Constraint auf das Paar(repo_id, issue_id)ist weiterhin nötig, wodurch Datenbankgröße und Overhead steigen. Allgemeiner betrachtet erzeugt auch die auf einer einzelnen Model-, Controller- und View-Schicht basierende monolithische MVC-Struktur von Ruby on Rails mit wachsender Anwendung Probleme bei Skalierbarkeit und Wartbarkeit; MVC passt meiner Ansicht nach besser zu modularen oder komponentenbasierten StrukturenProdukte, die sowohl Self-Hosting als auch Cloud anbieten, haben häufig Datenbanken pro Kunde bevorzugt. Denn dadurch wird der gemeinsam genutzte Teil der Codebasis deutlich einfacher, weil unabhängig vom Hosting-Typ dieselben Queries verwendet werden können. Bei Datenbanken pro Kunde käme man kaum in die Nähe solcher Nutzungslimits; und falls doch, wäre Self-Hosting wohl geeigneter
intzubigintist möglich. Sie erfordert etwas Vorbereitung und Custom Code, lässt sich aber ohne Downtime durchführen.Im Großen und Ganzen verwalte ich große Migrationen nach diesem Verfahren und nutze es mit kleinen Anpassungen: http://zemanta.github.io/2021/08/25/column-migration-from-in...
Foreign Keys, Indizes und Constraints insgesamt erschweren den Prozess, machen ihn aber nicht unmöglich. In meinem Fall dauerte die Datenmigration einige Stunden, musste aber nicht schnell sein. Soweit ich weiß, hat GitLab Werkzeuge, die Post-Upgrade-Jobs ausführen, sodass sie an jedem Punkt während eines Versions-Upgrades funktionieren
Die Diskussion über die Speichergröße von UUID-Spalten ist wenig überzeugend. Wenn eine Tabelle fünf weitere Spalten hat, ist der Unterschied zwischen 128 Bit und 64 Bit nicht groß
Die wichtigere Sorge ist die Performance. UUIDv4 wird breit unterstützt, ist aber vollständig zufällig und daher für Index-Performance nicht ideal. UUIDv7[0] kommt Snowflake[1] näher und hat zeitliche Lokalität, aber Implementierungen sind noch weniger verbreitet. Ein anderer Ansatz ist,
bigserialzu verwenden und den Schlüssel zu verschlüsseln: https://github.com/abevoelker/gfc64Dieses Verfahren hat jedoch die Nachteile, dass 1) der geheime Wert nicht rotiert werden kann und 2) nach einer einmaligen Offenlegung jeder die Tabellengröße per Fermi-Abschätzung schätzen kann. Öffentliche IDs und interne IDs zu trennen ist umständlich, und wenn die öffentliche ID UUIDv4 ist, opfert man auch Performance. UUIDv7 ist aus meiner Sicht die Lösung, die die meisten Anforderungen erfüllt
[0]: https://uuid7.com/
[1]: https://en.wikipedia.org/wiki/Snowflake_ID
idals Fremdschlüssel verwendet wird, sowie um die Größe der für diese Fremdschlüsselspalten nötigen IndizesMan denke an Werte wie Benutzer-IDs, auf die datenbankweit Dutzende oder Hunderte von Fremdschlüsseln verweisen können
Bei Datenbank-Performance gibt es drei Stufen: 1) Indexe und Daten passen beide in den Speicher. 2) Indexe passen in den Speicher, Daten aber nicht. 3) Weder Indexe noch Daten passen in den Speicher. Stufe 1 ist gut, aber wenn man sie nicht erreicht, muss man Stufe 2 um jeden Preis verteidigen. Eine Verdopplung der Indexgröße macht das schwieriger
typedef void*vorstellen. Seine Größe beeinflusst die Gesamtperformance: Speicher- und Plattenverbrauch, Durchsatz-Engpässe und sogar die CPU-Zeit für Schlüsselvergleiche in den innersten Schleifen von Joins und LookupsAls x86-64-CPUs neu aufkamen, war der Performance-Einfluss des Wechsels auf 64-Bit-Pointer so groß, dass x32/ilp32 entstand; aus demselben Grund setzt .NET bis heute standardmäßig auf „prefer 32-bit“. 128-Bit-UUIDs als Datenbank-Primärschlüssel zu verwenden, ist ein schrecklicher Fehler
Zum Beispiel könnte man ein Video einen Monat vor der Veröffentlichung hochladen wollen, ohne dass das Publikum davon erfährt
Es ist auch möglich, Werte bei der Anzeige in URLs, E-Mails usw. zu verschlüsseln: https://wiki.postgresql.org/wiki/Pseudo_encrypt
So lassen sich viele Vorteile sequenzieller Indizes erhalten, während Schlüsseländerungen möglich bleiben. Ändert man jedoch den Schlüssel, gehen Bookmarks kaputt, in früheren E-Mails versendete Links werden ungültig, und faktisch bleibt derselbe Effekt wie beim Umbenennen von allem
Eine kleine Spitzfindigkeit, aber den Abschnitt
textversusvarcharmöchte ich ansprechenDer Autor verwendet viele Worte darauf, einen nicht existierenden Performance-Unterschied zu belegen, und kommt dann zu dem Schluss, dass es „keinen großen Performance-Unterschied zwischen den beiden Typen“ gebe. Das Thema ist seit Langem geklärt, und es heißt nicht „nicht groß“, sondern „nicht vorhanden“. Das PostgreSQL-Wiki[1] sagt ausdrücklich, man solle
textverwenden, sofern es keinen sehr guten Grund dagegen gibt, und die Dokumentation[2] sagt ebenfalls: „For many purposes,character varyingacts as though it were a domain overtext“; in der grünen Tip-Box steht, dass es zwischen diesen drei Typen keinen Performance-Unterschied gibt. Dass GitLab überwiegendtextverwendet, wirkt daher so, als hätte man die Dokumentation gelesen und das Schema für PostgreSQL entworfen, statt ein halbgares „portables“ Schema zu bauen[1] https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use...
[2] https://www.postgresql.org/docs/current/datatype-character.h...
Um
varchar(300)invarchar(200)zu ändern, müssen alle Zeilen neu geschrieben werden; die Constraint einertext-Spalte zu aktualisieren ist dagegen im Grunde fast kostenlos und erfordert nur einen vollständigen Tabellenscan, um zu prüfen, ob die bestehenden Werte die neue Constraint erfüllen. Auch der Text sagt, dass die Verwendung des Typstextmit einerCHECK-Constraint die Schema-Evolution bei Längenprüfungen einfacher macht alscharacter varyingodervarchar(n)Die Aussage, dass Fremdschlüssel teuer sind, wird oft wiederholt, ist aber eine Behauptung, zu der Benchmarks selten sind
Es gibt viele Arten, sie falsch zu implementieren, aber irgendwo im Stack wird die Integrität ohnehin erzwungen. Wer dafür die Datenbank nutzen will, statt es neu zu implementieren, braucht Wissen und Experimente – und vermeidet damit in der Regel größere Unfälle
Mich würde interessieren, ob jemand die Performance-Unterschiede zwischen GitLab und GitHub zusammengefasst oder genauer beobachtet hat.
Beide sind Rails-basierte Anwendungen, aber insgesamt empfinde ich die Ladezeiten von GitLab-Seiten im Vergleich zu GitHub als furchtbar.
Chrome und GitHub werden alle möglichen Tricks anwenden, selbst wenn sie damit Nutzern schaden. Zum Beispiel habe ich in einem Firmen-GitHub einmal einen Merge-Diff geöffnet, mit
Ctrl Fgesucht, keine Treffer bekommen, mich dann manuell durch die Git-Historie zum nächsten Diff gehangelt und erst beim 100. Diff gemerkt, dass die wichtigste Datei tief darin versteckt war. Vermutlich, weil es für jemanden bequemer war, Seitenlade-Metriken zu erfüllen und befördert zu werden.Hier ist ein Beitrag eines ehemaligen Entwicklers, der helfen kann, GitLabs Kultur und die geringe Wertschätzung von Performance zu verstehen: https://news.ycombinator.com/item?id=39303323
Ich nutze GitLab nicht genug, um die Performance-Probleme selbst deutlich zu spüren, aber ich denke, dieser Beitrag kann hilfreich sein.
Ich habe mich immer gefragt, wofür das zusätzliche I in den CI-Variablen
CI_PIPELINE_IIDundCI_MERGE_REQUEST_IIDsteht.Ich hatte vermutet, dass es eine datenbankbezogene Entscheidung ist, und dieser Artikel bestätigt das.
Wenn man liest, dass „1 Quintillion 1.000.000.000 Milliarden entspricht“, wirkt es ziemlich seltsam, dass wir uns meist nur zwischen
int32undint64entscheiden. Es sollte wohl einen 5-Byte-Integer-Typ geben, der eine Kardinalität von ungefähr 1 Billion unterstützt.Keine automatisch inkrementierenden IDs zu verwenden kann sinnvoll sein, aber den Vorteil von zwei IDs für interne und externe Zwecke sehe ich nicht wirklich.
Die Zahl der Spalten und Indizes steigt, man muss immer zuerst nachschlagen, und mir fällt auch kein Sicherheitsszenario ein, in dem man den internen Schlüssel ändert, den externen aber nicht. Übersehe ich etwas?
Es heißt, dass die Verwendung des nativen PostgreSQL-Typs UUID v4 statt
bigserialdie Tabellengröße um 25 % erhöht und die Insert-Rate auf 25 % vonbigserialsenkt. Mich würde interessieren, warum UUIDv4 so schlecht ist.Ist eine UUID nicht einfach eine 128-Bit-Zahl? Ist die Erzeugung extrem teuer, oder was passiert da?
Deshalb wird das Indexieren von UUIDv4-Spalten langsamer, und das war ein Motiv für die Entwicklung von UUIDv6 und UUIDv7.
Das größere Problem ist die Insert-Rate. Bei UUIDs wird die Insert-Rate durch die verfügbare RAM-Menge begrenzt. Bei automatisch inkrementierenden Integern ist das nicht der Fall. Integer korrelieren mit der Zeit, UUID4 ist dagegen zufällig; dadurch ändern sich die Performance-Eigenschaften bei größerem Maßstab grundlegend. Bei kleinen Tabellen ist die Insert-Strafe fast vernachlässigbar, aber sobald die Größe des B-Tree-Index an die Speichergrenze stößt, kann PostgreSQL den UUID-B-Tree nicht mehr vollständig im Speicher halten und ist auf den Austausch von Disk-Pages angewiesen. Automatisch inkrementierende Integer verwenden für zeitlich nahe beieinanderliegende Zeilen dieselben Index-Pages, sodass sie bei derselben Last nicht auf die Disk zugreifen müssen. Wenn man diese Größenordnung erreicht, ist der Unterschied kein konstanter Rückgang um 25 %, sondern eine 25-fache Performance-Klippe; abgesehen von einer Schema-Migration bleibt dann nur, mehr RAM zu kaufen.
UUIDs erzeugen viele ungeordnete Schreibvorgänge und führen zu Page Bloat.
Auch die Lokalität von Batch-Inserts ist später bei Abfragen schlechter, sodass zusammengehörige Datensätze zufällig gesucht werden müssen. Am Ende zahlt man die Kosten sowohl beim Insert als auch später beim Select.