Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Partitionieren einer milliardenzeiligen Tabelle mit Fußballdaten mithilfe des Datenkontexts

In diesem Artikel erfahren Sie, wie Sie die Semantik hinter Ihren Daten verwenden, wenn Sie Ihre Datenbank partitionieren. Dies kann die Leistung Ihrer Anwendung drastisch verbessern. Und was am wichtigsten ist, Sie werden feststellen, dass Sie Ihre Partitionierungskriterien an Ihre einzigartige Anwendungsdomäne anpassen sollten.

Ich habe mit einem Startup zusammengearbeitet, um eine Web-App zu entwickeln, mit der Sportexperten Entscheidungen treffen und Daten untersuchen können. Die Anwendung unterstützt jede Sportart, aber wir sind in Europa ansässig – und Europäer lieben Fußball. Jedes der Hunderte von Spielen, die jeden Tag weltweit gespielt werden, kommt mit Tausenden von Reihen. In nur wenigen Monaten erreichte die Ereignistabelle in unserer App eine halbe Milliarde Zeilen!

Indem wir verstanden, wie Fußballexperten unsere Daten abgefragt haben, konnten wir die Datenbank intelligent partitionieren. Die durchschnittliche Zeitverbesserung an diesem neuen Tisch war zwischen 20x und 40x schneller. Die durchschnittliche Zeitverbesserung bei allen Abfragen betrug das 5- bis 10-fache.

Lassen Sie uns nun in dieses Szenario eintauchen und erfahren, warum Sie Ihren Datenkontext beim Partitionieren einer Datenbank nicht ignorieren können.

Darstellung des Kontexts

Unsere Sportanwendung bietet sowohl rohe als auch aggregierte Daten, obwohl die Profis, die sie übernommen haben, letzteres bevorzugen. Die zugrunde liegende Datenbank enthält Terabytes an komplexen, unstrukturierten, heterogenen Daten von mehreren Anbietern. Die größte Herausforderung bestand also darin, eine zuverlässige, schnelle und einfach zu durchsuchende Datenbank zu entwerfen.

Anwendungsdomäne

In dieser Branche bieten viele Anbieter ihren Kunden Zugang zu den Ereignissen der wichtigsten Fußballspiele. Insbesondere liefern sie Ihnen Daten über das, was während eines Spiels passiert ist, wie Tore, Vorlagen, gelbe Karten, Pässe und vieles mehr. Die Tabelle mit diesen Daten ist bei weitem die größte, mit der wir arbeiten mussten.

VPS-Spezifikationen, -Technologien und -Architektur

Mein Team hat die Backend-Anwendung entwickelt, die die wichtigsten Datenexplorationsfunktionen bietet. Wir haben Kotlin v1.6 übernommen, das auf einer JVM (Java Virtual Machine) als Programmiersprache läuft, Spring Boot 2.5.3 als Framework und Hibernate 5.4.32.Final als ORM (Object Relational Mapping). Der Hauptgrund, warum wir uns für diesen Technologie-Stack entschieden haben, ist, dass Geschwindigkeit eine der wichtigsten Geschäftsanforderungen ist. Also brauchten wir eine Technologie, die eine starke Multi-Thread-Verarbeitung nutzen konnte, und Spring Boot erwies sich als zuverlässige Lösung.

Wir haben unser Backend auf einem 16 GB 8-CPU-VPS über einen von Dokku verwalteten Docker-Container bereitgestellt. Es kann maximal 15 GB RAM verwenden. Dies liegt daran, dass ein GB RAM einem Redis-basierten Caching-System gewidmet ist. Wir haben es hinzugefügt, um die Leistung zu verbessern und eine Überlastung des Backends mit wiederholten Vorgängen zu vermeiden.

Datenbank- und Tabellenstruktur

