Perché MySQL dovrebbe eseguire l'I / O sincrono seriale?


8

Quando osservo una query particolarmente fastidiosa sulle tabelle MyISAM che richiede molto tempo per essere eseguita in diverse occasioni, ho notato che MySQL sembra esporre uno schema I / O piuttosto strano: quando si esegue una singola query e si deve fare un significativo quantità di I / O (ad es. per una scansione della tabella o quando le cache sono vuote a causa della echo 3 > /proc/sys/vm/drop_cachesnecessità di caricare prima gli indici dal disco), la dimensione della coda per il dispositivo a blocchi sottostante è vicina al valore 1, con prestazioni abissali di solo 4-5 MB / s:

root@mysql-test:~# iostat -xdm 5 /dev/sda
Linux 3.2.0-40-generic (mysql-test)  04/30/2014      _x86_64_        (4 CPU)

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.14    24.82   18.26   88.79     0.75     4.61   102.56     2.83   26.39   19.29   27.85   2.46  26.31

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00    69.29  151.52   72.73     5.31     0.59    53.95     1.21    5.39    7.84    0.29   4.39  98.51

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00   153.06  144.29  174.69     4.96     1.36    40.54     1.39    4.36    8.91    0.60   3.15 100.49

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00   105.75  150.92  109.03     4.53     0.85    42.41     1.29    4.96    8.15    0.54   3.90 101.36

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00    48.89  156.36   51.72     5.28     0.76    59.38     1.28    6.16    8.02    0.55   4.77  99.23

Mentre i 150 IOPS sono semplicemente ciò che un singolo disco in una determinata configurazione è in grado di fornire in termini di I / O casuale, il risultato mi sorprende ancora davvero poiché mi aspetterei che MySQL sia in grado di eseguire I / O asincroni per le letture e recuperare un una grande quantità di blocchi contemporaneamente invece di leggerli e valutarli uno per uno, trascurando efficacemente i guadagni di parallelizzazione disponibili nelle configurazioni RAID. Quale decisione di progettazione o opzione di configurazione è responsabile di questo? È un problema specifico della piattaforma?

Mentre l'ho provato con tabelle MyISAM di grandi dimensioni, vedo effetti simili con le stesse tabelle convertite in InnoDB (anche se non così male, la query di esempio impiega ancora 20-30 secondi con la maggior parte del tempo impiegato per leggere il disco con una lunghezza della coda di 1) dopo aver riavviato il demone mysql e quindi i pool di buffer sono vuoti. Ho anche verificato che lo stesso problema persiste su 5.6 GA e l'attuale 5.7 milestone 14 - fintanto che sto usando un singolo thread di query, MySQL sembra incapace di parallelizzare le operazioni di I / O necessarie per l'elaborazione delle query.


Come da richiesta alcuni dettagli aggiuntivi sullo scenario. Il comportamento può essere osservato con una moltitudine di tipi di query. Ne ho scelto arbitrariamente uno per ulteriori test che si legge in questo modo:

SELECT herp.id, herp.firstname, herp.lastname, derp.label, herp.email, 
(SELECT CONCAT(label, " (", zip_code, " ", city,")" ) FROM subsidiaries WHERE subsidiaries.id=herp.subsidiary_id ) AS subsidiary, 
(SELECT COUNT(fk_herp) from herp_missing_data WHERE fk_herp=herp.id) AS missing_data
FROM herp LEFT JOIN derp ON derp.id=herp.fk_derp
WHERE (herp.fk_pools='123456')  AND herp.city LIKE '%Some City%' AND herp.active='yes' 
ORDER BY herp.id desc LIMIT 0,10;

So che ha un certo margine di ottimizzazione, ma ho deciso di lasciarlo per una serie di motivi e concentrarmi sulla ricerca di una spiegazione generale per l'inaspettato schema I / O che sto vedendo.

Le tabelle utilizzate contengono un sacco di dati:

mysql> select table_name, engine, table_rows, data_length, index_length from information_schema.tables WHERE tables.TABLE_SCHEMA = 'mydb' and tables.table_name in ( 'herp', 'derp', 'missing_data', 'subsidiaries');
+-------------------------+--------+------------+-------------+--------------+
| table_name              | engine | table_rows | data_length | index_length |
+-------------------------+--------+------------+-------------+--------------+
| derp                    | MyISAM |      14085 |     1118676 |       165888 |
| herp                    | MyISAM |     821747 |   828106512 |    568057856 |
| missing_data            | MyISAM |    1220186 |    15862418 |     29238272 |
| subsidiaries            | MyISAM |       1499 |     6490308 |       103424 |
+-------------------------+--------+------------+-------------+--------------+
4 rows in set (0.00 sec)

