Mysql
 sql >> Database >  >> RDS >> Mysql

Partizionamento di una tabella di miliardi di righe di dati sul calcio utilizzando il contesto dei dati

In questo articolo imparerai come usare la semantica dietro i tuoi dati quando partizioni il tuo database. Questo può migliorare drasticamente le prestazioni della tua applicazione. E, soprattutto, scoprirai che dovresti adattare i tuoi criteri di partizionamento al tuo dominio di applicazione univoco.

Ho collaborato con una startup per sviluppare un'app web per gli esperti di sport per prendere decisioni ed esplorare i dati. L'applicazione supporta qualsiasi sport, ma la nostra sede è in Europa e gli europei adorano il calcio. Ciascuna delle centinaia di giochi giocati ogni giorno in tutto il mondo ha migliaia di righe. In pochi mesi, la tabella Eventi nella nostra app ha raggiunto mezzo miliardo di righe!

Comprendendo come gli esperti di calcio stavano interrogando i nostri dati, potremmo partizionare il database in modo intelligente. Il miglioramento del tempo medio su questo nuovo tavolo è stato tra 20x e 40x più veloce. Il miglioramento del tempo medio su tutte le query è stato compreso tra 5 volte e 10 volte.

Analizziamo ora questo scenario e scopriamo perché non è possibile ignorare il contesto dei dati durante il partizionamento di un database.

Presentazione del contesto

La nostra applicazione sportiva offre dati sia grezzi che aggregati, anche se i professionisti che l'hanno adottata preferiscono quest'ultimo. Il database sottostante contiene terabyte di dati complessi, non strutturati ed eterogenei provenienti da diversi provider. Quindi, la sfida più grande è stata la progettazione di un database affidabile, veloce e facile da esplorare.

Dominio dell'applicazione

In questo settore, molti fornitori offrono ai propri clienti l'accesso agli eventi delle partite di calcio più importanti. Nello specifico, ti forniscono dati relativi a ciò che è successo durante una partita, come goal, assist, cartellini gialli, passaggi e molto altro. La tabella contenente questi dati è di gran lunga la più grande con cui abbiamo dovuto lavorare.

Specifiche, tecnologie e architettura VPS

Il mio team ha sviluppato l'applicazione back-end che fornisce le funzionalità di esplorazione dei dati più importanti. Abbiamo adottato Kotlin v1.6 in esecuzione su una JVM (Java Virtual Machine) come linguaggio di programmazione, Spring Boot 2.5.3 come framework e Hibernate 5.4.32.Final come ORM (Object Relational Mapping). Il motivo principale per cui abbiamo optato per questo stack tecnologico è che la velocità è uno dei requisiti aziendali più importanti. Quindi, avevamo bisogno di una tecnologia in grado di sfruttare un'elaborazione multi-thread pesante e Spring Boot si è rivelata una soluzione affidabile.

Abbiamo implementato il nostro back-end su un VPS da 16 GB e 8 CPU tramite un container Docker gestito da Dokku. Può utilizzare al massimo 15 GB di RAM. Questo perché un GB di RAM è dedicato a un sistema di memorizzazione nella cache basato su Redis. L'abbiamo aggiunto per migliorare le prestazioni ed evitare di sovraccaricare il backend con operazioni ripetute.

Struttura del database e delle tabelle

Per quanto riguarda il database, abbiamo deciso di optare per MySQL 8. Un VPS da 8 GB e 2 CPU attualmente ospita il server del database, che supporta fino a 200 connessioni simultanee. L'applicazione back-end e il database si trovano nella stessa server farm per evitare un sovraccarico di comunicazione. Abbiamo progettato la struttura del database in modo da evitare duplicazioni e tenendo conto delle prestazioni. Abbiamo deciso di adottare un database relazionale perché volevamo avere una struttura coerente per convertire i dati ricevuti dai provider. In questo modo, standardizziamo i dati sportivi, semplificando l'esplorazione e la presentazione agli utenti finali.

Il database contiene centinaia di tabelle al momento della scrittura e non posso presentarle tutte a causa dell'NDA che ho firmato. Fortunatamente, una tabella è sufficiente per analizzare a fondo il motivo per cui abbiamo finito per adottare la partizione basata sul contesto dei dati che stai per vedere. La vera sfida è arrivata quando abbiamo iniziato a eseguire query pesanti sulla tabella Eventi. Ma prima di approfondire, vediamo come appare la tabella Eventi:

Come puoi vedere, non coinvolge molte colonne, ma tieni presente che ho dovuto ometterne alcune per motivi di riservatezza. Ma cosa davvero le questioni qui sono il parameterId e gameId colonne. Usiamo queste due chiavi esterne per selezionare un tipo di parametro (es. goal, cartellino giallo, passaggio, rigore) e le partite in cui è successo.

Problemi di prestazioni

La tabella Eventi ha raggiunto mezzo miliardo di righe in pochi mesi. Come abbiamo già trattato in modo approfondito in questo post del blog, il problema principale è che dobbiamo eseguire operazioni aggregate utilizzando query IN lente. Questo perché ciò che accade durante una partita non è così importante. Invece, gli esperti di sport vogliono analizzare i dati aggregati per trovare le tendenze e prendere decisioni basate su di esse.

Inoltre, sebbene generalmente analizzino l'intera stagione o le ultime 5 o 10 partite, gli utenti spesso vogliono escludere dalla loro analisi alcune partite particolari. Questo perché non vogliono che una partita giocata particolarmente male o bene polarizzi i loro risultati. Non possiamo pre-generare i dati aggregati perché dovremmo farlo su tutte le possibili combinazioni, il che non è fattibile. Quindi, dobbiamo archiviare tutti i dati e aggregarli al volo.

Comprendere il problema delle prestazioni

Ora, tuffiamoci nell'aspetto centrale che ha portato ai problemi di prestazioni che abbiamo dovuto affrontare.

Le tabelle da un milione di righe sono lente

Se hai mai avuto a che fare con tabelle contenenti centinaia di milioni di righe, sai che sono intrinsecamente lente. Non puoi nemmeno pensare di eseguire JOIN su tavoli così grandi. Tuttavia, puoi eseguire query SELECT in un ragionevole lasso di tempo. Ciò è particolarmente vero quando queste query coinvolgono semplici condizioni WHERE. D'altra parte, diventano terribilmente lenti quando si utilizzano funzioni aggregate o clausole IN. In questi casi, possono facilmente volerci fino a 80 secondi, il che è semplicemente troppo.

Gli indici non bastano

Per migliorare la performance, abbiamo deciso di definire alcuni indici. Questo è stato il nostro primo approccio per trovare una soluzione ai problemi di prestazioni. Ma, sfortunatamente, questo ha portato a un altro problema. Gli indici richiedono tempo e spazio. Questo è generalmente insignificante, ma non quando si tratta di tavoli così grandi. Si è scoperto che la definizione di indici complessi basati sulle query più comuni richiedeva diverse ore e GB di spazio. Inoltre, gli indici sono utili ma non magici.

Partizionamento del database basato sul contesto dei dati come soluzione

Poiché non siamo riusciti a risolvere il problema delle prestazioni con indici personalizzati, abbiamo deciso di provare un nuovo approccio. Abbiamo parlato con altri esperti, cercato soluzioni online, letto articoli basati su scenari simili e alla fine abbiamo deciso che il partizionamento del database era l'approccio giusto da seguire.

Perché il partizionamento tradizionale potrebbe non essere l'approccio giusto

Prima di partizionare tutte le nostre tabelle più grandi, abbiamo studiato l'argomento sia nella documentazione ufficiale di MySQL che in articoli interessanti. Sebbene fossimo tutti d'accordo sul fatto che questa fosse la strada da percorrere, ci siamo anche resi conto che applicare il partizionamento senza prendere in considerazione il nostro particolare dominio applicativo sarebbe stato un errore. In particolare, abbiamo capito quanto fosse fondamentale trovare i criteri corretti durante il partizionamento di un database. Alcuni esperti di partizionamento ci hanno insegnato che l'approccio tradizionale consiste nel partizionare sul numero di righe. Ma volevamo trovare qualcosa di più intelligente ed efficiente di così.

Approfondimento nel dominio dell'applicazione per trovare i criteri di partizionamento

Abbiamo imparato una lezione essenziale analizzando il dominio dell'applicazione e intervistando i nostri utenti. Gli esperti di sport tendono ad analizzare i dati aggregati delle partite della stessa competizione. Ad esempio, una competizione di calcio può essere un campionato, un torneo o una singola partita in cui puoi vincere un trofeo. Ci sono migliaia di competizioni diverse. Le più importanti in Europa sono Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 e Primeira Liga.

Ciò significa che i nostri utenti prendono in considerazione molto raramente i dati provenienti da diverse competizioni. Inoltre, preferiscono esplorare i dati stagione per stagione. In altre parole, raramente escono dal contesto rappresentato da una competizione sportiva giocata in una determinata stagione. La nostra struttura del database ha espresso questo concetto con una tabella chiamata SeasonCompetition , il cui obiettivo è associare una competizione a una stagione specifica. Quindi, ci siamo resi conto che un buon approccio sarebbe stato quello di suddividere le nostre tabelle più grandi in sottotabelle relative a una particolare SeasonCompetition esempio.

Nello specifico, abbiamo definito il seguente formato del nome per queste nuove tabelle:<tableName>_<seasonCompetitionId> .

Di conseguenza, se avessimo 100 righe nella SeasonCompetition tabella, dovremmo dividere i grandi Events tabella nel più piccolo Events_1 , Events_2 , …, Events_100 tavoli. Sulla base della nostra analisi, questo approccio porterebbe a un notevole aumento delle prestazioni nel caso medio, sebbene introducendo un sovraccarico nei casi più rari.

Corrispondenza dei criteri con le query più comuni

Prima di codificare e avviare gli script per eseguire questa operazione complessa e potenzialmente senza ritorno, abbiamo convalidato i nostri studi esaminando le query più comuni eseguite dalla nostra applicazione di back-end. Ma così facendo, abbiamo scoperto che la stragrande maggioranza delle domande riguardava solo le partite giocate all'interno di una SeasonCompetition. Questo ci ha convinto che avevamo ragione. Quindi abbiamo partizionato tutte le tabelle di grandi dimensioni nel database con l'approccio appena definito.


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'

Ora, studiamo i pro ei contro di questa decisione.

Pro

  • L'esecuzione di query su una tabella contenente al massimo mezzo milione di righe è molto più efficiente che su una tabella di mezzo miliardo di righe, soprattutto quando si tratta di query aggregate.
  • Le tabelle più piccole sono più facili da gestire e aggiornare. L'aggiunta di una colonna o di un indice non è nemmeno paragonabile a prima in termini di tempo e spazio. Inoltre, ogni SeasonCompetition è diverso e richiede analisi diverse. Di conseguenza, potrebbe richiedere colonne e indici speciali e il suddetto partizionamento ci consente di gestirlo facilmente.
  • Il provider potrebbe modificare alcuni dati. Questo ci costringe a eseguire query di eliminazione e aggiornamento, che sono infinitamente più veloci su tabelle così piccole. Inoltre, riguardano sempre solo alcuni giochi di una particolare SeasonCompetition , quindi ora dobbiamo solo operare solo su un singolo tavolo.

Contro

  • Prima di effettuare una query su queste sottotabelle, dobbiamo conoscere il seasonCompetitionId associati ai giochi di interesse. Questo perché il seasonCompetitionId value viene utilizzato nel nome della tabella. Pertanto, il nostro back-end deve recuperare queste informazioni prima di eseguire la query osservando i giochi in analisi, che rappresentano un piccolo sovraccarico.
  • Quando una query riguarda un insieme di giochi che coinvolgono molte SeasonCompetitions , l'applicazione back-end deve eseguire una query su ogni sottotabella. Quindi, in questi casi, non possiamo più aggregare i dati a livello di database e dobbiamo farlo a livello di applicazione. Ciò introduce una certa complessità nella logica di back-end. Allo stesso tempo, possiamo eseguire queste query in parallelo. Inoltre, possiamo aggregare i dati recuperati in modo efficiente e in parallelo.
  • Gestire un database con migliaia di tabelle non è facile e può essere difficile da esplorare in un client. Allo stesso modo, l'aggiunta di una nuova colonna o l'aggiornamento di una colonna esistente in ogni tabella è ingombrante e richiede uno script personalizzato.

Effetti del partizionamento basato sul contesto dei dati sulle prestazioni

Esaminiamo ora il miglioramento del tempo ottenuto durante l'esecuzione di una query nel nuovo database partizionato.

  • Miglioramento del tempo nel caso medio (query che coinvolge un solo SeasonCompetition ):da 20x a 40x
  • Miglioramento del tempo nel caso generale (query che coinvolge una o più SeasonCompetitions ):da 5x a 10x

Considerazioni finali

Il partizionamento del database è senza dubbio un ottimo modo per migliorare le prestazioni, soprattutto su database di grandi dimensioni. Tuttavia, farlo senza considerare il tuo particolare dominio dell'applicazione potrebbe essere un errore o portare a una soluzione inefficiente. Invece, dedicare del tempo allo studio del dominio intervistando esperti e utenti e osservando le query più eseguite è fondamentale per concepire criteri di partizionamento altamente efficienti. Questo articolo ti ha mostrato come farlo e ha dimostrato i risultati di un tale approccio attraverso un caso di studio nel mondo reale.