Bei der Datenbank haben wir uns für MySQL 8 entschieden. Ein VPS mit 8 GB und 2 CPUs hostet derzeit den Datenbankserver, der bis zu 200 gleichzeitige Verbindungen unterstützt. Die Back-End-Anwendung und die Datenbank befinden sich in derselben Serverfarm, um den Kommunikationsaufwand zu vermeiden. Wir haben die Datenbankstruktur entworfen, um Duplikate zu vermeiden und die Leistung im Auge zu behalten. Wir haben uns für eine relationale Datenbank entschieden, weil wir eine konsistente Struktur für die Konvertierung der von den Anbietern erhaltenen Daten haben wollten. Auf diese Weise standardisieren wir die Sportdaten, sodass sie einfacher zu durchsuchen und den Endnutzern präsentiert werden können.

Die Datenbank enthält zum Zeitpunkt des Schreibens Hunderte von Tabellen, und ich kann sie aufgrund der NDA, die ich unterzeichnet habe, nicht alle präsentieren. Glücklicherweise reicht eine Tabelle aus, um gründlich zu analysieren, warum wir schließlich die datenkontextbasierte Partition übernommen haben, die Sie gleich sehen werden. Die eigentliche Herausforderung kam, als wir anfingen, umfangreiche Abfragen für die Ereignistabelle durchzuführen. Aber bevor wir uns damit befassen, sehen wir uns an, wie die Ereignistabelle aussieht:

Wie Sie sehen können, enthält es nicht viele Spalten, aber denken Sie daran, dass ich einige davon aus Vertraulichkeitsgründen weglassen musste. Aber was wirklich wichtig ist hier die parameterId und gameId Säulen. Wir verwenden diese beiden Fremdschlüssel, um eine Art von Parameter (z. B. Tor, gelbe Karte, Pass, Elfmeter) und die Spiele auszuwählen, in denen es passiert ist.

Leistungsprobleme

Die Ereignistabelle erreichte in nur wenigen Monaten eine halbe Milliarde Zeilen. Wie wir in diesem Blogbeitrag bereits ausführlich behandelt haben, besteht das Hauptproblem darin, dass wir Aggregatoperationen mit langsamen IN-Abfragen durchführen müssen. Das liegt daran, dass es nicht so wichtig ist, was während eines Spiels passiert. Stattdessen wollen Sportexperten aggregierte Daten analysieren, um Trends zu erkennen und darauf basierende Entscheidungen zu treffen.

Obwohl sie im Allgemeinen die gesamte Saison oder die letzten 5 oder 10 Spiele analysieren, möchten Benutzer häufig einige bestimmte Spiele aus ihrer Analyse ausschließen. Denn sie wollen nicht, dass ein besonders schlecht oder gut gespieltes Spiel ihre Ergebnisse polarisiert. Wir können die aggregierten Daten nicht im Voraus generieren, da wir dies für alle möglichen Kombinationen tun müssten, was nicht machbar ist. Also müssen wir alle Daten speichern und spontan aggregieren.

Das Leistungsproblem verstehen

Lassen Sie uns nun auf den zentralen Aspekt eingehen, der zu den Leistungsproblemen führte, mit denen wir konfrontiert waren.

Tabellen mit einer Million Zeilen sind langsam

Wenn Sie jemals mit Tabellen gearbeitet haben, die Hunderte Millionen Zeilen enthalten, wissen Sie, dass sie von Natur aus langsam sind. Sie können nicht einmal daran denken, JOINs auf so großen Tabellen auszuführen. Dennoch können Sie SELECT-Abfragen in angemessener Zeit durchführen. Dies gilt insbesondere, wenn diese Abfragen einfache WHERE-Bedingungen beinhalten. Andererseits werden sie furchtbar langsam, wenn Aggregatfunktionen oder IN-Klauseln verwendet werden. In diesen Fällen können sie leicht bis zu 80 Sekunden dauern, was einfach zu viel ist.

Indizes sind nicht genug

Um die Leistung zu verbessern, haben wir uns entschieden, einige Indizes zu definieren. Dies war unser erster Ansatz, um eine Lösung für die Leistungsprobleme zu finden. Aber leider führte dies zu einem anderen Problem. Indizes brauchen Zeit und Platz. Dies ist im Allgemeinen unbedeutend, jedoch nicht bei solch großen Tabellen. Es stellte sich heraus, dass das Definieren komplexer Indizes basierend auf den häufigsten Abfragen mehrere Stunden und GB Speicherplatz in Anspruch nahm. Außerdem sind Indizes hilfreich, aber keine Zauberei.