Ora, quando sto eseguendo la query sopra su queste tabelle, sto ottenendo tempi di esecuzione di oltre 1 minuto mentre il sistema è apparentemente continuamente impegnato a leggere i dati dal disco con un singolo thread.

Il profilo per un'esecuzione di query di esempio (che ha richiesto 1 minuto 9,17 secondi in questo esempio) è simile al seguente:

mysql> show profile for query 1;
+--------------------------------+-----------+
| Status                         | Duration  |
+--------------------------------+-----------+
| starting                       |  0.000118 |
| Waiting for query cache lock   |  0.000035 |
| init                           |  0.000033 |
| checking query cache for query |  0.000399 |
| checking permissions           |  0.000077 |
| checking permissions           |  0.000030 |
| checking permissions           |  0.000031 |
| checking permissions           |  0.000035 |
| Opening tables                 |  0.000158 |
| init                           |  0.000294 |
| System lock                    |  0.000056 |
| Waiting for query cache lock   |  0.000032 |
| System lock                    |  0.000116 |
| optimizing                     |  0.000063 |
| statistics                     |  0.001964 |
| preparing                      |  0.000104 |
| Sorting result                 |  0.000033 |
| executing                      |  0.000030 |
| Sending data                   |  2.031349 |
| optimizing                     |  0.000054 |
| statistics                     |  0.000039 |
| preparing                      |  0.000024 |
| executing                      |  0.000013 |
| Sending data                   |  0.000044 |
| optimizing                     |  0.000017 |
| statistics                     |  0.000021 |
| preparing                      |  0.000019 |
| executing                      |  0.000013 |
| Sending data                   | 21.477528 |
| executing                      |  0.000070 |
| Sending data                   |  0.000075 |
| executing                      |  0.000027 |
| Sending data                   | 45.692623 |
| end                            |  0.000076 |
| query end                      |  0.000036 |
| closing tables                 |  0.000109 |
| freeing items                  |  0.000067 |
| Waiting for query cache lock   |  0.000038 |
| freeing items                  |  0.000080 |
| Waiting for query cache lock   |  0.000044 |
| freeing items                  |  0.000037 |
| storing result in query cache  |  0.000033 |
| logging slow query             |  0.000103 |
| cleaning up                    |  0.000073 |
+--------------------------------+-----------+
44 rows in set, 1 warning (0.00 sec)

Hai un caso di prova ripetibile (idealmente semplice) che potresti spiegare in modo più dettagliato? Ad esempio una query che genera questo comportamento? In quali circostanze? Hai iniziato su questa strada con "echo 3> ..." e "riavvia il demone mysql" ma non sei entrato nei dettagli.
Scott Leadley,

@ScottLeadley grazie per averci esaminato. Non penso che sarei in grado di renderlo "semplice" - il problema sarebbe osservabile solo se fosse necessario leggere una grande quantità di dati per una singola query e sarebbe per lo più I / O casuale. Tabelle e query sono ragionevolmente semplici e mentre potrei pubblicare i testi DDL e Query, dubito che chiunque sarebbe in grado di riprodurlo immediatamente a meno che i dati di tabella / indice non siano cresciuti a centinaia di Megabyte.
syneticon-dj,

Come hai accennato, i 5 ms attesi per le letture sono coerenti con la latenza di rotazione media di un disco da 5400 RPM. Cercare contese quando si legge "una grande quantità di dati ... I / O per lo più casuali" spiegherebbe questo. Per quanto riguarda RAID, l'hai menzionato, ma non hai fornito alcun dettaglio di questa specifica configurazione.
Scott Leadley,

Non sono sicuro di poterti aiutare direttamente, perché non eseguo la tua configurazione. Ma la regola empirica di StackExchange è che una domanda davvero buona attira più attenzione di una taglia. Scrivi la domanda perfetta
Scott Leadley,

@ScottLeadley i 5 ms attesi sono dovuti principalmente alla latenza del sistema di archiviazione impiegato. Ho provato questo in diversi scenari: da un semplice RAID10 a 4 dischi a un filer di archiviazione a più livelli con uno scaffale a 16 dischi e un supporto SSD, i risultati mostrano costantemente che il carico I / O non è parallelizzato e quindi legato alla latenza. Ciò che ritengo fondamentalmente sbagliato. Ho aggiunto i dettagli della query alla domanda, ma non sono ancora convinto che sarebbero di grande aiuto.
syneticon-dj,

Risposte:


8

Prima di tutto vorrei chiarire confermando che MyISAM non esegue I / O asincroni, ma che InnoDB lo fa e lo farà di default da MySQL 5.5. Prima della 5.5 utilizzava "AIO simulato" utilizzando thread di lavoro.

Penso che sia anche importante distinguere tra tre situazioni:

  1. Più query in esecuzione contemporaneamente
  2. Una singola query in esecuzione in parallelo
  3. Una sorta di lettura logica per scansioni di tabelle / casi chiari in cui le pagine successive sono ben note.

Per (1) l'I / O sarà in grado di eseguire in parallelo per questo. Ci sono alcuni limiti con MyISAM: blocco della tabella e un blocco globale che protegge la key_buffer(cache dell'indice). InnoDB in MySQL 5.5+ brilla davvero qui.

Per (2) questo non è attualmente supportato. Un buon caso d'uso sarebbe con il partizionamento, in cui è possibile cercare in parallelo ogni tabella partizionata.

Per (3) InnoDB ha un read-ahead lineare per leggere in modo completo (gruppo di 64 pagine) se vengono lette> 56 pagine (questo è configurabile), ma c'è spazio per ulteriori miglioramenti. Facebook ha scritto sull'implementazione di lettori logici nel loro ramo (con un guadagno di perf 10 volte sui piani di lettura).


Grazie, questo mi dà un'ulteriore comprensione di ciò che sto vedendo. Ciò significa generalmente che MyISAM non è in grado di utilizzare più di un disco IOPS per un carico a thread singolo? Non riesco a trovare riferimenti a questo nei documenti: ti capita di avere qualcosa di utile?
syneticon-dj,

Sì. Non riesco a pensare a un posto nei documenti in cui questo sarebbe.
Morgan Tocker,

2

Spero missing_datanon sia MyISAM perché una tabella MyISAM vuota di solito ha 1024 byte .MYI. Una dimensione di byte diversa da zero è prevista per un MyISAM. Un byte zero mi .MYIsembra un po 'inquietante.

Se si esegue questa query di metadati

select table_name, table_rows, data_length, index_length, engine
from information_schema.tables
WHERE tables.TABLE_SCHEMA = 'mydb'
and tables.table_name = 'missing_data';

e il motore di quel tavolo è MyISAM, devi ripararlo.

NOTA LATERALE: Se lo engineè NULL, è una vista. Se è una vista o non è MyISAM, ignora il resto del mio post e aggiungi le informazioni alla domanda. Se la tabella è MyISAM, continua a leggere ...

Secondo la tua query sui metadati, missing_data.MYDè di circa 46 milioni.

Innanzitutto, esegui questo

SHOW CREATE TABLE mydb.missing_data\G

Riceverai la descrizione della tabella o un messaggio di errore che dice qualcosa del genere

ERROR 126 (HY000): Incorrect key file for table ...

Se ottieni la descrizione della tabella ed è MyISAM, esegui

OPTIMIZE TABLE mydb.missing_data;

Verrà ricreata la tabella senza frammentazione e calcolerà nuove statistiche sugli indici. Se il problema persiste, prova:

REPAIR TABLE mydb.missing_data;

Ciò dovrebbe rigenerare le pagine dell'indice per MyISAM.

Giusto per sicurezza (se si utilizza MySQL 5.6), eseguirlo dopo la riparazione

FLUSH TABLES mydb.missing_data;

La tua domanda

Gli indici della tabella potrebbero non essere caricati in memoria se MySQL Query Optimizer decide di non utilizzare. Se la tua clausola WHERE impone che una quantità significativa di righe deve essere letta dagli indici, MySQL Query Optimizer lo vedrà durante la costruzione del piano EXPLAIN e deciderà invece di utilizzare una scansione completa della tabella.

Le operazioni di I / O parallele su una tabella MyISAM non sono raggiungibili perché non sono configurabili.

InnoDB può essere ottimizzato per aumentare le prestazioni in questo modo.


Devo enfatizzarlo di nuovo: se mydb.missing_data è MyISAM e ha un indice zero byte, qualcosa è sicuramente sbagliato.
RolandoMySQLDBA

Ho aggiornato i dati per renderli più coerenti: ora mostra solo i risultati di MyISAM da un singolo host, in modo che la gente non si confonda.
syneticon-dj,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.