34 Punkte von GN⁺ 2024-03-15 | 1 Kommentare | Auf WhatsApp teilen
  • Das Datenbankteam von Figma fasst die neunmonatige Reise zur horizontalen Shardierung des Postgres-Stacks zusammen und erklärt, wie dadurch nahezu unbegrenzte Skalierbarkeit möglich wurde

Die Reise zur horizontalen Shardierung von Figmas Postgres-Stack

  • Der Umfang von Figmas Datenbank-Stack ist seit 2020 um fast das 100-Fache gewachsen: Das ist zwar ein positives Problem, das auf die Expansion des Geschäfts hindeutet, bringt aber zugleich technische Herausforderungen mit sich. 2020 betrieb Figma eine einzelne Postgres-Datenbank auf der größten physischen Instanz von AWS; bis Ende 2022 wurde eine verteilte Architektur aufgebaut, die Caching, Read Replicas und mehrere vertikal partitionierte Datenbanken umfasst.
  • Vertikale Partitionierung: Durch das Trennen zusammengehöriger Tabellengruppen in eigene vertikale Partitionen wurden schrittweise Skalierungsvorteile erzielt und genügend Spielraum geschaffen, um dem Wachstum voraus zu bleiben. So wurden etwa zusammengehörige Tabellengruppen wie „Figma-Dateien“ oder „Organisationen“ in eigene vertikale Partitionen aufgeteilt.
  • Der Wechsel zur horizontalen Shardierung: Es wurde erkannt, dass vertikale Partitionierung allein Grenzen hat. Nach frühen Skalierungsmaßnahmen mit Fokus auf die Senkung der CPU-Auslastung begann das Team, in einer größeren und vielfältigeren Flotte unterschiedliche Engpässe zu überwachen. Die Skalierungsgrenzen der Datenbank wurden in vielen Dimensionen quantifiziert, von CPU und IO bis hin zu Tabellengröße und Anzahl geschriebener Zeilen. Das Erkennen dieser Grenzen war entscheidend, um vorherzusagen, wie viel Spielraum pro Shard vorhanden ist.
  • Grenzen der Tabellengröße: Einige Tabellen erreichten Größen von mehreren Terabyte und Milliarden von Zeilen und wurden damit zu groß, um zuverlässig in einer einzelnen Datenbank verwaltet zu werden. In dieser Größenordnung leidet die Zuverlässigkeit von Postgres bei vacuum-Vorgängen, also essenziellen Hintergrundprozessen, die verhindern, dass Transaktions-IDs erschöpft werden und das System stoppt. Die am stärksten beschriebenen Tabellen würden zudem bald die maximalen IOPS überschreiten, die Amazons Relational Database Service (RDS) unterstützt. Das lässt sich durch vertikale Partitionierung nicht lösen; es war eine größere Lösung nötig, um einen Kollaps der Datenbank zu verhindern.

Die Grundlage für Skalierung schaffen

  • Auswirkungen auf Entwickler minimieren: Der Großteil des komplexen relationalen Datenmodells sollte abgefangen werden, damit sich Anwendungsentwickler darauf konzentrieren können, bei Figma interessante neue Funktionen zu bauen, statt große Teile der Codebasis zu refaktorisieren.
  • Transparente Skalierung: Künftige Skalierung sollte keine zusätzlichen Änderungen in der Anwendungsschicht erfordern. Mit anderen Worten: Nach der anfänglichen Vorarbeit, um Tabellen kompatibel zu machen, sollte künftige Skalierung für die Produktteams transparent erfolgen.
  • Teure Backfills vermeiden: Lösungen, die ein Backfill für große oder gar alle Tabellen von Figma erfordern, wurden vermieden. Angesichts der Größe der Tabellen und der Durchsatzgrenzen von Postgres würden solche Backfills Monate dauern.
  • Schrittweises Vorgehen: Es wurde ein Ansatz gesucht, der schrittweise ausgerollt werden kann und dabei das Risiko großer Änderungen in der Produktion reduziert. Das senkt das Risiko größerer Ausfälle und hilft dem Datenbankteam, die Zuverlässigkeit von Figma während der Migration aufrechtzuerhalten.
  • Einbahnstraßen-Migrationen vermeiden: Auch nach Abschluss der physischen Shardierung sollte die Möglichkeit zum Rollback erhalten bleiben. Das reduziert das Risiko, bei unbekannten Variablen in einem schlechten Zustand stecken zu bleiben.
  • Starke Datenkonsistenz bewahren: Komplexe Lösungen, die ohne Downtime schwer umzusetzen sind oder die Konsistenz beeinträchtigen könnten, etwa Double-Writes, wurden vermieden. Gewünscht war eine Lösung, die sich mit nahezu null Downtime skalieren lässt.
  • Eigene Stärken nutzen: Unter engem Zeitdruck wurde ein Ansatz bevorzugt, der sich so schrittweise wie möglich ausrollen lässt. Für die am schnellsten wachsenden Tabellen sollte vorhandenes Fachwissen und bestehende Technik genutzt werden.