Datenkontextbasierte Datenbankpartitionierung als Lösung

Da wir das Performance-Problem mit benutzerdefinierten Indizes nicht lösen konnten, entschieden wir uns für einen neuen Ansatz. Wir haben mit anderen Experten gesprochen, online nach Lösungen gesucht, Artikel zu ähnlichen Szenarien gelesen und schließlich entschieden, dass die Partitionierung der Datenbank der richtige Ansatz ist.

Warum traditionelle Partitionierung möglicherweise nicht der richtige Ansatz ist

Bevor wir alle unsere größten Tabellen partitioniert haben, haben wir uns sowohl in der offiziellen MySQL-Dokumentation als auch in interessanten Artikeln mit dem Thema befasst. Obwohl wir uns alle darin einig waren, dass dies der richtige Weg war, erkannten wir auch, dass die Anwendung der Partitionierung ohne Berücksichtigung unserer speziellen Anwendungsdomäne ein Fehler wäre. Insbesondere haben wir verstanden, wie wichtig es ist, beim Partitionieren einer Datenbank die richtigen Kriterien zu finden. Einige Partitionierungsexperten haben uns beigebracht, dass der traditionelle Ansatz darin besteht, nach der Anzahl der Zeilen zu partitionieren. Aber wir wollten etwas Intelligenteres und Effizienteres finden.

Eintauchen in die Anwendungsdomäne, um die Partitionierungskriterien zu finden

Wir haben eine wichtige Lektion gelernt, indem wir die Anwendungsdomäne analysiert und unsere Benutzer befragt haben. Sportexperten neigen dazu, aggregierte Daten von Spielen desselben Wettbewerbs zu analysieren. Ein Wettbewerb im Fußball kann beispielsweise eine Liga, ein Turnier oder ein einzelnes Spiel sein, bei dem Sie einen Pokal gewinnen können. Es gibt Tausende von verschiedenen Wettbewerben. Die wichtigsten in Europa sind die Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 und Primeira Liga.

Dies bedeutet, dass unsere Benutzer nur sehr selten Daten aus verschiedenen Wettbewerben berücksichtigen. Außerdem ziehen sie es vor, Daten Saison für Saison zu erkunden. Mit anderen Worten, sie verlassen selten den Kontext, der durch einen in einer bestimmten Saison ausgetragenen Sportwettkampf repräsentiert wird. Unsere Datenbankstruktur drückte dieses Konzept mit einer Tabelle namens SeasonCompetition aus , deren Ziel es ist, einen Wettbewerb einer bestimmten Saison zuzuordnen. Wir haben also festgestellt, dass es ein guter Ansatz wäre, unsere größeren Tabellen in Untertabellen zu unterteilen, die sich auf einen bestimmten SeasonCompetition beziehen Beispiel.

Insbesondere haben wir das folgende Namensformat für diese neuen Tabellen definiert:<tableName>_<seasonCompetitionId> .

Folglich, wenn wir 100 Reihen im SeasonCompetition hätten Tabelle müssten wir die großen Events aufteilen Tabelle in die kleinere Events_1 , Events_2 , …, Events_100 Tische. Basierend auf unserer Analyse würde dieser Ansatz im Durchschnitt zu einer erheblichen Leistungssteigerung führen, obwohl er in den seltensten Fällen einen gewissen Overhead mit sich bringt.

Abgleich der Kriterien mit den häufigsten Suchanfragen

Vor dem Codieren und Starten der Skripts zum Ausführen dieser komplexen und möglicherweise rückgabefreien Operation validierten wir unsere Studien, indem wir uns die häufigsten Abfragen ansahen, die von unserer Back-End-Anwendung ausgeführt wurden. Dabei stellten wir jedoch fest, dass die überwiegende Mehrheit der Anfragen nur Spiele betraf, die innerhalb eines SeasonCompetition gespielt wurden. Das hat uns davon überzeugt, dass wir Recht hatten. Also haben wir alle großen Tabellen in der Datenbank mit dem gerade definierten Ansatz partitioniert.


