Più sviluppatori dovrebbero saperlo sui database

Nota. trad.: Jaana Dogan è un ingegnere esperto di Google che attualmente sta lavorando sull'osservabilità dei servizi di produzione dell'azienda scritti in Go. In questo articolo, che ha riscosso grande successo tra il pubblico di lingua inglese, ha raccolto in 17 punti importanti dettagli tecnici riguardanti i DBMS (e talvolta i sistemi distribuiti in generale) che sono utili da considerare per gli sviluppatori di applicazioni grandi/richieste.

Più sviluppatori dovrebbero saperlo sui database

La stragrande maggioranza dei sistemi informatici tiene traccia del proprio stato e, di conseguenza, richiede un qualche tipo di sistema di archiviazione dei dati. Ho accumulato conoscenze sui database per un lungo periodo di tempo, commettendo nel frattempo errori di progettazione che hanno portato a perdite e interruzioni dei dati. Nei sistemi che elaborano grandi volumi di informazioni, i database rappresentano il cuore dell’architettura del sistema e fungono da elemento chiave nella scelta della soluzione ottimale. Nonostante si presti molta attenzione al lavoro del database, i problemi che gli sviluppatori di applicazioni cercano di anticipare spesso sono solo la punta dell'iceberg. In questa serie di articoli condivido alcune idee che saranno utili agli sviluppatori non specializzati in questo campo.

  1. Sei fortunato se il 99,999% delle volte la rete non causa problemi.
  2. ACIDO significa molte cose diverse.
  3. Ogni database ha i propri meccanismi per garantire coerenza e isolamento.
  4. Il blocco ottimistico viene in soccorso quando è difficile mantenere quello abituale.
  5. Ci sono altre anomalie oltre alle letture sporche e alla perdita di dati.
  6. Il database e l'utente non sempre concordano sulla linea d'azione.
  7. Lo sharding a livello di applicazione può essere spostato all'esterno dell'applicazione.
  8. L'incremento automatico può essere pericoloso.
  9. I dati obsoleti possono essere utili e non è necessario bloccarli.
  10. Le distorsioni sono tipiche di qualsiasi sorgente temporale.
  11. Il ritardo ha molti significati.
  12. I requisiti prestazionali dovrebbero essere valutati per una transazione specifica.
  13. Le transazioni nidificate possono essere pericolose.
  14. Le transazioni non devono essere legate allo stato dell'applicazione.
  15. I pianificatori di query possono dirti molto sui database.
  16. La migrazione online è difficile, ma possibile.
  17. Un aumento significativo del database comporta un aumento dell'imprevedibilità.

Vorrei ringraziare Emmanuel Odeke, Rein Henrichs e altri per il loro feedback su una versione precedente di questo articolo.

Sei fortunato se il 99,999% delle volte la rete non causa problemi.

Rimane la domanda su quanto siano affidabili le moderne tecnologie di rete e quanto spesso i sistemi siano inattivi a causa di guasti alla rete. Le informazioni su questo tema sono scarse e la ricerca è spesso dominata da grandi organizzazioni con reti, attrezzature e personale specializzati.

Con un tasso di disponibilità del 99,999% per Spanner (il database distribuito a livello globale di Google), Google afferma che solo 7,6% i problemi sono legati alla rete. Allo stesso tempo, l’azienda definisce la propria rete specializzata il “pilastro principale” dell’alta disponibilità. Studio Bailis e Kingsbury, condotto nel 2014, sfida uno dei “idee sbagliate sul calcolo distribuito", formulato da Peter Deutsch nel 1994. La rete è davvero affidabile?

Una ricerca completa al di fuori delle grandi aziende, condotta per Internet nel suo complesso, semplicemente non esiste. Inoltre non ci sono dati sufficienti da parte dei principali attori su quale percentuale dei problemi dei loro clienti sia legata alla rete. Siamo ben consapevoli delle interruzioni nello stack di rete dei grandi fornitori di servizi cloud che possono bloccare un'intera porzione di Internet per diverse ore semplicemente perché si tratta di eventi di alto profilo che colpiscono un gran numero di persone e aziende. Le interruzioni della rete possono causare problemi in molti più casi, anche se non tutti questi casi sono sotto i riflettori. Anche i clienti dei servizi cloud non sanno nulla delle cause dei problemi. Se si verifica un guasto, è quasi impossibile attribuirlo a un errore di rete da parte del fornitore di servizi. Per loro, i servizi di terze parti sono scatole nere. È impossibile valutare l’impatto senza essere un grande fornitore di servizi.