Mögliche Optionen erkunden

  • Optionen für horizontal shardierende Datenbanken prüfen: Es gibt verschiedene populäre Open-Source- und Managed-Lösungen für horizontal shardierende Datenbanken, die mit Postgres oder MySQL kompatibel sind. Im Rahmen der Evaluierung wurden CockroachDB, TiDB, Spanner und Vitess geprüft. Der Wechsel auf eine solche alternative Datenbank hätte jedoch eine komplexe Datenmigration erfordert, um Konsistenz und Zuverlässigkeit zwischen zwei unterschiedlichen Datenbankspeichern sicherzustellen.
  • Vorhandene Expertise nutzen: In den vergangenen Jahren wurde viel Know-how darin aufgebaut, RDS Postgres stabil und effizient zu betreiben. Während einer Migration hätte dieses Domänenwissen praktisch von Grund auf neu aufgebaut werden müssen. Angesichts der sehr aggressiven Wachstumsraten blieben dafür nur wenige Monate.
  • Ausschluss einer NoSQL-Datenbank: Eine weitere skalierbare Lösung, die Unternehmen beim Wachstum häufig wählen, ist eine NoSQL-Datenbank. Figma verfügt jedoch über ein sehr komplexes relationales Datenmodell, das auf der aktuellen Postgres-Architektur aufbaut, und NoSQL-APIs bieten diese Vielfalt nicht. Die Ingenieure sollten sich darauf konzentrieren können, großartige Funktionen auszuliefern und neue Produkte zu bauen, statt nahezu alle Backend-Anwendungen neu zu schreiben; NoSQL war daher keine praktikable Lösung.
  • Eine horizontale Shardierungslösung auf der bestehenden RDS-Postgres-Infrastruktur in Betracht ziehen: Für ein kleines Team ergibt es keinen Sinn, intern eine allgemeine relational shardierende Datenbank neu zu implementieren. Damit würde man mit Werkzeugen konkurrieren, die von großen Open-Source-Communities oder spezialisierten Datenbankanbietern entwickelt wurden. Da die horizontale Shardierung aber speziell auf Figmas Architektur zugeschnitten werden sollte, reichte möglicherweise ein deutlich kleinerer Funktionsumfang aus. So wurde etwa entschieden, keine shardübergreifenden Transaktionen mit garantierter Atomarität zu unterstützen, weil es Wege gibt, Fehler bei shardübergreifenden Transaktionen zu behandeln. Es wurde eine Colocation-Strategie gewählt, die Änderungen auf Anwendungsebene minimiert. Dadurch konnte eine Teilmenge von Postgres unterstützt werden, die mit dem Großteil der Produktlogik kompatibel ist. Außerdem ließ sich die Abwärtskompatibilität zwischen shardiertem und nicht shardiertem Postgres vergleichsweise einfach erhalten. Falls unbekannte Variablen aufgetreten wären, hätte ein Rollback zu nicht shardiertem Postgres leicht erfolgen können.