SELECT AVG('value') as 'value', SUM('minutes') as 'minutes'
FROM 'Events'
WHERE 'parameterId' = 15 AND 'gameId' IN(223,241,245,212,201,299,187,304,187,205)
GROUP BY 'teamId'

Lassen Sie uns nun die Vor- und Nachteile dieser Entscheidung untersuchen.

Vorteile

  • Das Ausführen von Abfragen für eine Tabelle mit höchstens einer halben Million Zeilen ist viel leistungsfähiger als das Ausführen von Abfragen für eine Tabelle mit einer halben Milliarde Zeilen, insbesondere wenn es um aggregierte Abfragen geht.
  • Kleinere Tabellen sind einfacher zu verwalten und zu aktualisieren. Das Hinzufügen einer Spalte oder eines Indexes ist zeitlich und räumlich nicht einmal vergleichbar mit vorher. Plus, jeder SeasonCompetition ist anders und erfordert andere Analysen. Folglich kann es sein, dass spezielle Spalten und Indizes erforderlich sind, und die oben erwähnte Partitionierung ermöglicht es uns, damit leicht umzugehen.
  • Der Anbieter kann einige Daten ändern. Dies zwingt uns dazu, Lösch- und Aktualisierungsabfragen durchzuführen, die bei solch kleinen Tabellen unendlich schneller sind. Außerdem betreffen sie immer nur einige Spiele einer bestimmten SeasonCompetition , sodass wir jetzt nur noch an einer einzigen Tabelle arbeiten müssen.

Nachteile

  • Bevor wir eine Abfrage zu diesen Untertabellen machen, müssen wir die seasonCompetitionId kennen in Verbindung mit den Spielen von Interesse. Das liegt daran, dass die seasonCompetitionId Wert wird im Tabellennamen verwendet. Daher muss unser Back-End diese Informationen abrufen, bevor es die Abfrage durchführt, indem es sich die Spiele in der Analyse ansieht, was einen kleinen Overhead darstellt.
  • Wenn eine Abfrage eine Reihe von Spielen umfasst, die viele SeasonCompetitions umfassen , muss die Back-End-Anwendung eine Abfrage für jede Untertabelle ausführen. In diesen Fällen können wir die Daten also nicht mehr auf Datenbankebene aggregieren, sondern müssen dies auf Anwendungsebene tun. Dies führt zu einer gewissen Komplexität in der Backend-Logik. Gleichzeitig können wir diese Abfragen parallel ausführen. Außerdem können wir die abgerufenen Daten effizient und parallel aggregieren.
  • Das Verwalten einer Datenbank mit Tausenden von Tabellen ist nicht einfach und kann in einem Client eine Herausforderung darstellen. Ebenso ist das Hinzufügen einer neuen Spalte oder das Aktualisieren einer vorhandenen Spalte in jeder Tabelle umständlich und erfordert ein benutzerdefiniertes Skript.

Auswirkungen der kontextbasierten Datenpartitionierung auf die Leistung

Sehen wir uns nun die Zeitverbesserung an, die beim Ausführen einer Abfrage in der neuen partitionierten Datenbank erreicht wird.

  • Zeitverbesserung im Durchschnittsfall (Abfrage mit nur einem SeasonCompetition ):von 20x bis 40x
  • Zeitverbesserung im allgemeinen Fall (Abfrage mit einem oder mehreren SeasonCompetitions ):von 5x bis 10x

Abschließende Gedanken

Das Partitionieren Ihrer Datenbank ist zweifellos eine hervorragende Möglichkeit, die Leistung zu verbessern, insbesondere bei großen Datenbanken. Wenn Sie dies jedoch tun, ohne Ihre spezielle Anwendungsdomäne zu berücksichtigen, kann dies ein Fehler sein oder zu einer ineffizienten Lösung führen. Stattdessen ist es entscheidend, sich die Zeit zu nehmen, die Domäne zu studieren, indem Sie Experten und Ihre Benutzer befragen und sich die am häufigsten ausgeführten Abfragen ansehen, um hocheffiziente Partitionierungskriterien zu entwickeln. Dieser Artikel hat Ihnen gezeigt, wie das geht, und die Ergebnisse eines solchen Ansatzes anhand einer Fallstudie aus der Praxis demonstriert.