Considerato ciò che i grandi player riportano sui loro sistemi, è sicuro dire che sei fortunato se le difficoltà di rete rappresentano solo una piccola percentuale di potenziali problemi di inattività. Le comunicazioni di rete soffrono ancora di cose banali come guasti hardware, modifiche alla topologia, modifiche alla configurazione amministrativa e interruzioni di corrente. Recentemente sono rimasto sorpreso nell'apprendere che è stato aggiunto l'elenco dei possibili problemi morsi di squalo (sì, hai sentito bene).

ACIDO significa molte cose diverse

L'acronimo ACID sta per Atomicità, Coerenza, Isolamento, Affidabilità. Queste proprietà delle transazioni hanno lo scopo di garantirne la validità in caso di guasti, errori, guasti hardware, ecc. Senza ACID o schemi simili, sarebbe difficile per gli sviluppatori di applicazioni distinguere tra ciò di cui sono responsabili e ciò di cui è responsabile il database. La maggior parte dei database transazionali relazionali tenta di essere conforme ad ACID, ma nuovi approcci come NoSQL hanno dato origine a molti database senza transazioni ACID perché sono costosi da implementare.

Quando sono entrato per la prima volta nel settore, il nostro responsabile tecnico ha parlato di quanto fosse rilevante il concetto ACID. Per essere onesti, ACID è considerato una descrizione approssimativa piuttosto che uno standard di implementazione rigoroso. Oggi lo trovo utile soprattutto perché solleva una specifica categoria di problemi (e suggerisce una serie di possibili soluzioni).

Non tutti i DBMS sono conformi ad ACID; Allo stesso tempo, le implementazioni di database che supportano ACID comprendono l'insieme di requisiti in modo diverso. Uno dei motivi per cui le implementazioni ACID sono discontinue è dovuto ai numerosi compromessi che devono essere fatti per implementare i requisiti ACID. I creatori possono presentare i propri database come conformi ad ACID, ma l'interpretazione dei casi limite può variare notevolmente, così come il meccanismo per gestire eventi "improbabili". Come minimo, gli sviluppatori possono acquisire una comprensione di alto livello delle complessità delle implementazioni di base per comprendere adeguatamente il loro comportamento speciale e i compromessi di progettazione.

Il dibattito sulla conformità di MongoDB ai requisiti ACID continua anche dopo il rilascio della versione 4. MongoDB non è supportato da molto tempo registrazione, anche se per impostazione predefinita i dati venivano salvati su disco non più di una volta ogni 60 secondi. Immagina il seguente scenario: un'applicazione pubblica due scritture (w1 e w2). MongoDB memorizza con successo w1, ma w2 viene perso a causa di un guasto hardware.

Più sviluppatori dovrebbero saperlo sui database
Diagramma che illustra lo scenario. MongoDB si arresta in modo anomalo prima di poter scrivere i dati su disco

Il commit su disco è un processo costoso. Evitando commit frequenti, gli sviluppatori migliorano le prestazioni di registrazione a scapito dell'affidabilità. MongoDB attualmente supporta il logging, ma le scritture dirty possono comunque avere un impatto sull'integrità dei dati poiché i log vengono acquisiti ogni 100 ms per impostazione predefinita. Cioè, uno scenario simile è ancora possibile per i log e le modifiche in essi presentate, sebbene il rischio sia molto inferiore.

Ogni database ha i propri meccanismi di coerenza e isolamento

Tra i requisiti ACID, coerenza e isolamento vantano il maggior numero di implementazioni diverse perché la gamma di compromessi è più ampia. Va detto che la coerenza e l'isolamento sono funzioni piuttosto costose. Richiedono coordinamento e aumentano la concorrenza per la coerenza dei dati. La complessità del problema aumenta notevolmente quando è necessario scalare orizzontalmente il database su più data center (soprattutto se ubicati in regioni geografiche diverse). Raggiungere un livello elevato di coerenza è molto difficile, poiché riduce anche la disponibilità e aumenta la segmentazione della rete. Per una spiegazione più generale di questo fenomeno vi consiglio di fare riferimento a Teorema del CAP. Vale anche la pena notare che le applicazioni possono gestire piccole quantità di incoerenza e i programmatori possono comprendere le sfumature del problema abbastanza bene da implementare logica aggiuntiva nell'applicazione per gestire l'incoerenza senza fare molto affidamento sul database per gestirla.

I DBMS spesso forniscono diversi livelli di isolamento. Gli sviluppatori di applicazioni possono scegliere quella più efficace in base alle loro preferenze. Il basso isolamento consente una maggiore velocità, ma aumenta anche il rischio di una corsa ai dati. Un isolamento elevato riduce questa probabilità, ma rallenta il lavoro e può portare alla concorrenza, che porterà a tali freni nella base che iniziano i guasti.

Più sviluppatori dovrebbero saperlo sui database
Revisione dei modelli di concorrenza esistenti e delle relazioni tra loro

Lo standard SQL definisce solo quattro livelli di isolamento, anche se in teoria e in pratica ce ne sono molti di più. Jepson.io offre un'eccellente panoramica dei modelli di concorrenza esistenti. Ad esempio, Google Spanner garantisce la serializzabilità esterna con la sincronizzazione dell'orologio e, sebbene si tratti di un livello di isolamento più rigoroso, non è definito nei livelli di isolamento standard.

Lo standard SQL menziona i seguenti livelli di isolamento:

  • Serializzabile (più rigoroso e costoso): l'esecuzione serializzabile ha lo stesso effetto dell'esecuzione di alcune transazioni sequenziali. L'esecuzione sequenziale significa che ogni transazione successiva inizia solo dopo il completamento di quella precedente. Va notato che il livello Serializzabile spesso implementato come cosiddetto isolamento degli snapshot (ad esempio, in Oracle) a causa delle differenze di interpretazione, sebbene l'isolamento degli snapshot stesso non sia rappresentato nello standard SQL.
  • Letture ripetibili: i record non impegnati nella transazione corrente sono disponibili per la transazione corrente, ma le modifiche apportate da altre transazioni (come nuove righe) non visibile.
  • Leggi impegnato: i dati non impegnati non sono disponibili per le transazioni. In questo caso, le transazioni possono vedere solo i dati salvati e potrebbero verificarsi letture fantasma. Se una transazione inserisce e conferma nuove righe, la transazione corrente sarà in grado di vederle quando interrogata.
  • Leggi senza impegno (livello meno rigoroso e costoso): sono consentite letture sporche, le transazioni possono vedere le modifiche non confermate apportate da altre transazioni. In pratica, questo livello può essere utile per stime approssimative, come le query COUNT(*) sul tavolo.

Livello Serializzabile riduce al minimo il rischio di corse di dati, pur essendo il più costoso da implementare e comportando il carico competitivo più elevato sul sistema. Altri livelli di isolamento sono più facili da implementare, ma aumentano la probabilità di corse di dati. Alcuni DBMS consentono di impostare un livello di isolamento personalizzato, altri hanno preferenze forti e non tutti i livelli sono supportati.

Il supporto per i livelli di isolamento viene spesso pubblicizzato in un determinato DBMS, ma solo uno studio attento del suo comportamento può rivelare cosa sta realmente accadendo.

Più sviluppatori dovrebbero saperlo sui database
Revisione delle anomalie di concorrenza a diversi livelli di isolamento per diversi DBMS

Martin Kleppmann nel suo progetto eremo Confronta diversi livelli di isolamento, parla di anomalie di concorrenza e se il database è in grado di aderire a un particolare livello di isolamento. La ricerca di Kleppmann mostra in che modo gli sviluppatori di database pensano diversamente ai livelli di isolamento.

Il blocco ottimistico viene in soccorso quando è difficile mantenere quello abituale.

Il blocco può essere molto costoso, non solo perché aumenta la concorrenza nel database, ma anche perché richiede che i server delle applicazioni si connettano costantemente al database. La segmentazione della rete può esacerbare situazioni di blocco esclusivo e portare a blocchi difficili da identificare e risolvere. Nei casi in cui il blocco esclusivo non è adatto, il blocco ottimistico aiuta.

Blocco ottimista è un metodo in cui durante la lettura di una stringa tiene conto della sua versione, checksum o ora dell'ultima modifica. Ciò ti consente di assicurarti che non vi sia alcuna modifica della versione atomica prima di modificare una voce:

UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1