Der Weg zur horizontalen Shardierung

  • Einführung der horizontalen Shardierung: Horizontale Shardierung ist der Prozess, bei dem eine einzelne Tabelle oder eine Tabellengruppe aufgeteilt wird, sodass die Daten über mehrere physische Datenbankinstanzen verteilt werden. Dadurch können horizontal shardierte Tabellen in der Anwendungsschicht auf der physischen Ebene eine beliebige Anzahl von Shards unterstützen. Zusätzliche Skalierung ist jederzeit möglich, indem physische Shard-Splits durchgeführt werden; diese laufen transparent im Hintergrund ab, mit minimaler Downtime und ohne Änderungen auf Anwendungsebene. Damit konnte Figma den verbliebenen Engpässen bei der Datenbankskalierung voraus bleiben und eine der letzten großen Skalierungsherausforderungen des Unternehmens beseitigen. Wenn vertikale Partitionierung die Beschleunigung auf Autobahntempo ermöglichte, dann hob horizontale Shardierung das Tempolimit auf und machte Fliegen möglich.
  • Komplexität der horizontalen Shardierung: Horizontale Shardierung ist eine Größenordnung komplexer als frühere Skalierungsmaßnahmen. Wenn Tabellen über mehrere physische Datenbanken verteilt werden, gehen viele Zuverlässigkeits- und Konsistenzeigenschaften verloren, die man bei einer ACID-SQL-Datenbank als selbstverständlich ansieht. Bestimmte SQL-Abfragen können zum Beispiel ineffizient oder unmöglich zu unterstützen sein, und der Anwendungscode muss so angepasst werden, dass er genügend Informationen liefert, um Abfragen effizient auf möglichst den richtigen Shard zu routen. Schemaänderungen müssen koordiniert werden, damit alle Shards synchron bleiben, und Fremdschlüssel sowie global eindeutige Indizes können nicht länger von Postgres erzwungen werden. Transaktionen erstrecken sich nun über mehrere Shards, sodass Postgres sie nicht mehr erzwingen kann. Dadurch kann es vorkommen, dass Schreibvorgänge auf einigen Datenbanken erfolgreich sind, während andere fehlschlagen. Die Produktlogik muss deshalb robust gegenüber solchen „teilweisen Commit-Fehlern“ sein; man stelle sich etwa vor, ein Team wird zwischen zwei Organisationen verschoben und nur die Hälfte der Daten fehlt!
  • Ein mehrjähriger Weg zur horizontalen Shardierung: Es war klar, dass vollständige horizontale Shardierung ein mehrjähriges Vorhaben sein würde. Gleichzeitig musste der Projektrisiko so weit wie möglich reduziert und schrittweise Nutzen geliefert werden. Das erste Ziel war, so schnell wie möglich eine relativ einfache, aber sehr trafficstarke Tabelle in der Produktion zu sharden. Das würde nicht nur die Machbarkeit horizontaler Shardierung belegen, sondern auch den Spielraum für die am stärksten belasteten Datenbanken vergrößern. Danach könnten weitere Funktionen aufgebaut werden, während komplexere Tabellengruppen shardiert werden. Selbst der kleinstmögliche Funktionsumfang bedeutete noch erhebliche Arbeit. Vom Anfang bis zum Ende benötigte das Team etwa neun Monate, um die erste Tabelle zu sharden.

Unser einzigartiger Ansatz

  • Colocations (Colos): Zugehörige Tabellengruppen werden per horizontalem Sharding in Colocations aufgeteilt, die denselben Sharding-Key und dasselbe physische Sharding-Layout teilen (intern liebevoll „Colo“ genannt). Das bietet Entwickler:innen eine vertraute Abstraktion für die Interaktion mit horizontal geshardeten Tabellen.
  • Logisches Sharding: Die Konzepte von „logischem Sharding“ auf der Anwendungsebene und „physischem Sharding“ auf der Postgres-Ebene werden getrennt. Dazu werden Views genutzt, um zunächst ein sichereres und kostengünstigeres logisches Sharding-Rollout durchzuführen, bevor die riskantere verteilte physische Failover-Umstellung erfolgt.
  • DBProxy Query Engine: Es wurde der Dienst DBProxy aufgebaut, der auf Anwendungsebene erzeugte SQL-Abfragen abfängt und dynamisch an verschiedene Postgres-Datenbanken weiterleitet. DBProxy enthält eine Query Engine, die komplexe horizontal geshardete Abfragen parsen und ausführen kann. Darüber konnten Funktionen wie dynamischer Lastenausgleich und Request Hedging umgesetzt werden.
  • Shadow Application Readiness: Es wurde ein Framework für „Shadow Application Readiness“ ergänzt, das vorhersagen kann, wie sich echter Produktionstraffic unter verschiedenen potenziellen Sharding-Keys verhalten würde. Dadurch erhalten Produktteams ein klares Bild davon, ob Anwendungslogik refaktoriert oder entfernt werden muss, um die Anwendung für horizontales Sharding vorzubereiten.
  • Vollständige logische Replikation: Es musste keine „gefilterte logische Replikation“ implementiert werden, bei der nur Teilmengen der Daten auf einzelne Shards kopiert werden. Stattdessen wurde der gesamte Datensatz kopiert und anschließend nur für die Teilmenge der Daten Lese- und Schreibzugriff erlaubt, die zu einem bestimmten Shard gehört.

Sharding implementieren

  • Die Bedeutung der Wahl des Shard-Keys: Eine der wichtigsten Entscheidungen beim horizontalen Sharding ist, welcher Shard-Key verwendet wird. Horizontales Sharding bringt rund um den Shard-Key mehrere Einschränkungen des Datenmodells mit sich. So müssen die meisten Abfragen den Shard-Key enthalten, damit Anfragen an den richtigen Shard weitergeleitet werden können. Bestimmte Datenbank-Constraints, etwa Fremdschlüssel, funktionieren nur dann, wenn der Fremdschlüssel zugleich der Sharding-Key ist. Der Shard-Key muss die Daten gleichmäßig über alle Shards verteilen, um Hotspots zu vermeiden, die Zuverlässigkeitsprobleme verursachen oder die Skalierbarkeit beeinträchtigen.
  • Figmas an das Datenmodell angepasster Ansatz: Figma läuft im Browser, und viele Nutzer:innen können gleichzeitig an derselben Figma-Datei zusammenarbeiten. Das wird von einem relativ komplexen relationalen Datenmodell getragen, das Dateimetadaten, Organisationsmetadaten, Kommentare, Dateiversionen und mehr erfasst. Da es im bestehenden Datenmodell keinen einzelnen guten Kandidaten gab, wurde erwogen, für alle Tabellen denselben Sharding-Key zu verwenden. Das hätte jedoch bedeutet, zusammengesetzte Schlüssel zu erzeugen, um einen einheitlichen Sharding-Key einzuführen, Spalten zu allen Tabellenschemata hinzuzufügen, teure Backfills auszuführen, um diese zu befüllen, und anschließend die Produktlogik erheblich zu refaktorieren. Stattdessen wurde der Ansatz an Figmas einzigartiges Datenmodell angepasst, indem eine kleine Zahl von Sharding-Keys wie UserID, FileID und OrgID ausgewählt wurde. Fast alle Tabellen bei Figma lassen sich mit einem dieser Keys sharden.
  • Einführung von Colocations (Colos): Es wurde das Konzept der Colocation eingeführt, um Produktentwickler:innen eine vertraute Abstraktion zu bieten. Tabellen innerhalb einer Colo unterstützen tabellenübergreifende Joins und vollständige Transaktionen, solange sie auf einen einzigen Sharding-Key beschränkt sind. Der Großteil des Anwendungscodes interagierte bereits auf diese Weise mit der Datenbank, wodurch der Aufwand für Anwendungsentwickler:innen minimiert wurde, Tabellen für horizontales Sharding anzupassen.
  • Gleichmäßige Datenverteilung sicherstellen: Nachdem die Sharding-Keys ausgewählt waren, musste eine gleichmäßige Datenverteilung über alle Backend-Datenbanken sichergestellt werden. Leider verwendeten viele der gewählten Sharding-Keys autoinkrementierende IDs oder IDs mit Snowflake-Zeitstempelpräfix. Das hätte zu erheblichen Hotspots geführt, bei denen der Großteil der Daten auf einem einzelnen Shard liegt. Eine Migration zu stärker randomisierten IDs wurde geprüft, hätte aber eine kostspielige und langwierige Datenmigration erfordert. Stattdessen entschied man sich, für das Routing einen Hash des Sharding-Keys zu verwenden. Wenn eine ausreichend zufällige Hash-Funktion gewählt wird, lässt sich eine gleichmäßige Datenverteilung sicherstellen. Ein Nachteil dabei ist, dass Range Scans über den Shard-Key weniger effizient sind, weil aufeinanderfolgende Keys auf unterschiedliche Datenbank-Shards gehasht werden. Da dieses Abfragemuster in der Codebasis jedoch selten vorkommt, war das ein akzeptabler Kompromiss.

Eine „logische“ Lösung

  • Risiko beim Rollout von horizontalem Sharding verringern: Um das Risiko beim Rollout von horizontalem Sharding zu senken, sollte der physische Prozess der Shard-Aufteilung von der Vorbereitung der Tabellen auf Anwendungsebene getrennt werden. Dazu wurden „logisches Sharding“ und „physisches Sharding“ voneinander getrennt. So konnten die beiden Teile der Migration separat umgesetzt werden, was das Risiko reduzierte. Logisches Sharding sorgte über ein risikoarmes, schrittweises Rollout für Vertrauen in den Serving-Stack. Wenn Bugs gefunden wurden, war ein Rollback des logischen Shardings lediglich eine einfache Konfigurationsänderung. Ein Rollback physischer Shard-Arbeiten war zwar möglich, erforderte aber komplexere Koordination, um Datenkonsistenz sicherzustellen.
  • Verhalten nach logischem Sharding: Sobald eine Tabelle logisch geshardet ist, funktionieren alle Lese- und Schreiboperationen bereits so, als wäre sie horizontal geshardet. In Bezug auf Zuverlässigkeit, Latenz und Konsistenz wirkt das System dann wie horizontal geshardet, obwohl die Daten physisch weiterhin auf einem einzelnen Datenbank-Host liegen. Sobald ausreichend Vertrauen bestand, dass das logische Sharding wie erwartet funktioniert, wurde das physische Sharding durchgeführt. Dabei wurden Daten aus einer einzelnen Datenbank kopiert, über mehrere Backends geshardet und anschließend Lese- und Schreibtraffic auf die neuen Datenbanken umgeleitet.

Eine Query Engine, die etwas kann

  • Neugestaltung des Backend-Stacks für horizontales Sharding: Anfangs kommunizierten die Anwendungsdienste direkt mit PGBouncer, der als Connection-Pooling-Schicht diente. Horizontales Sharding erfordert jedoch deutlich komplexeres Parsing, Planning und Ausführen von Abfragen. Um das zu unterstützen, wurde mit DBProxy ein neuer golang-Dienst aufgebaut. DBProxy sitzt zwischen der Anwendungsebene und PGBouncer. Er enthält Logik für Lastenausgleich, verbesserte Observability, Transaktionsunterstützung, Verwaltung der Datenbanktopologie und eine leichtgewichtige Query Engine.
  • Kernkomponenten der Query Engine:
    • Query Parser: Liest das von der Anwendung gesendete SQL und wandelt es in einen Abstract Syntax Tree (AST) um.
    • Logischer Planner: Parst den AST und extrahiert aus dem Query-Plan den Abfragetyp (Insert, Update usw.) sowie die logische Shard-ID.
    • Physischer Planner: Ordnet die Abfrage von der logischen Shard-ID einer physischen Datenbank zu. Er schreibt die Abfrage so um, dass sie auf dem passenden physischen Shard ausgeführt werden kann.
  • „Scatter-Gather“-Ansatz: Er funktioniert wie ein Versteckspiel über die gesamte Datenbank hinweg: Die Abfrage wird an alle Shards gesendet (scatter), und die Antworten werden von jedem eingesammelt (gather). Das ist praktisch, kann die Datenbank bei komplexen Abfragen und übermäßigem Einsatz jedoch erheblich ausbremsen.
  • Abfragen in einer horizontal geshardeten Welt umsetzen: Single-Shard-Abfragen werden nach einem einzelnen Shard-Key gefiltert. Die Query Engine muss dann lediglich den Shard-Key extrahieren und die Abfrage an die passende physische Datenbank routen. Die Komplexität der Abfrageausführung wird dabei an Postgres „nach unten gereicht“. Fehlt der Sharding-Key in der Abfrage, muss die Query Engine hingegen ein komplexeres „scatter-gather“ ausführen. In diesem Fall wird die Abfrage auf alle Shards aufgefächert (Scatter-Phase), anschließend werden die Ergebnisse aggregiert (Gather-Phase).
  • SQL-Kompatibilität vereinfachen: Würde der DBProxy-Dienst vollständige SQL-Kompatibilität unterstützen, sähe er der Query Engine von Postgres sehr ähnlich. Um die Komplexität von DBProxy zu minimieren, sollte die API vereinfacht werden und zugleich der Aufwand für Anwendungsentwickler:innen sinken, nicht unterstützte Abfragen umzuschreiben. Um die passende Teilmenge zu bestimmen, wurde ein „Shadow Planning“-Framework aufgebaut, mit dem sich potenzielle Sharding-Schemata für Tabellen definieren und die logische Planning-Phase auf echtem Produktionstraffic in Echtzeit ausführen lassen. Die zugehörigen Query-Pläne werden in einer Snowflake-Datenbank protokolliert, sodass Offline-Analysen möglich sind. Auf Basis dieser Daten wurde eine Query-Sprache ausgewählt, die die häufigsten 90 % der Abfragen unterstützt, ohne in die Worst-Case-Komplexität der Query Engine zu geraten. So sind zum Beispiel alle Range Scans und Point Queries erlaubt, Joins jedoch nur dann, wenn sie zwischen zwei Tabellen derselben Colo über den Sharding-Key ausgeführt werden.

Ausblick

  • Kapselung logischer Shards: Es musste entschieden werden, wie logische Shards gekapselt werden sollen. Es wurde untersucht, Daten mithilfe separater Postgres-Datenbanken oder Postgres-Schemas aufzuteilen. Leider hätte dies bei logischem Sharding physische Datenänderungen erfordert, was ähnlich komplex gewesen wäre wie das Aufteilen physischer Shards.
  • Darstellung von Shards über Postgres-Views: Stattdessen entschied man sich, Shards als Postgres-Views darzustellen. Für jede Tabelle können mehrere Views erstellt werden, von denen jede einer Teilmenge der Daten eines bestimmten Shards entspricht. Das sieht dann so aus: CREATE VIEW table_shard1 AS SELECT * FROM table WHERE hash(shard_key) >= min_shard_range AND hash(shard_key) < max_shard_range). Alle Lese- und Schreibvorgänge erfolgen über diese Views.
  • Erstellung geshardeter Views auf einer bestehenden, nicht geshardeten physischen Datenbank: Dadurch war logisches Sharding möglich, bevor riskante physische Resharding-Operationen durchgeführt wurden. Auf jede View wird über einen eigenen geshardeten Connection-Pooler-Service zugegriffen. Der Connection-Pooler verweist weiterhin auf die nicht geshardete physische Instanz und lässt sie dennoch wie geshardet erscheinen. Über Feature-Flags in der Query-Engine konnten geshardete Lese- und Schreibvorgänge schrittweise mit geringerem Risiko ausgerollt werden, und durch das Zurückrouten des Traffics auf die Haupttabellen war jederzeit innerhalb weniger Sekunden ein Rollback möglich. Bis zum ersten Resharding bestand daher Vertrauen in die Sicherheit der geshardeten Topologie.
  • Risiken der Abhängigkeit von Views: Views fügen Performance-Overhead hinzu und können in manchen Fällen die Art und Weise grundlegend verändern, wie der Postgres-Query-Planer Queries optimiert. Zur Validierung dieses Ansatzes wurde ein Query-Korpus aus bereinigten Produktions-Queries gesammelt und Lasttests mit und ohne Views durchgeführt. Dabei ließ sich bestätigen, dass Views in den meisten Fällen nur minimalen Performance-Overhead verursachen und selbst im schlimmsten Fall unter 10 % bleiben. Außerdem wurde ein Shadow-Read-Framework aufgebaut, das den gesamten Echtzeit-Lesetraffic über Views leitet und Performance sowie Korrektheit von Queries mit und ohne Views vergleicht. Das Ergebnis bestätigte, dass Views mit minimalem Performance-Einfluss eine praktikable Lösung sind.

Lösung des Topologieproblems

  • Topologieverständnis von DBProxy für das Query-Routing: Es war notwendig, dass DBProxy die Topologie von Tabellen und physischen Datenbanken versteht. Durch die Trennung der Konzepte von logischem und physischem Sharding entstand die Notwendigkeit, diese Abstraktionen innerhalb der Topologie abzubilden.
  • Mapping von Tabellen und Shard-Schlüsseln: Es wurde ein Mechanismus benötigt, um die Tabelle users dem Shard-Schlüssel user_id zuzuordnen und eine logische Shard-ID (123) den passenden logischen und physischen Datenbanken zuzuordnen.
  • Vertikale Partitionierung und Abhängigkeit von hartcodierten Konfigurationsdateien: Bei der vertikalen Partitionierung stützte man sich auf einfache hartcodierte Konfigurationsdateien, die Tabellen ihren jeweiligen Partitionen zuordneten. Der Übergang zu horizontalem Sharding erforderte jedoch ein deutlich komplexeres System.
  • Dynamische Änderungen der Topologie und Bedarf an schneller Statusaktualisierung in DBProxy: Während der Aufteilung von Shards ändert sich die Topologie dynamisch, weshalb DBProxy seinen Status schnell aktualisieren muss, um Anfragen nicht an die falsche Datenbank weiterzuleiten.
  • Abwärtskompatibilität von Topologieänderungen: Alle Änderungen an der Topologie mussten abwärtskompatibel sein, ohne Änderungen auf kritischen Pfaden der Website.
  • Aufbau einer Datenbanktopologie zur Kapselung komplexer Metadaten des horizontalen Shardings: Es wurde eine Datenbanktopologie aufgebaut, die die komplexen Metadaten des horizontalen Shardings kapselt und Echtzeit-Updates in unter einer Sekunde bereitstellt.
  • Vereinfachung des Datenbankmanagements durch Trennung logischer und physischer Topologie: In Nicht-Produktionsumgebungen konnten Kosten gesenkt und die Komplexität reduziert werden, indem dieselbe logische Topologie wie in der Produktion beibehalten, aber die Anzahl physischer Datenbanken verringert wurde.
  • Erzwingen von Invarianten in der Topologie über eine Topologie-Bibliothek: Um beim Aufbau des horizontalen Shardings die Korrektheit des Systems sicherzustellen, wurden Invarianten in der Topologie erzwungen, etwa dass jede Shard-ID genau einer physischen Datenbank zugeordnet sein muss.

Physische Sharding-Operationen

  • Letzter Schritt nach Abschluss der Sharding-Vorbereitung für Tabellen: Die physische Umschaltung von einer nicht geshardeten auf eine geshardete Datenbank. Zwar konnte ein Großteil derselben Logik für horizontales Sharding wiederverwendet werden, es gab aber einige bemerkenswerte Unterschiede, etwa den Wechsel von einer 1:1-Datenbank zu 1:N.
  • Notwendigkeit höherer Robustheit im Failover-Prozess: Der Failover-Prozess musste robuster gemacht werden, um neue Fehlermodi abzufangen, bei denen eine Sharding-Operation nur in einem Teil der Datenbank erfolgreich sein kann.
  • Die meisten Risiken bereits bei der vertikalen Partitionierung entschärft: Da viele Risiken schon während der vertikalen Partitionierung reduziert worden waren, konnte deutlich schneller als sonst zur ersten physischen Sharding-Operation übergegangen werden.

