V tomto článku se dozvíte, jak používat sémantiku za vašimi daty při rozdělení databáze. To může výrazně zlepšit výkon vaší aplikace. A co je nejdůležitější, zjistíte, že byste měli svá rozdělovací kritéria přizpůsobit své jedinečné doméně aplikace.
Spolupracoval jsem se startupem na vývoji webové aplikace pro sportovní experty, aby mohli rozhodovat a zkoumat data. Aplikace podporuje jakýkoli sport, ale sídlíme v Evropě – a Evropané milují fotbal. Každá ze stovek her hraných každý den po celém světě obsahuje tisíce řádků. Během několika měsíců dosáhla tabulka Události v naší aplikaci půl miliardy řádků!
Když jsme porozuměli tomu, jak se fotbaloví experti dotazovali na naše data, mohli jsme inteligentně rozdělit databázi. Průměrné zlepšení času na tomto novém stole bylo 20x až 40x rychlejší. Průměrné zlepšení času u všech dotazů bylo 5x až 10x.
Pojďme se nyní ponořit do tohoto scénáře a zjistit, proč nemůžete při rozdělování databáze ignorovat kontext dat.
Prezentace kontextu
Naše sportovní aplikace nabízí nezpracovaná i agregovaná data, ačkoli profesionálové, kteří ji přijali, preferují to druhé. Základní databáze obsahuje terabajty komplexních, nestrukturovaných, heterogenních dat od několika poskytovatelů. Největší výzvou tedy bylo navrhnout spolehlivou, rychlou a snadno prozkoumatelnou databázi.
Doména aplikace
V tomto odvětví mnoho poskytovatelů nabízí svým klientům přístup k událostem nejdůležitějších fotbalových zápasů. Konkrétně vám poskytují data související s tím, co se stalo během hry, jako jsou góly, asistence, žluté karty, přihrávky a mnoho dalšího. Tabulka obsahující tato data je zdaleka největší, se kterou jsme museli pracovat.
Specifikace, technologie a architektura VPS
Můj tým vyvíjel backendovou aplikaci, která poskytuje nejdůležitější funkce pro průzkum dat. Přijali jsme Kotlin v1.6 běžící na JVM (Java Virtual Machine) jako programovací jazyk, Spring Boot 2.5.3 jako framework a Hibernate 5.4.32.Final jako ORM (Object Relational Mapping). Hlavním důvodem, proč jsme se rozhodli pro tuto sadu technologií, je to, že rychlost je jedním z nejdůležitějších obchodních požadavků. Potřebovali jsme tedy technologii, která dokáže využít náročné vícevláknové zpracování, a Spring Boot se ukázal jako spolehlivé řešení.
Náš backend jsme nasadili na 16GB 8CPU VPS prostřednictvím kontejneru Docker spravovaného Dokku. Může využít maximálně 15 GB RAM. Je to proto, že jeden GB paměti RAM je vyhrazen pro systém mezipaměti založený na Redis. Přidali jsme jej, abychom zlepšili výkon a zabránili přetěžování backendu opakovanými operacemi.
Struktura databáze a tabulky
Pokud jde o databázi, rozhodli jsme se pro MySQL 8. Databázový server v současné době hostí 8GB a 2 CPU VPS, který podporuje až 200 souběžných připojení. Backendová aplikace a databáze jsou ve stejné serverové farmě, aby nedocházelo k režii komunikace. Strukturu databáze jsme navrhli tak, aby se zabránilo duplicitě as ohledem na výkon. Rozhodli jsme se přijmout relační databázi, protože jsme chtěli mít konzistentní strukturu pro převod dat obdržených od poskytovatelů. Tímto způsobem standardizujeme sportovní data, což usnadňuje jejich zkoumání a prezentaci koncovým uživatelům.
Databáze obsahuje v době psaní stovky tabulek a nemohu je všechny prezentovat kvůli NDA, kterou jsem podepsal. Naštěstí jedna tabulka stačí k důkladné analýze toho, proč jsme nakonec přijali datový kontextový oddíl, který se chystáte vidět. Skutečná výzva přišla, když jsme začali provádět těžké dotazy na tabulce Události. Než se do toho ale ponoříme, podívejme se, jak vypadá tabulka Události:
Jak vidíte, nezahrnuje mnoho sloupců, ale mějte na paměti, že některé z nich jsem musel z důvodu zachování důvěrnosti vynechat. Ale co skutečně důležité jsou zde parameterId
a gameId
sloupců. Tyto dva cizí klíče používáme k výběru typu parametru (např. gól, žlutá karta, přihrávka, penalta) a her, ve kterých k tomu došlo.
Problémy s výkonem
Tabulka Události dosáhla během několika měsíců půl miliardy řádků. Jak jsme se již do hloubky zabývali v tomto blogovém příspěvku, hlavním problémem je, že musíme provádět agregované operace pomocí pomalých IN dotazů. Je to proto, že to, co se děje během hry, není tak důležité. Namísto toho chtějí sportovní odborníci analyzovat agregovaná data, aby našli trendy a na základě nich činili rozhodnutí.
I když obecně analyzují celou sezónu nebo posledních 5 nebo 10 zápasů, uživatelé často chtějí některé konkrétní hry ze své analýzy vyloučit. Je to proto, že nechtějí, aby hra zahraná zvlášť špatně nebo dobře polarizovala jejich výsledky. Nemůžeme předem vygenerovat agregovaná data, protože bychom to museli udělat pro všechny možné kombinace, což není proveditelné. Musíme tedy ukládat všechna data a agregovat je za běhu.
Pochopení problému s výkonem
Nyní se pojďme ponořit do hlavního aspektu, který vedl k problémům s výkonem, kterým jsme museli čelit.
Tabulky s milionem řádků jsou pomalé
Pokud jste se někdy zabývali tabulkami obsahujícími stovky milionů řádků, víte, že jsou ze své podstaty pomalé. Na tak velkých stolech vás ani nenapadne spouštět JOINy. Přesto můžete provádět SELECT dotazy v rozumném čase. To platí zejména v případě, že tyto dotazy zahrnují jednoduché podmínky WHERE. Na druhou stranu jsou strašně pomalé při použití agregačních funkcí nebo klauzulí IN. V těchto případech mohou snadno trvat až 80 sekund, což je prostě příliš mnoho.
Indexy nestačí
Pro zlepšení výkonu jsme se rozhodli definovat některé indexy. Toto byl náš první přístup k nalezení řešení problémů s výkonem. To ale bohužel vedlo k dalšímu problému. Indexy zabírají čas a prostor. To je obecně bezvýznamné, ale ne při práci s tak velkými tabulkami. Ukázalo se, že definování složitých indexů na základě nejběžnějších dotazů zabralo několik hodin a GB místa. Indexy jsou také užitečné, ale nejsou kouzelné.
Kontextové dělení databází na oddíly jako řešení
Protože jsme nemohli vyřešit problém s výkonem pomocí vlastních indexů, rozhodli jsme se vyzkoušet nový přístup. Mluvili jsme s dalšími odborníky, hledali online řešení, četli jsme články založené na podobných scénářích a nakonec jsme se rozhodli, že rozdělení databáze na oddíly je ten správný přístup.
Proč tradiční dělení nemusí být tím správným přístupem
Před rozdělením všech našich největších tabulek jsme toto téma prostudovali jak v oficiální dokumentaci MySQL, tak v zajímavých článcích. Ačkoli jsme se všichni shodli, že toto je správná cesta, také jsme si uvědomili, že použití rozdělení bez zohlednění naší konkrétní aplikační domény by byla chyba. Konkrétně jsme pochopili, jak důležité bylo najít správná kritéria při rozdělování databáze. Někteří odborníci na dělení nás naučili, že tradičním přístupem je dělení podle počtu řádků. Ale chtěli jsme najít něco inteligentnějšího a efektivnějšího.
Ponoření se do aplikační domény za účelem nalezení kritérií rozdělení
Analýzou aplikační domény a rozhovory s našimi uživateli jsme se naučili zásadní lekci. Sportovní experti mají tendenci analyzovat agregovaná data z her ve stejné soutěži. Fotbalová soutěž může být například liga, turnaj nebo jeden zápas, ve kterém můžete vyhrát trofej. Existují tisíce různých soutěží. Nejdůležitější v Evropě jsou Liga mistrů, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 a Primeira Liga.
To znamená, že naši uživatelé berou v úvahu data pocházející z různých soutěží velmi zřídka. Také upřednostňují prozkoumávání dat sezónu po sezóně. Jinými slovy, málokdy opouštějí kontext reprezentovaný sportovní soutěží hranou v určité sezóně. Naše databázová struktura vyjádřila tento koncept tabulkou nazvanou SeasonCompetition
, jehož cílem je spojit soutěž s konkrétní sezónou. Uvědomili jsme si tedy, že dobrým přístupem by bylo rozdělit naše větší tabulky na podtabulky související s konkrétní SeasonCompetition
instance.
Konkrétně jsme definovali následující formát názvu pro tyto nové tabulky:<tableName>_<seasonCompetitionId>
.
Pokud bychom tedy měli v SeasonCompetition
100 řádků museli bychom rozdělit velké Events
tabulky do menší Events_1
, Events_2
, …, Events_100
tabulky. Na základě naší analýzy by tento přístup v průměrném případě vedl ke značnému zvýšení výkonu, i když v nejvzácnějších případech představuje určitou režii.
Shoda kritérií s nejběžnějšími dotazy
Před kódováním a spuštěním skriptů pro provedení této složité a potenciálně nevratné operace jsme ověřili naše studie tím, že jsme se podívali na nejčastější dotazy prováděné naší backendovou aplikací. Zjistili jsme však, že velká většina dotazů se týkala pouze her hraných v rámci sezónní soutěže. To nás přesvědčilo, že jsme měli pravdu. Takže jsme rozdělili všechny velké tabulky v databázi právě definovaným přístupem.
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'
Pojďme si nyní prostudovat klady a zápory tohoto rozhodnutí.
Klady
- Spouštění dotazů na tabulce obsahující nejvýše půl milionu řádků je mnohem výkonnější než na tabulce s půl miliardou řádků, zejména pokud jde o souhrnné dotazy.
- Menší tabulky se snáze spravují a aktualizují. Přidání sloupce nebo indexu není z hlediska času a prostoru ani srovnatelné s dříve. Navíc každá
SeasonCompetition
je jiný a vyžaduje různé analýzy. V důsledku toho může vyžadovat speciální sloupce a indexy a výše uvedené rozdělení nám umožňuje snadno se s tím vypořádat. - Poskytovatel může některá data upravit. To nás nutí provádět mazací a aktualizační dotazy, které jsou na tak malých tabulkách nekonečně rychlejší. Navíc se vždy týkají pouze některých her konkrétní
SeasonCompetition
, takže nyní potřebujeme operovat pouze na jednom stole.
Nevýhody
- Než provedeme dotaz na tyto podtabulky, potřebujeme znát
seasonCompetitionId
spojené se zájmovými hrami. Důvodem jeseasonCompetitionId
hodnota se používá v názvu tabulky. Náš backend proto potřebuje získat tyto informace před spuštěním dotazu tak, že se podívá na analýzy her, což představuje malou režii. - Pokud dotaz zahrnuje sadu her, které zahrnují mnoho
SeasonCompetitions
, musí backendová aplikace spustit dotaz na každou podtabulku. Takže v těchto případech již nemůžeme agregovat data na úrovni databáze a musíme to udělat na úrovni aplikace. To zavádí určitou složitost v logice backendu. Zároveň můžeme tyto dotazy provádět paralelně. Také můžeme agregovat získaná data efektivně a paralelně. - Správa databáze s tisíci tabulkami není snadná a její prozkoumání v klientovi může být náročné. Podobně přidání nového sloupce nebo aktualizace existujícího sloupce v každé tabulce je těžkopádné a vyžaduje vlastní skript.
Vliv dělení dat na kontextové oddíly na výkon
Podívejme se nyní na zlepšení času dosaženého při provádění dotazu v nové rozdělené databázi.
- Zvýšení času v průměrném případě (dotaz zahrnující pouze jednu soutěž
SeasonCompetition
):od 20x do 40x - Zvýšení času v obecném případě (dotaz zahrnující jednu nebo více
SeasonCompetitions
):od 5x do 10x
Závěrečné myšlenky
Rozdělení databáze je nepochybně vynikající způsob, jak zlepšit výkon, zejména u velkých databází. Pokud to však uděláte bez zohlednění vaší konkrétní aplikační domény, může to být chyba nebo vést k neefektivnímu řešení. Místo toho věnovat čas studiu domény rozhovory s odborníky a vašimi uživateli a sledováním nejčastěji prováděných dotazů je zásadní pro vytvoření vysoce účinných kritérií pro rozdělení. Tento článek vám ukázal, jak to udělat, a demonstroval výsledky takového přístupu prostřednictvím případové studie z reálného světa.