I denne artikel lærer du, hvordan du bruger semantikken bag dine data, når du partitionerer din database. Dette kan forbedre din applikations ydeevne drastisk. Og vigtigst af alt vil du opdage, at du bør skræddersy dine partitioneringskriterier til dit unikke applikationsdomæne.
Jeg har samarbejdet med en startup om at udvikle en web-app, hvor sportseksperter kan træffe beslutninger og udforske data. Applikationen understøtter enhver sport, men vi er baseret i Europa - og europæere elsker fodbold. Hvert af de hundredvis af spil, der spilles hver dag verden over, kommer med tusindvis af rækker. På blot et par måneder nåede begivenhedstabellen i vores app en halv milliard rækker!
Ved at forstå, hvordan fodboldeksperter forespurgte vores data, kunne vi opdele databasen intelligent. Den gennemsnitlige tidsforbedringer på dette nye bord var mellem 20x og 40x hurtigere. Den gennemsnitlige tidsforbedring på alle forespørgsler var 5X til 10X.
Lad os nu dykke ned i dette scenarie og lære, hvorfor du ikke kan ignorere din datakontekst, når du partitionerer en database.
Præsentation af konteksten
Vores sportsapplikation tilbyder både rå og aggregerede data, selvom de professionelle, der har brugt det, foretrækker sidstnævnte. Den underliggende database indeholder terabyte af komplekse, ustrukturerede, heterogene data fra flere udbydere. Så den største udfordring var at designe en pålidelig, hurtig og let-at-udforske database.
Applikationsdomæne
I denne branche tilbyder mange udbydere deres kunder adgang til begivenhederne i de vigtigste fodboldkampe. Specifikt giver de dig data relateret til, hvad der skete under en kamp, såsom mål, assists, gule kort, afleveringer og meget mere. Tabellen med disse data er langt den største, vi skulle arbejde med.
VPS-specifikationer, teknologier og arkitektur
Mit team har udviklet backend-applikationen, der giver de mest afgørende funktioner til dataudforskning. Vi adopterede Kotlin v1.6, der kører oven på en JVM (Java Virtual Machine) som programmeringssproget, Spring Boot 2.5.3 som rammen og Hibernate 5.4.32.Final som ORM (Object Relational Mapping). Hovedårsagen til, at vi valgte denne teknologistak, er, at hastighed er et af de mest afgørende forretningskrav. Så vi havde brug for en teknologi, der kunne udnytte tung flertrådsbehandling, og Spring Boot viste sig at være en pålidelig løsning.
Vi implementerede vores backend på en 16GB 8CPU VPS gennem en Docker-beholder administreret af Dokku. Den kan højst bruge 15 GB RAM. Dette skyldes, at en GB RAM er dedikeret til et Redis-baseret cachesystem. Vi tilføjede det for at forbedre ydeevnen og undgå at overbelaste backend med gentagne operationer.
Database og tabelstruktur
Med hensyn til databasen besluttede vi at vælge MySQL 8. En 8GB og 2 CPU VPS er i øjeblikket vært for databaseserveren, som understøtter op til 200 samtidige forbindelser. Backend-applikationen og databasen er i samme serverfarm for at undgå kommunikationsomkostninger. Vi designede databasestrukturen for at undgå dobbeltarbejde og med ydeevne i tankerne. Vi besluttede at vedtage en relationsdatabase, fordi vi ønskede at have en konsistent struktur til at konvertere de data, der blev modtaget fra udbyderne. På denne måde standardiserer vi sportsdataene, hvilket gør det nemmere at udforske og præsentere dem for slutbrugerne.
Databasen indeholder hundredvis af tabeller i skrivende stund, og jeg kan ikke præsentere dem alle på grund af den NDA, jeg underskrev. Heldigvis er én tabel nok til grundigt at analysere, hvorfor vi endte med at adoptere den datakontekstbaserede partition, du er ved at se. Den virkelige udfordring kom, da vi begyndte at udføre tunge forespørgsler på begivenhedstabellen. Men før vi dykker ned i det, lad os se, hvordan tabellen Begivenheder ser ud:
Som du kan se, involverer det ikke mange spalter, men husk på, at jeg var nødt til at udelade nogle af dem af fortrolighedshensyn. Men hvad egentlig spørgsmål her er parameterId
og gameId
kolonner. Vi bruger disse to fremmednøgler til at vælge en type parameter (f.eks. mål, gult kort, aflevering, straf) og de spil, hvor det skete.
Ydeevneproblemer
Begivenhedstabellen nåede en halv milliard rækker på få måneder. Som vi allerede har dækket i dybden i dette blogindlæg, er hovedproblemet, at vi skal udføre aggregerede operationer ved hjælp af langsomme IN-forespørgsler. Det er fordi, hvad der sker under et spil, ikke er så vigtigt. I stedet ønsker sportseksperter at analysere aggregerede data for at finde tendenser og træffe beslutninger baseret på dem.
Selvom de generelt analyserer hele sæsonen eller de sidste 5 eller 10 kampe, ønsker brugere ofte at ekskludere nogle bestemte spil fra deres analyse. Dette skyldes, at de ikke ønsker, at et spil spillet særlig dårligt eller godt for at polarisere deres resultater. Vi kan ikke prægenerere de samlede data, fordi vi bliver nødt til at gøre dette på alle mulige kombinationer, hvilket ikke er muligt. Så vi er nødt til at gemme alle data og aggregere dem med det samme.
Forståelse af ydeevneproblemet
Lad os nu dykke ned i det centrale aspekt, der førte til de præstationsproblemer, vi skulle stå over for.
Tabeller med millioner rækker er langsomme
Hvis du nogensinde har beskæftiget dig med tabeller, der indeholder hundreder af millioner af rækker, ved du, at de i sagens natur er langsomme. Du kan ikke engang tænke på at køre JOINs på så store borde. Alligevel kan du udføre SELECT-forespørgsler inden for en rimelig tid. Dette gælder især, når disse forespørgsler involverer simple WHERE-betingelser. På den anden side bliver de frygtelig langsomme, når man bruger aggregerede funktioner eller IN-klausuler. I disse tilfælde kan de nemt tage op til 80 sekunder, hvilket simpelthen er for meget.
Indekser er ikke nok
For at forbedre ydeevnen besluttede vi at definere nogle indekser. Dette var vores første tilgang til at finde en løsning på ydeevneproblemerne. Men det førte desværre til et andet problem. Indekser tager tid og plads. Dette er generelt ubetydeligt, men ikke når man har med så store borde at gøre. Det viste sig, at det at definere komplekse indekser baseret på de mest almindelige forespørgsler tog flere timer og GBs plads. Indekser er også nyttige, men er ikke magiske.
Datakontekstbaseret databasepartitionering som en løsning
Da vi ikke kunne løse ydeevneproblemet med specialdefinerede indekser, besluttede vi at prøve en ny tilgang. Vi talte med andre eksperter, ledte online efter løsninger, læste artikler baseret på lignende scenarier og besluttede til sidst, at opdeling af databasen var den rigtige tilgang at følge.
Hvorfor traditionel partitionering måske ikke er den rigtige tilgang
Før vi opdelte alle vores største tabeller, studerede vi emnet både i den officielle MySQL-dokumentation og i interessante artikler. Selvom vi alle var enige om, at dette var vejen at gå, indså vi også, at det ville være en fejl at anvende partitionering uden at tage vores særlige applikationsdomæne i betragtning. Konkret forstod vi, hvor afgørende det var at finde de rigtige kriterier, når en database partitioneres. Nogle eksperter i partitionering lærte os, at den traditionelle tilgang er at partitionere på antallet af rækker. Men vi ville finde noget mere intelligent og mere effektivt end det.
Dykker ned i applikationsdomænet for at finde partitioneringskriterierne
Vi lærte en vigtig lektie ved at analysere applikationsdomænet og interviewe vores brugere. Sportseksperter har en tendens til at analysere aggregerede data fra spil i samme konkurrence. For eksempel kan en konkurrence i fodbold være en liga, en turnering eller en enkelt kamp, hvor du kan vinde et trofæ. Der er tusindvis af forskellige konkurrencer. De vigtigste i Europa er Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 og Primeira Liga.
Det betyder, at vores brugere meget sjældent tager hensyn til data, der kommer fra forskellige konkurrencer. De foretrækker også at udforske data sæson for sæson. Med andre ord forlader de sjældent konteksten repræsenteret af en sportskonkurrence, der spilles i en bestemt sæson. Vores databasestruktur udtrykte dette koncept med en tabel kaldet SeasonCompetition
, hvis mål er at forbinde en konkurrence med en bestemt sæson. Så vi indså, at en god tilgang ville være at opdele vores større tabeller i undertabeller relateret til en bestemt SeasonCompetition
eksempel.
Specifikt definerede vi følgende navneformat for disse nye tabeller:<tableName>_<seasonCompetitionId>
.
Derfor, hvis vi havde 100 rækker i SeasonCompetition
tabel, bliver vi nødt til at opdele de store Events
tabellen i den mindre Events_1
, Events_2
, …, Events_100
tabeller. Baseret på vores analyse ville denne tilgang føre til et betydeligt præstationsløft i det gennemsnitlige tilfælde, selvom det i de sjældneste tilfælde vil indføre nogle overhead.
Matcher kriterierne med de mest almindelige forespørgsler
Før vi kodede og lancerede scripts for at udføre denne komplekse og potentielt returneringsfri operation, validerede vi vores undersøgelser ved at se på de mest almindelige forespørgsler udført af vores backend-applikation. Men ved at gøre det fandt vi ud af, at langt de fleste forespørgsler kun involverede spil, der blev spillet i en sæsonkonkurrence. Dette overbeviste os om, at vi havde ret. Så vi partitionerede alle de store tabeller i databasen med den netop definerede tilgang.
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'
Lad os nu studere fordele og ulemper ved denne beslutning.
Fordele
- At køre forespørgsler på en tabel, der højst indeholder en halv million rækker, er meget mere effektiv end at gøre det på en tabel med en halv milliard rækker, især når det kommer til aggregerede forespørgsler.
- Mindre tabeller er nemmere at administrere og opdatere. Tilføjelse af en kolonne eller et indeks kan ikke engang sammenlignes med tidligere, hvad angår tid og rum. Plus hver
SeasonCompetition
er anderledes og kræver forskellige analyser. Som følge heraf kan det kræve specielle kolonner og indekser, og den førnævnte opdeling giver os mulighed for nemt at håndtere dette. - Udbyderen ændrer muligvis nogle data. Dette tvinger os til at udføre slette- og opdateringsforespørgsler, som er uendeligt meget hurtigere på så små tabeller. Derudover vedrører de altid kun nogle spil i en bestemt
SeasonCompetition
, så vi behøver kun at operere på et enkelt bord nu.
Ulemper
- Før vi foretager en forespørgsel på disse undertabeller, skal vi kende
seasonCompetitionId
forbundet med spil af interesse. Dette er fordiseasonCompetitionId
værdi bruges i tabelnavnet. Derfor skal vores backend hente disse oplysninger, før de kører forespørgslen ved at se på spillene i analyse, der repræsenterer en lille overhead. - Når en forespørgsel involverer et sæt spil, der involverer mange
SeasonCompetitions
, skal backend-applikationen køre en forespørgsel på hver undertabel. Så i disse tilfælde kan vi ikke længere aggregere dataene på databaseniveau, og vi skal gøre det på applikationsniveau. Dette introducerer en vis kompleksitet i backend-logikken. Samtidig kan vi udføre disse forespørgsler parallelt. Vi kan også samle de hentede data effektivt og parallelt. - At administrere en database med tusindvis af tabeller er ikke let og kan være udfordrende at udforske i en klient. Tilsvarende er det besværligt at tilføje en ny kolonne eller opdatere en eksisterende kolonne i hver tabel og kræver et brugerdefineret script.
Effekter af datakontekstbaseret partitionering på ydeevnen
Lad os nu se på den tidsforbedring, der opnås ved udførelse af en forespørgsel i den nye partitionerede database.
- Tidsforbedring i det gennemsnitlige tilfælde (forespørgsel, der kun involverer én
SeasonCompetition
):fra 20x til 40x - Tidsforbedring i det generelle tilfælde (forespørgsel, der involverer en eller flere
SeasonCompetitions
):fra 5x til 10x
Sidste tanker
Partitionering af din database er uden tvivl en fremragende måde at forbedre ydeevnen på, især på store databaser. Men at gøre det uden at overveje dit særlige applikationsdomæne kan være en fejl eller føre til en ineffektiv løsning. I stedet er det afgørende at tage dig tid til at studere domænet ved at interviewe eksperter og dine brugere og se på de mest udførte forespørgsler for at udtænke yderst effektive opdelingskriterier. Denne artikel viste dig, hvordan du gør dette, og demonstrerede resultaterne af en sådan tilgang gennem et casestudie fra den virkelige verden.