Aktueller Stand der Reise zum horizontalen Sharding

  • Mehrjährige Investition in horizontales Sharding: Figma erkannte, dass für die zukünftige Skalierbarkeit mehrjährige Investitionen in horizontales Sharding nötig sind, und führte im September 2023 die erste horizontal geshardete Tabelle ein.
  • Erfolgreich ausgeführtes Failover: Ein erfolgreiches Failover wurde erreicht, mit 10 Sekunden vorübergehender teilweiser Verfügbarkeitseinschränkung bei den Datenbank-Primaries und ohne Verfügbarkeitsauswirkungen bei Replikaten. Nach dem Sharding gab es keine Regressionen bei Latenz oder Verfügbarkeit.
  • Umgang mit komplexen Shards: Zunächst wurde ein relativ einfacher Shard der Datenbank mit der höchsten Schreiblast verarbeitet. In diesem Jahr sollen zunehmend komplexere Datenbanken mit Dutzenden Tabellen und Tausenden Code-Aufrufstellen geshardet werden.
  • Bedarf an horizontalem Sharding für alle Tabellen bei Figma: Um die letzte Skalierungsgrenze zu beseitigen und echte Ausfallsicherheit zu erreichen. Eine vollständig horizontal geshardete Welt bietet verschiedene Vorteile wie höhere Zuverlässigkeit, geringere Kosten und mehr Entwicklertempo.
  • Zu lösende Probleme:
    • Unterstützung für Schema-Updates in horizontal geshardeten Umgebungen
    • Erzeugung global eindeutiger IDs für horizontal geshardete Primärschlüssel
    • Atomare shard-übergreifende Transaktionen für geschäftskritische Anwendungsfälle
    • Verteilte global eindeutige Indizes (derzeit nur für Indizes unterstützt, die den Sharding-Schlüssel enthalten)
    • Mehr Entwicklertempo durch ORM-Modelle, die reibungslos mit horizontalem Sharding kompatibel sind
    • Vollautomatisierte Resharding-Operationen, bei denen sich Shard-Splits per Knopfdruck ausführen lassen
  • Neubewertung des bestehenden horizontalen RDS-Sharding-Ansatzes: Die Reise begann vor 18 Monaten unter sehr engem Zeitdruck. Gleichzeitig entwickeln sich NewSQL-Stores kontinuierlich weiter und werden reifer. Inzwischen besteht genügend Spielraum, die Trade-offs zwischen dem Beibehalten des aktuellen Wegs und einem Wechsel zu einer Open-Source- oder Managed-Lösung neu zu bewerten.
  • Spannende Fortschritte auf dem Weg zum horizontalen Sharding: Viele der noch offenen Herausforderungen stehen erst am Anfang. Weitere Deep Dives in verschiedene Teile des horizontalen Sharding-Stacks sind zu erwarten. Wer sich für Projekte wie dieses interessiert, soll sich melden. Es wird eingestellt.

Meinung von GN⁺

  • Das Datenbankteam von Figma wollte mit horizontalem Sharding die Grenzen der Datenbankskalierbarkeit überwinden, was ein wichtiger Schritt für das Wachstum und die Aufrechterhaltung der Performance eines Cloud-basierten Kollaborationstools ist.
  • Horizontales Sharding bringt neue Herausforderungen bei Datenmanagement und Query-Optimierung mit sich und verlangt Datenbankadministratoren und Entwicklern neues Wissen und neue Fähigkeiten ab.
  • Horizontales Sharding verbessert die Skalierbarkeit von Datenbanken deutlich, erfordert aber neue Lösungen für die Verarbeitung komplexer Queries und die Wahrung der Datenkonsistenz.
  • Ein Open-Source-Projekt mit ähnlichen Funktionen ist CitusDB, das Möglichkeiten zur horizontalen Skalierung von Postgres-Datenbanken bietet.
  • Bei der Einführung von horizontalem Sharding sollten Aspekte wie die Komplexität des Datenmodells, Query-Performance sowie Flexibilität und Wartbarkeit des Systems berücksichtigt werden. Letztlich geht es darum, ein Gleichgewicht zwischen Datenbankskalierbarkeit und einfacher Verwaltung zu finden.

