Gli utenti tendono a notare più facilmente un calo delle prestazioni a bassa concorrenza, mentre i miglioramenti delle prestazioni ad alta concorrenza sono spesso più difficili da percepire. Pertanto, mantenere le prestazioni a bassa concorrenza è cruciale, poiché influisce direttamente sull’esperienza dell’utente e sulla volontà di aggiornare [1].
Secondo ampi feedback degli utenti, dopo l’aggiornamento a MySQL 8.0, gli utenti hanno generalmente percepito un calo delle prestazioni, in particolare nelle operazioni di inserimento in batch e nelle operazioni di join. Questa tendenza al ribasso è diventata più evidente nelle versioni superiori di MySQL. Inoltre, alcuni appassionati e tester di MySQL hanno segnalato un degrado delle prestazioni in diversi test sysbench dopo l’aggiornamento.
Possono essere evitati questi problemi di prestazioni? O, più specificamente, come dovremmo valutare scientificamente la tendenza attuale al calo delle prestazioni? Queste sono domande importanti da considerare.
Anche se il team ufficiale continua a ottimizzare, il graduale deterioramento delle prestazioni non può essere trascurato. In alcuni scenari, potrebbero sembrare esserci miglioramenti, ma questo non significa che le prestazioni in tutti gli scenari siano ottimizzate in egual misura. Inoltre, è anche facile ottimizzare le prestazioni per scenari specifici a scapito del degrado delle prestazioni in altre aree.
Le Cause Fondamentali del Calo delle Prestazioni di MySQL
In generale, man mano che vengono aggiunte più funzionalità, il codice cresce, e con l’espansione continua della funzionalità, le prestazioni diventano sempre più difficili da controllare.
Gli sviluppatori MySQL spesso non notano il declino delle prestazioni, poiché ogni aggiunta al codice porta solo a una piccolissima diminuzione delle prestazioni. Tuttavia, nel tempo, questi piccoli cali si accumulano, portando a un significativo effetto cumulativo, che porta gli utenti a percepire un evidente degrado delle prestazioni nelle nuove versioni di MySQL.
Ad esempio, la seguente figura mostra le prestazioni di un’operazione di join singolo semplice, con MySQL 8.0.40 che mostra un calo delle prestazioni rispetto a MySQL 8.0.27:
La figura seguente mostra il test delle prestazioni di inserimento batch con singola concorrenza, con il calo delle prestazioni di MySQL 8.0.40 rispetto alla versione 5.7.44:
Dai due grafici sopra, si può vedere che le prestazioni della versione 8.0.40 non sono buone.
Successivamente, analizziamo la causa radice del degrado delle prestazioni in MySQL dal livello del codice. Di seguito è riportata la funzione PT_insert_values_list::contextualize
in MySQL 8.0:
La corrispondente funzione PT_insert_values_list::contextualize
in MySQL 5.7 è la seguente:
Dalla comparazione del codice, MySQL 8.0 sembra avere un codice più elegante, sembrando progredire.
Purtroppo, molte volte, sono proprio le motivazioni dietro questi miglioramenti del codice a portare al degrado delle prestazioni. Il team ufficiale di MySQL ha sostituito la precedente struttura dati List
con una deque
, che è diventata una delle cause principali del graduale degrado delle prestazioni. Diamo un’occhiata alla documentazione del deque
:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
Come mostrato nella descrizione sopra, nei casi estremi, mantenere un singolo elemento richiede l’allocazione dell’intero array, con conseguente bassa efficienza di memoria. Ad esempio, durante l’inserimento in blocco, dove è necessario inserire un gran numero di record, l’implementazione ufficiale memorizza ciascun record in una coda separata. Anche se il contenuto del record è minimo, deve comunque essere allocata una coda. L’implementazione della coda di MySQL alloca 1KB di memoria per ogni coda per supportare ricerche veloci.
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
L’implementazione ufficiale utilizza 1KB di memoria per memorizzare le informazioni sull’indice e, anche se la lunghezza del record non è grande ma ci sono molti record, gli indirizzi di accesso alla memoria possono diventare non contigui, portando a una scarsa amicizia con la cache. Questo design era pensato per migliorare l’amicizia con la cache, ma non è stato pienamente efficace.
È importante notare che l’implementazione originale utilizzava una struttura dati List, dove la memoria veniva allocata attraverso un pool di memoria, garantendo un certo livello di amicizia con la cache. Anche se l’accesso casuale è meno efficiente, l’ottimizzazione per l’accesso sequenziale agli elementi della List migliora significativamente le prestazioni.
Durante l’aggiornamento a MySQL 8.0, gli utenti hanno osservato un significativo calo delle prestazioni dell’inserimento in blocco, e una delle principali cause è stata la sostanziale modifica delle strutture dati sottostanti.
Inoltre, mentre il team ufficiale ha migliorato il meccanismo del log di ripristino, ciò ha portato anche a una diminuzione dell’efficienza dell’operazione di commit MTR. Rispetto a MySQL 5.7, il codice aggiunto riduce significativamente le prestazioni dei commit individuali, anche se complessivamente la capacità di scrittura è stata notevolmente migliorata.
Esaminiamo l’operazione core execute
del commit MTR in MySQL 5.7.44:
Esaminiamo l’operazione principale execute
del commit MTR in MySQL 8.0.40:
In confronto, è chiaro che in MySQL 8.0.40, l’operazione di esecuzione nel commit MTR è diventata molto più complessa, con più passaggi coinvolti. Questa complessità è una delle principali cause del declino nelle prestazioni di scrittura a bassa concorrenza.
In particolare, le operazioni m_impl->m_log.for_each_block(write_log)
e log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn)
comportano un sovraccarico significativo. Queste modifiche sono state apportate per migliorare le prestazioni in alta concorrenza, ma sono avvenute a scapito delle prestazioni in bassa concorrenza.
La priorizzazione del log di redo in modalità alta concorrenza porta a scarse prestazioni per i carichi di lavoro a bassa concorrenza. Sebbene l’introduzione di innodb_log_writer_threads
fosse intesa a mitigare i problemi di prestazioni a bassa concorrenza, non influisce sull’esecuzione delle funzioni sopra menzionate. Poiché queste operazioni sono diventate più complesse e richiedono frequenti commit MTR, le prestazioni sono comunque scese significativamente.
Esaminiamo l’impatto della funzionalità di aggiunta/rimozione istantanea sulle prestazioni. Di seguito è riportata la funzione rec_init_offsets_comp_ordinary
in MySQL 5.7:
La funzione rec_init_offsets_comp_ordinary
in MySQL 8.0.40 è la seguente:
Dal codice sopra, è chiaro che con l’introduzione della funzionalità di aggiunta/rimozione istantanea di colonne, la funzione rec_init_offsets_comp_ordinary
è diventata notevolmente più complessa, introducendo più chiamate di funzione e aggiungendo un’istruzione switch che influisce pesantemente sull’ottimizzazione della cache. Poiché questa funzione viene chiamata frequentemente, influisce direttamente sulle prestazioni dell’aggiornamento degli indici, degli inserimenti batch e dei join, causando un notevole calo delle prestazioni.
Inoltre, il declino delle prestazioni in MySQL 8.0 non è limitato a quanto sopra; ci sono molte altre aree che contribuiscono al degrado complessivo delle prestazioni, specialmente l’impatto sull’espansione delle funzioni inline. Ad esempio, il seguente codice influisce sull’espansione delle funzioni inline:
Secondo i nostri test, l’istruzione ib::fatal
interferisce pesantemente con l’ottimizzazione inline. Per funzioni ad accesso frequente, è consigliabile evitare istruzioni che interferiscono con l’ottimizzazione inline.
Successivamente, esaminiamo un problema simile. La funzione row_sel_store_mysql_field
viene chiamata frequentemente, con row_sel_field_store_in_mysql_format
che è una funzione hotspot al suo interno. Il codice specifico è il seguente:
La funzione row_sel_field_store_in_mysql_format
chiama in definitiva row_sel_field_store_in_mysql_format_func
.
La funzione row_sel_field_store_in_mysql_format_func
non può essere inlined a causa della presenza del codice ib::fatal
.
Funzioni inefficienti chiamate frequentemente, eseguite decine di milioni di volte al secondo, possono influire pesantemente sulle prestazioni dei join.
Continuiamo a esplorare le ragioni del calo delle prestazioni. L’ottimizzazione delle prestazioni ufficiale seguente è, di fatto, una delle cause principali del calo delle prestazioni nelle operazioni di join. Anche se alcune query possono essere migliorate, rimangono comunque alcune delle ragioni per il degrado delle prestazioni delle normali operazioni di join.
I problemi di MySQL vanno oltre questi. Come mostrato nelle analisi sopra, il calo delle prestazioni in MySQL non è senza causa. Una serie di piccoli problemi, accumulandosi, può portare a un degrado delle prestazioni percepibile dagli utenti. Tuttavia, questi problemi sono spesso difficili da identificare, rendendoli ancora più difficili da risolvere.
Il cosiddetto “ottimizzazione prematura” è la radice di tutti i mali, e non si applica nello sviluppo di MySQL. Lo sviluppo di database è un processo complesso, e trascurare le prestazioni nel tempo rende i successivi miglioramenti delle prestazioni significativamente più difficili.
Soluzioni per Mitigare il Calo delle Prestazioni di MySQL
Le principali ragioni del calo delle prestazioni di scrittura sono legate ai problemi di commit MTR, all’aggiunta/rimozione istantanea di colonne e ad altri fattori. Questi sono difficili da ottimizzare nei modi tradizionali. Tuttavia, gli utenti possono compensare il calo delle prestazioni attraverso l’ottimizzazione PGO. Con una strategia adeguata, le prestazioni possono generalmente rimanere stabili.
Per la degradazione delle prestazioni dell’inserimento batch, la nostra versione open-source [2] sostituisce la deque ufficiale con un’implementazione migliorata della lista. Questo affronta principalmente problemi di efficienza della memoria e può alleviare parzialmente il declino delle prestazioni. Combinando l’ottimizzazione PGO con la nostra versione open-source, le prestazioni dell’inserimento batch possono avvicinarsi a quelle di MySQL 5.7.
Gli utenti possono anche sfruttare più thread per l’elaborazione batch concorrente, utilizzando appieno la maggiore concorrenza del registro di ripristino, il che può aumentare significativamente le prestazioni dell’inserimento batch.
Per quanto riguarda i problemi di aggiornamento dell’indice, a causa dell’inevitabile aggiunta di nuovo codice, l’ottimizzazione PGO può aiutare a mitigare questo problema. La nostra versione PGO [2] può alleviare significativamente questo problema.
Per le prestazioni di lettura, in particolare le prestazioni di join, abbiamo apportato miglioramenti sostanziali, tra cui la correzione di problemi inline e altre ottimizzazioni. Con l’aggiunta di PGO, le prestazioni di join possono aumentare di oltre il 30% rispetto alla versione ufficiale.
Continueremo a investire tempo nell’ottimizzazione delle prestazioni a bassa concorrenza. Questo processo è lungo ma coinvolge numerose aree che necessitano di miglioramento.
La versione open-source è disponibile per i test, e gli sforzi continueranno a migliorare le prestazioni di MySQL.
Riferimenti
[1] Bin Wang (2024). L’arte della risoluzione dei problemi nell’ingegneria del software: come rendere MySQL migliore.
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis