- 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
Hacker-News-Kommentare
Große Tabellen und RDS-IOPS-Grenzen
Ergebnisse des Shardings und Kosten
Zeit- und Kostenaufwand für das Sharding
Kostenvergleich mit YugabyteDB
Vorschlag zur Trennung der Datenbanken nach Kunden
Aufbau einer PG-Version ähnlich zu Vitess für MySQL
Überlegungen zu FoundationDB
Ein Ansatz, Sharding wie einen Hack zu behandeln
Fragen zur Nichtverwendung der Citus-Erweiterung
Möglicher Einsatz von Aurora Limitless
Verständnis von NoSQL-Datenbanken
jsonb, aber da bereits ein gutes Datenmodell vorhanden ist, besteht wenig Bedarf dafür.Reifegrad von Sharding und Überlegungen zu NewSQL-Lösungen
Googles Spanner-Technologie und Figmas Bewertung