In questo caso, aggiornando la tabella products non verrà eseguita se un'altra operazione ha precedentemente apportato modifiche a questa riga. Se non sono state eseguite altre operazioni su questa riga, verrà effettuata la modifica per una riga e potremo dire che l'aggiornamento è andato a buon fine.

Ci sono altre anomalie oltre alle letture sporche e alla perdita di dati

Quando si tratta di coerenza dei dati, l’attenzione è rivolta al rischio di condizioni di competizione che possono portare a letture sporche e perdita di dati. Tuttavia, le anomalie dei dati non si fermano qui.

Un esempio di tali anomalie è la distorsione della registrazione (scrivi distorsioni). Le distorsioni sono difficili da rilevare perché di solito non vengono ricercate attivamente. Non sono dovuti a letture sporche o perdita di dati, ma a violazioni dei vincoli logici posti sui dati.

Ad esempio, consideriamo un'applicazione di monitoraggio che richiede che un operatore sia sempre reperibile:

BEGIN tx1;                      BEGIN tx2;
SELECT COUNT(*)
FROM operators
WHERE oncall = true;
0                               SELECT COUNT(*)
                                FROM operators
                                WHERE oncall = TRUE;
                                0
UPDATE operators                UPDATE operators
SET oncall = TRUE               SET oncall = TRUE
WHERE userId = 4;               WHERE userId = 2;
COMMIT tx1;                     COMMIT tx2;

Nella situazione di cui sopra, si verificherà un danneggiamento del record se entrambe le transazioni vengono eseguite con successo. Sebbene non si siano verificate letture sporche o perdite di dati, l'integrità dei dati è stata compromessa: ora due persone sono considerate reperibili contemporaneamente.

L'isolamento serializzabile, la progettazione dello schema o i vincoli del database possono aiutare a eliminare il danneggiamento della scrittura. Gli sviluppatori devono essere in grado di identificare tali anomalie durante lo sviluppo per evitarle in produzione. Allo stesso tempo, le distorsioni della registrazione sono estremamente difficili da ricercare nel codice base. Soprattutto nei sistemi di grandi dimensioni, quando diversi team di sviluppo sono responsabili dell'implementazione di funzioni basate sulle stesse tabelle e non concordano sulle specifiche dell'accesso ai dati.

Il database e l'utente non sempre sono d'accordo su cosa fare

Una delle caratteristiche principali dei database è la garanzia dell'ordine di esecuzione, ma questo ordine stesso potrebbe non essere trasparente per lo sviluppatore del software. I database eseguono le transazioni nell'ordine in cui vengono ricevute, non nell'ordine previsto dai programmatori. L'ordine delle transazioni è difficile da prevedere, soprattutto nei sistemi paralleli altamente caricati.

Durante lo sviluppo, soprattutto quando si lavora con librerie non bloccanti, uno stile scadente e una scarsa leggibilità possono indurre gli utenti a credere che le transazioni vengano eseguite in sequenza, quando in realtà potrebbero arrivare nel database in qualsiasi ordine.

A prima vista, nel programma seguente, T1 e T2 vengono chiamati in sequenza, ma se queste funzioni non sono bloccanti e restituiscono immediatamente il risultato nella forma PROMETTIAMO, quindi l'ordine delle chiamate sarà determinato dai momenti in cui sono entrate nel database:

risultato1 = T1() // i risultati reali sono promesse
risultato2 = T2()

Se è richiesta l'atomicità (ovvero, tutte le operazioni devono essere completate o interrotte) e la sequenza è importante, allora le operazioni T1 e T2 devono essere eseguite all'interno di una singola transazione.

Lo sharding a livello di applicazione può essere spostato all'esterno dell'applicazione

Lo sharding è un metodo per partizionare orizzontalmente un database. Alcuni database possono dividere automaticamente i dati in senso orizzontale, mentre altri non possono, o non sono molto bravi a farlo. Quando gli architetti/sviluppatori di dati sono in grado di prevedere esattamente come si accederà ai dati, possono creare partizioni orizzontali nello spazio utente invece di delegare questo lavoro al database. Questo processo è chiamato "sharding a livello di applicazione" (sharding a livello di applicazione).

Sfortunatamente, questo nome crea spesso l'idea sbagliata che lo sharding risieda nei servizi applicativi. In effetti, può essere implementato come un livello separato davanti al database. A seconda della crescita dei dati e delle iterazioni dello schema, i requisiti di partizionamento orizzontale possono diventare piuttosto complessi. Alcune strategie possono trarre vantaggio dalla capacità di eseguire iterazioni senza dover ridistribuire i server delle applicazioni.

