Pourquoi MySQL ferait-il des E / S synchrones en série?

8

En regardant une requête particulièrement ennuyeuse sur les tables MyISAM qui prend beaucoup de temps à exécuter à plusieurs reprises, j'ai remarqué que MySQL semble exposer un modèle d'E / S plutôt étrange: lors de l'exécution d'une seule requête et de devoir effectuer une importante quantité d'E / S (par exemple pour une analyse de table ou lorsque les caches sont vides en raison de echo 3 > /proc/sys/vm/drop_cachesla nécessité de charger d'abord les index sur le disque), la taille de la file d'attente pour le périphérique sous-jacent est proche de la valeur 1, avec des performances abyssales de seulement 4-5 Mo / 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

Alors que les 150 IOPS sont tout simplement ce qu'un seul disque dans la configuration donnée est capable de fournir en termes d'E / S aléatoires, le résultat me surprend vraiment car je m'attendrais à ce que MySQL soit capable d'exécuter des E / S asynchrones pour les lectures et récupérer un grande quantité de blocs simultanément au lieu de les lire et de les évaluer un par un, en négligeant efficacement les gains de parallélisation disponibles dans les configurations RAID. Quelle décision de conception ou option de configuration en est responsable? Est-ce un problème spécifique à la plate-forme?

Bien que j'aie testé cela avec des tables MyISAM de grande taille, je vois des effets similaires avec les mêmes tables converties en InnoDB (bien que ce ne soit pas aussi mauvais, l'exemple de requête prend toujours 20 à 30 secondes, la plupart du temps étant consacré à la lecture du disque avec une longueur de file d'attente de 1) après avoir redémarré le démon mysql et donc les pools de mémoire tampon sont vides. J'ai également vérifié que le même problème persiste sur 5.6 GA et le jalon actuel 5.7 5.7 - tant que j'utilise un seul thread de requête, MySQL semble incapable de paralléliser les opérations d'E / S nécessaires au traitement des requêtes.


Selon la demande, quelques détails supplémentaires sur le scénario. Le comportement peut être observé avec une multitude de types de requêtes. J'en ai arbitrairement choisi un pour des tests supplémentaires qui se lit un peu comme ceci:

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;

Je sais qu'il a une certaine marge d'optimisation, mais j'ai décidé de m'en tenir à cela pour un certain nombre de raisons et de me concentrer sur la recherche d'une explication générale du modèle d'E / S inattendu que je vois.

Les tables utilisées contiennent un tas de données:

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)

Maintenant, lorsque j'exécute la requête ci-dessus sur ces tables, j'obtiens des temps d'exécution de plus de 1 minute alors que le système est apparemment occupé en permanence à lire des données sur le disque avec un seul thread.

Le profil d'un exemple d'exécution de requête (qui a pris 1 min 9,17 secondes dans cet exemple) ressemble à ceci:

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)
syneticon-dj
la source
Avez-vous un scénario de test reproductible (idéalement simple) que vous pourriez expliquer plus en détail? Par exemple, une requête qui génère ce comportement? Dans quelles circonstances? Vous avez commencé dans cette voie avec "echo 3> ..." & "redémarrer le démon mysql" mais vous n'êtes pas entré dans les détails.
Scott Leadley
@ScottLeadley merci d'avoir étudié cela. Je ne pense pas que je serais en mesure de le rendre "simple" - le problème ne serait observable que si une grande quantité de données devait être lue pour une seule requête et ce serait principalement des E / S aléatoires. Les tables et les requêtes sont relativement simples et même si je pouvais publier les textes DDL et Query, je doute que quiconque puisse les reproduire immédiatement à moins que les données de table / index aient atteint des centaines de mégaoctets.
syneticon-dj
Comme vous y avez fait allusion, les 5 ms d'attente pour les lectures correspondent à la latence de rotation moyenne d'un disque à 5400 tr / min. Rechercher la contention lors de la lecture d'une "grande quantité de données ... principalement des E / S aléatoires" expliquerait cela. Quant au RAID, vous l'avez mentionné, mais vous n'avez donné aucun détail sur cette configuration spécifique.
Scott Leadley
Je ne suis pas sûr de pouvoir vous aider directement, car je n'exécute pas votre configuration. Mais la règle empirique de StackExchange est qu'une très bonne question reçoit plus d'attention qu'une prime. Écrire la question parfaite
Scott Leadley
@ScottLeadley, les 5 ms d'attente sont principalement dues à la latence du système de stockage utilisé. J'ai testé cela dans différents scénarios - d'un simple RAID10 à 4 disques à un serveur de stockage à plusieurs niveaux avec une étagère à 16 disques et un support SSD, les résultats montrent systématiquement que la charge d'E / S n'est pas parallélisée et donc liée à la latence. Ce qui me semble fondamentalement faux. J'ai ajouté les détails de la requête à la question, mais je ne suis pas encore convaincu qu'ils seraient d'une grande aide.
syneticon-dj

Réponses:

8

Permettez-moi d'abord de clarifier en confirmant que MyISAM ne fait pas d'E / S asynchrones, mais qu'InnoDB le fait et le fera par défaut à partir de MySQL 5.5. Avant 5.5, il utilisait "AIO simulé" en utilisant des threads de travail.

Je pense qu'il est également important de distinguer trois situations:

  1. Plusieurs requêtes exécutées simultanément
  2. Une seule requête s'exécutant en parallèle
  3. Une sorte de lecture logique à l'avance pour les analyses de tableau / cas clairs où les pages suivantes sont bien connues.

Pour (1), les E / S pourront s'exécuter en parallèle pour cela. Il y a quelques limites avec MyISAM: le verrouillage de table et un verrou global protégeant le key_buffer(cache d'index). InnoDB dans MySQL 5.5+ brille vraiment ici.

Pour (2), cela n'est actuellement pas pris en charge. Un bon cas d'utilisation serait le partitionnement, où vous pourriez rechercher chaque table partitionnée en parallèle.

Pour (3), InnoDB a une lecture anticipée linéaire pour lire une étendue complète (groupe de 64 pages) si> 56 pages sont lues (ceci est configurable), mais il y a place pour une amélioration supplémentaire. Facebook a écrit sur l' implémentation de la tête de lecture logique dans leur branche (avec un gain de performance 10x sur les tablescans).

Morgan Tocker
la source
Merci, cela me donne une compréhension supplémentaire de ce que je vois. Cela signifie-t-il généralement que MyISAM n'est pas en mesure d'utiliser plusieurs disques IOPS pour une charge à thread unique? Je ne trouve aucune référence à cela dans la documentation - avez-vous quelque chose à portée de main?
syneticon-dj
Oui. Je ne peux pas penser à une place dans les documents où ce serait.
Morgan Tocker
2

J'espère que ce missing_datan'est pas MyISAM car une table MyISAM vide a généralement 1024 octets .MYI. Une taille d'octet différente de zéro est attendue d'un MyISAM. Un octet zéro .MYIme semble un peu effrayant.

Si vous exécutez cette requête de métadonnées

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';

et le moteur de cette table est MyISAM, vous devez le réparer.

SIDE NOTE: Si l' engineest NULL, il est une vue. S'il s'agit d'une vue ou que ce n'est pas MyISAM, veuillez ignorer le reste de mon message et ajouter ces informations à la question. Si le tableau est MyISAM, lisez la suite ...

Selon votre requête de métadonnées, missing_data.MYDc'est environ 46M.

Tout d'abord, exécutez cette

SHOW CREATE TABLE mydb.missing_data\G

Vous obtiendrez soit la description du tableau, soit un message d'erreur indiquant quelque chose comme

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

Si vous obtenez la description du tableau et qu'il s'agit de MyISAM, veuillez exécuter

OPTIMIZE TABLE mydb.missing_data;

Il recréera la table sans fragmentation et calculera de nouvelles statistiques d'index. Si cela ne fonctionne pas, essayez:

REPAIR TABLE mydb.missing_data;

Cela devrait régénérer les pages d'index pour MyISAM.

Juste pour être sûr (si vous utilisez MySQL 5.6), exécutez-le après la réparation

FLUSH TABLES mydb.missing_data;

Ta question

Les index de votre table peuvent ne pas être chargés en mémoire si MySQL Query Optimizer décide de ne pas utiliser. Si votre clause WHERE dicte qu'un nombre important de lignes doivent être lues à partir des index, MySQL Query Optimizer verra cela lors de la construction du plan EXPLAIN et décider d'utiliser à la place une analyse complète de la table.

Les opérations d'E / S parallèles sur une table MyISAM sont inaccessibles car non configurables.

InnoDB peut être réglé pour augmenter les performances comme ça.

RolandoMySQLDBA
la source
Je dois le souligner à nouveau: si mydb.missing_data est MyISAM et a un index de zéro octet, quelque chose ne va vraiment pas.
RolandoMySQLDBA
J'ai mis à jour les données pour être plus cohérentes - elles montrent maintenant les résultats de MyISAM uniquement à partir d'un seul hôte afin que les gens ne se trompent pas.
syneticon-dj