1 Kommentare

 
GN⁺ 2024-03-15
Hacker-News-Kommentare
  • Große Tabellen und RDS-IOPS-Grenzen

    • Es wird erwähnt, dass die größte Tabelle mehrere TB groß ist und bald die maximal von RDS unterstützten IOPS überschreiten könnte.
    • RDS für PostgreSQL erreicht auf einem 64-TB-Volume maximal 256.000 IOPS.
    • Bei einer Multi-AZ-Konfiguration entstehen dadurch Kosten von 70.000 US-Dollar pro Monat.
  • Ergebnisse des Shardings und Kosten

    • Am Ende wird von einem 5-Wege-Sharding ausgegangen, bei dem jeder Shard etwa 50.000 IOPS und 12 TB Daten unterstützt.
    • Bei einer Multi-AZ-Konfiguration entstehen dadurch Kosten von 100.000 US-Dollar pro Monat.
  • Zeit- und Kostenaufwand für das Sharding

    • Das Sharding der ersten Tabelle dauerte 9 Monate.
    • Da auch Änderungen an der Anwendung nötig waren, ergibt sich die Rechnung 9 Monate * 20 Arbeitstage/Monat * (3 DB-Ingenieure + 2 App-Ingenieure) = 900 Arbeitstage.
    • Wenn man von einem durchschnittlichen Jahresgehalt von 100.000 US-Dollar pro Ingenieur ausgeht, belaufen sich die Gesamtkosten auf etwa 400.000 US-Dollar.
  • Kostenvergleich mit YugabyteDB

    • Für YugabyteDB, ein PostgreSQL-kompatibles NewSQL-System, werden 15.000 US-Dollar pro Monat veranschlagt, um die höchste RDS-Leistung zu erreichen.
    • Figma hat intern etwa das 25-Fache (400.000/15.000 US-Dollar) ausgegeben, um horizontales Sharding zu implementieren, und nutzt weiterhin RDS, was zudem etwa das 6-Fache (100.000/15.000 US-Dollar) kostet.
  • Vorschlag zur Trennung der Datenbanken nach Kunden

    • Es wird vorgeschlagen, jeden Kunden in eine separate (logische) Datenbank zu legen, was einfacher sein könnte.
    • Da keine Transaktionen zwischen verschiedenen Kunden nötig sind, scheint man ein schwierigeres Problem zu lösen als tatsächlich vorhanden ist.
    • Ob sich die (logischen) Datenbanken von PostgreSQL gut skalieren lassen, ist unklar, aber grundsätzlich nicht unmöglich.
  • Aufbau einer PG-Version ähnlich zu Vitess für MySQL

    • Das Umschreiben von Queries ist interessant.
    • Wenn man eine Schicht zwischen DB und Anwendung einzieht, wären auch verschiedene ACLs (Access Control Lists) möglich.
  • Überlegungen zu FoundationDB

    • Es wird gefragt, warum FoundationDB nicht ausprobiert wurde.
    • Es gab Probleme mit dem Vacuuming (Garbage Collection) von PostgreSQL.
    • In früheren Versionen wurde für das Vacuuming doppelt so viel Speicherplatz benötigt, in neueren Versionen könnte sich das geändert haben.
  • Ein Ansatz, Sharding wie einen Hack zu behandeln

    • Es sei besser, sich auf die OS-APIs zu verlassen, statt Low-Level-I/O-Buffering/Caching selbst zu handhaben.
    • Es besteht weiterhin der Eindruck, dass vergleichbare Techniken/Infrastrukturen für DB-Sharding noch fehlen.
  • Fragen zur Nichtverwendung der Citus-Erweiterung

    • Citus ist bereits eine ausgereifte Postgres-Erweiterung, wird im Artikel aber nicht erwähnt.
    • Möglicherweise kannte man Citus nicht oder hat es aus bestimmten Gründen ignoriert.
  • Möglicher Einsatz von Aurora Limitless

    • Es wird gefragt, ob Amazon Aurora Limitless hätte verwendet werden können.
  • Verständnis von NoSQL-Datenbanken

    • NoSQL eignet sich für Backends, die unstrukturierte Daten aufnehmen, bei denen kein komplexes relationales Modell nötig ist.
    • Postgres unterstützt dies mit dem Datentyp jsonb, aber da bereits ein gutes Datenmodell vorhanden ist, besteht wenig Bedarf dafür.
  • Reifegrad von Sharding und Überlegungen zu NewSQL-Lösungen

    • Es wird gefragt, ob es angesichts des ausgereiften Shardings sinnvoll ist, NewSQL-Lösungen für automatisches Sharding und Unterstützung über mehrere Regionen hinweg in Betracht zu ziehen.
    • Zusätzliche Kosten entstehen auch dadurch, dass man lernen muss, wie man NewSQL-Datenbanken betreibt.
  • Googles Spanner-Technologie und Figmas Bewertung

    • Bei Google gilt Spanner als eine fast magische Technologie, die unbegrenztes horizontales Sharding und Transaktionen unterstützt, weshalb fast alle Projekte dorthin migrieren.
    • Es wird gefragt, wie Figma Cloud Spanner bewertet hat und ob man mit dem horizontalen Postgres-Schema tatsächlich die Unterstützung echter Transaktionen aufgegeben hat, wenn auch nur vorübergehend.