Più sviluppatori dovrebbero saperlo sui database
Un esempio di architettura in cui i server delle applicazioni sono separati dal servizio di sharding

Lo spostamento dello sharding in un servizio separato espande la capacità di utilizzare diverse strategie di sharding senza la necessità di ridistribuire le applicazioni. Velocità è un esempio di tale sistema di sharding a livello di applicazione. Vitess fornisce lo sharding orizzontale per MySQL e consente ai client di connettersi ad esso tramite il protocollo MySQL. Il sistema segmenta i dati in diversi nodi MySQL che non sanno nulla l'uno dell'altro.

L'incremento automatico può essere pericoloso

AUTOINCREMENT è un modo comune per generare chiavi primarie. Ci sono spesso casi in cui i database vengono utilizzati come generatori di ID e il database contiene tabelle progettate per generare identificatori. Esistono diversi motivi per cui generare chiavi primarie utilizzando l'incremento automatico è tutt'altro che ideale:

  • In un database distribuito, l'incremento automatico è un problema serio. Per generare l'ID è necessario un blocco globale. Puoi invece generare un UUID: questo non richiede l'interazione tra diversi nodi del database. L'incremento automatico con i blocchi può portare a conflitti e ridurre significativamente le prestazioni sugli inserti in situazioni distribuite. Alcuni DBMS (ad esempio MySQL) potrebbero richiedere una configurazione speciale e un'attenzione maggiore per organizzare correttamente la replica master-master. Ed è facile commettere errori durante la configurazione, il che porterà a errori di registrazione.
  • Alcuni database dispongono di algoritmi di partizionamento basati su chiavi primarie. Gli ID consecutivi possono portare a punti caldi imprevedibili e ad un aumento del carico su alcune partizioni mentre altre rimangono inattive.
  • Una chiave primaria è il modo più veloce per accedere a una riga in un database. Grazie a metodi migliori per identificare i record, gli ID sequenziali possono trasformare la colonna più importante delle tabelle in una colonna inutile piena di valori senza significato. Pertanto, quando possibile, si prega di scegliere una chiave primaria naturale e univoca a livello globale (ad esempio il nome utente).

Prima di decidere un approccio, considerare l'impatto degli ID e degli UUID con incremento automatico su indicizzazione, partizionamento e partizionamento orizzontale.

I dati obsoleti possono essere utili e non richiedono il blocco

Multiversion Concurrency Control (MVCC) implementa molti dei requisiti di coerenza brevemente discussi in precedenza. Alcuni database (ad esempio Postgres, Spanner) utilizzano MVCC per "alimentare" le transazioni con snapshot, versioni precedenti del database. Le transazioni snapshot possono anche essere serializzate per garantire la coerenza. Durante la lettura da una vecchia istantanea, vengono letti i dati obsoleti.

La lettura di dati leggermente obsoleti può essere utile, ad esempio, quando si generano analisi dai dati o si calcolano valori aggregati approssimativi.

Il primo vantaggio di lavorare con dati legacy è la bassa latenza (soprattutto se il database è distribuito in diverse aree geografiche). Il secondo è che le transazioni di sola lettura sono prive di vincoli. Questo è un vantaggio significativo per le applicazioni che leggono molto, purché siano in grado di gestire dati non aggiornati.

Più sviluppatori dovrebbero saperlo sui database
Il server delle applicazioni legge i dati dalla replica locale che non è aggiornata da 5 secondi, anche se la versione più recente è disponibile dall'altra parte dell'Oceano Pacifico

I DBMS eliminano automaticamente le versioni precedenti e, in alcuni casi, consentono di farlo su richiesta. Ad esempio, Postgres consente agli utenti di farlo VACUUM su richiesta, e periodicamente esegue anche questa operazione in modo automatico. Spanner esegue un garbage collector per eliminare gli snapshot più vecchi di un'ora.

Tutte le fonti temporali sono soggette a distorsione

Il segreto meglio custodito in informatica è che tutte le API di timing mentono. Infatti le nostre macchine non conoscono l'ora esatta. I computer contengono cristalli di quarzo che generano vibrazioni utilizzate per tenere il tempo. Tuttavia, non sono sufficientemente precisi e potrebbero essere in anticipo/in ritardo rispetto all'ora esatta. Lo spostamento può raggiungere i 20 secondi al giorno. Pertanto, l'ora dei nostri computer deve essere periodicamente sincronizzata con quella della rete.

Per la sincronizzazione vengono utilizzati server NTP, ma il processo di sincronizzazione stesso è soggetto a ritardi di rete. Anche la sincronizzazione con un server NTP nello stesso data center richiede del tempo. È chiaro che lavorare con un server NTP pubblico può portare a distorsioni ancora maggiori.

Gli orologi atomici e i loro equivalenti GPS sono migliori per determinare l’ora corrente, ma sono costosi e richiedono una configurazione complessa, quindi non possono essere installati su tutte le auto. Per questo motivo, i data center utilizzano un approccio a più livelli. Gli orologi atomici e/o GPS mostrano l'ora esatta, dopodiché viene trasmessa ad altre macchine tramite server secondari. Ciò significa che ogni macchina subirà un certo spostamento rispetto all'ora esatta.

La situazione è aggravata dal fatto che spesso applicazioni e database si trovano su macchine diverse (se non in data center diversi). Pertanto, il tempo sarà diverso non solo sui nodi DB distribuiti su macchine diverse. Sarà diverso anche sul server delle applicazioni.

Google TrueTime adotta un approccio completamente diverso. La maggior parte delle persone crede che i progressi di Google in questa direzione siano spiegati dal banale passaggio agli orologi atomici e GPS, ma questa è solo una parte del quadro generale. Ecco come funziona TrueTime:

  • TrueTime utilizza due diverse fonti: GPS e orologi atomici. Questi orologi hanno modalità di guasto non correlate. [vedere pagina 5 per i dettagli qui - ca. trad.), quindi il loro utilizzo congiunto aumenta l'affidabilità.
  • TrueTime ha un'API insolita. Restituisce il tempo come un intervallo con errore di misurazione e incertezza incorporati. Il momento reale nel tempo è da qualche parte tra i limiti superiore e inferiore dell'intervallo. Spanner, il database distribuito di Google, aspetta semplicemente finché non è sicuro dire che l'ora corrente è fuori intervallo. Questo metodo introduce una certa latenza nel sistema, soprattutto se l'incertezza sui master è elevata, ma garantisce la correttezza anche in una situazione distribuita a livello globale.

Più sviluppatori dovrebbero saperlo sui database
I componenti Spanner utilizzano TrueTime, dove TT.now() restituisce un intervallo, quindi Spanner semplicemente dorme fino al punto in cui può essere sicuro che l'ora corrente ha superato un certo punto

Una ridotta precisione nel determinare l'ora corrente significa un aumento della durata delle operazioni di Spanner e una diminuzione delle prestazioni. Ecco perché è importante mantenere la massima precisione possibile anche se è impossibile ottenere un orologio completamente preciso.

Il ritardo ha molti significati

Se chiedi a una dozzina di esperti cosa sia un ritardo, probabilmente otterrai risposte diverse. Nei DBMS la latenza viene spesso chiamata "latenza del database" ed è diversa da quella percepita dal client. Il fatto è che il client osserva la somma del ritardo della rete e del ritardo del database. La capacità di isolare il tipo di latenza è fondamentale durante il debug di problemi crescenti. Quando raccogli e visualizzi le metriche, cerca sempre di tenere d'occhio entrambi i tipi.

I requisiti prestazionali dovrebbero essere valutati per una transazione specifica

A volte le caratteristiche prestazionali di un DBMS e le sue limitazioni sono specificate in termini di throughput di scrittura/lettura e di latenza. Ciò fornisce una panoramica generale dei parametri chiave del sistema, ma quando si valutano le prestazioni di un nuovo DBMS, un approccio molto più completo consiste nel valutare separatamente le operazioni critiche (per ciascuna query e/o transazione). Esempi:

  • Scrivi la velocità effettiva e la latenza quando inserisci una nuova riga nella tabella X (con 50 milioni di righe) con i vincoli specificati e il riempimento delle righe nelle tabelle correlate.
  • Ritardo nella visualizzazione degli amici degli amici di un determinato utente quando il numero medio di amici è 500.
  • Latenza nel recupero delle prime 100 voci dalla cronologia di un utente quando l'utente segue altri 500 utenti con X voci all'ora.

La valutazione e la sperimentazione possono includere casi critici fino a quando non si è sicuri che il database soddisfi i requisiti di prestazioni. Una regola pratica simile tiene conto di questa ripartizione anche quando si raccolgono i parametri di latenza e si determinano gli SLO.

Tieni presente l'elevata cardinalità quando raccogli i parametri per ogni operazione. Utilizza log, raccolta eventi o traccia distribuita per ottenere dati di debug ad alta potenza. Nell'articolo "Vuoi eseguire il debug della latenza?» puoi familiarizzare con le metodologie di debugging del ritardo.

Le transazioni nidificate possono essere pericolose

Non tutti i DBMS supportano transazioni nidificate, ma quando lo fanno, tali transazioni possono provocare errori imprevisti che non sono sempre facili da rilevare (ovvero, dovrebbe essere ovvio che esiste qualche tipo di anomalia).

Puoi evitare di utilizzare transazioni nidificate utilizzando librerie client in grado di rilevarle e ignorarle. Se le transazioni nidificate non possono essere abbandonate, prestare particolare attenzione nella loro implementazione per evitare situazioni impreviste in cui le transazioni completate vengono accidentalmente interrotte a causa di quelle nidificate.

L'incapsulamento delle transazioni in diversi livelli può portare a transazioni nidificate inaspettate e, dal punto di vista della leggibilità del codice, può rendere difficile comprendere le intenzioni dell'autore. Dai un'occhiata al seguente programma:

with newTransaction():
   Accounts.create("609-543-222")
   with newTransaction():
       Accounts.create("775-988-322")
       throw Rollback();

Quale sarà l'output del codice sopra? Ripristinerà entrambe le transazioni o solo quella interna? Cosa succede se ci affidiamo a più livelli di librerie che incapsulano la creazione di transazioni per noi? Saremo in grado di identificare e migliorare questi casi?

Immagina un livello dati con più operazioni (ad es. newAccount) è già attuato nelle sue stesse operazioni. Cosa succede se li esegui come parte di una logica aziendale di livello superiore che viene eseguita all'interno della stessa transazione? Quale sarebbe l’isolamento e la coerenza in questo caso?

function newAccount(id string) {
  with newTransaction():
      Accounts.create(id)
}

Invece di cercare risposte a domande così infinite, è meglio evitare transazioni annidate. Dopotutto, il tuo livello dati può facilmente eseguire operazioni di alto livello senza creare transazioni proprie. Inoltre, la logica aziendale stessa è in grado di avviare una transazione, eseguire operazioni su di essa, confermare o interrompere una transazione.

function newAccount(id string) {
   Accounts.create(id)
}
// In main application:
with newTransaction():
   // Read some data from database for configuration.
   // Generate an ID from the ID service.
   Accounts.create(id)
   Uploads.create(id) // create upload queue for the user.

Le transazioni non devono essere legate allo stato dell'applicazione

A volte si è tentati di utilizzare lo stato dell'applicazione nelle transazioni per modificare determinati valori o modificare i parametri della query. La sfumatura fondamentale da considerare è il corretto ambito di applicazione. I clienti spesso riavviano le transazioni quando si verificano problemi di rete. Se la transazione dipende quindi da uno stato che viene modificato da qualche altro processo, potrebbe scegliere il valore sbagliato a seconda della possibilità di una corsa ai dati. Le transazioni devono considerare il rischio di condizioni di competizione dei dati nell'applicazione.

var seq int64
with newTransaction():
    newSeq := atomic.Increment(&seq)
    Entries.query(newSeq)
    // Other operations...

La transazione di cui sopra incrementerà il numero di sequenza ogni volta che viene eseguita, indipendentemente dal risultato finale. Se il commit fallisce a causa di problemi di rete, la richiesta verrà eseguita con un numero di sequenza diverso quando riproverai.

I pianificatori di query possono dirti molto su un database

I pianificatori di query determinano come verrà eseguita una query in un database. Analizzano inoltre le richieste e le ottimizzano prima di inviarle. I pianificatori possono solo fornire alcune possibili stime sulla base dei segnali a loro disposizione. Ad esempio, qual è il metodo di ricerca migliore per la seguente query?

SELECT * FROM articles where author = "rakyll" order by title;

I risultati possono essere recuperati in due modi:

  • Scansione completa della tabella: puoi esaminare ciascuna voce nella tabella e restituire gli articoli con il nome dell'autore corrispondente, quindi ordinarli.
  • Scansione dell'indice: puoi utilizzare un indice per trovare gli ID corrispondenti, ottenere quelle righe e quindi ordinarle.

Il compito del pianificatore di query è determinare quale strategia è la migliore. Vale la pena considerare che i query planner hanno solo capacità predittive limitate. Ciò può portare a decisioni sbagliate. Gli amministratori di database o gli sviluppatori possono utilizzarli per diagnosticare e ottimizzare le query con prestazioni inferiori. Le nuove versioni del DBMS possono configurare i pianificatori di query e l'autodiagnosi può aiutare durante l'aggiornamento del database se la nuova versione porta a problemi di prestazioni. Registri di query lenti, report sui problemi di latenza o statistiche sui tempi di esecuzione possono aiutare a identificare le query che necessitano di ottimizzazione.

Alcuni parametri presentati dal pianificatore di query potrebbero essere soggetti a rumore (specialmente quando si stima la latenza o il tempo della CPU). Una buona aggiunta agli scheduler sono gli strumenti per tracciare e monitorare il percorso di esecuzione. Consentono di diagnosticare tali problemi (ahimè, non tutti i DBMS forniscono tali strumenti).

La migrazione online è difficile ma possibile

La migrazione online, la migrazione in tempo reale o la migrazione in tempo reale significa passare da un database a un altro senza tempi di inattività o danneggiamento dei dati. La migrazione in tempo reale è più semplice da eseguire se la transizione avviene all'interno dello stesso DBMS/motore. La situazione si complica quando è necessario passare ad un nuovo DBMS con requisiti prestazionali e di schema diversi.

Esistono diversi modelli di migrazione online. Eccone uno:

  • Abilita la doppia immissione in entrambi i database. Il nuovo database in questa fase non dispone di tutti i dati, ma accetta solo i dati più recenti. Una volta che sei sicuro di questo, puoi passare al passaggio successivo.
  • Abilita la lettura da entrambi i database.
  • Configurare il sistema in modo che la lettura e la scrittura vengano eseguite principalmente sul nuovo database.
  • Smetti di scrivere sul vecchio database mentre continui a leggere i dati da esso. In questa fase, il nuovo database è ancora privo di alcuni dati. Dovrebbero essere copiati dal vecchio database.
  • Il vecchio database è di sola lettura. Copia i dati mancanti dal vecchio database a quello nuovo. Una volta completata la migrazione, cambia i percorsi al nuovo database, interrompi quello vecchio ed eliminalo dal sistema.

Per ulteriori informazioni consiglio di contattare Articolo, che descrive in dettaglio la strategia di migrazione di Stripe basata su questo modello.

Un aumento significativo del database comporta un aumento dell'imprevedibilità

La crescita del database porta a problemi imprevedibili associati alla sua portata. Quanto più conosciamo la struttura interna di un database, tanto meglio possiamo prevedere la sua scalabilità. Tuttavia, alcuni momenti sono ancora impossibili da prevedere.
Man mano che la base cresce, le ipotesi e le aspettative precedenti relative al volume dei dati e ai requisiti di larghezza di banda della rete potrebbero diventare obsolete. Questo è il momento in cui sorge la questione di importanti revisioni della progettazione, miglioramenti operativi su larga scala, ripensamento delle implementazioni o migrazione ad altri DBMS per evitare potenziali problemi.

Ma non pensare che l'unica cosa necessaria sia un'ottima conoscenza della struttura interna del database esistente. Nuove scale porteranno con sé nuove incognite. Punti critici imprevedibili, distribuzione non uniforme dei dati, problemi hardware e di larghezza di banda imprevisti, traffico in costante aumento e nuovi segmenti di rete ti costringeranno a riconsiderare l'approccio al database, il modello di dati, il modello di distribuzione e le dimensioni del database.

...

Nel momento in cui ho iniziato a pensare di pubblicare questo articolo, c’erano già altri cinque elementi nella mia lista originale. Poi arrivò un numero enorme nuove idee su cos'altro può essere coperto. Pertanto, l'articolo tocca i problemi meno evidenti che richiedono la massima attenzione. Tuttavia, ciò non significa che l'argomento sia esaurito e non lo riprenderò più nei miei materiali futuri e non apporterò modifiche a quello attuale.

PS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento