Por que o MySQL faria E / S síncrona em série?

8

Ao olhar para uma consulta particularmente irritante sobre tabelas MyISAM que leva muito tempo para ser executada em várias ocasiões, observei que o MySQL parece expor um padrão de E / S bastante estranho: ao executar uma única consulta e ter que fazer uma significativa quantidade de E / S (por exemplo, para uma varredura de tabela ou quando os caches estão vazios como resultado, echo 3 > /proc/sys/vm/drop_cachespara que os índices precisem ser carregados primeiro do disco), o tamanho da fila do dispositivo de bloco subjacente fica próximo ao valor 1, com desempenho abismal de apenas 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

Enquanto os 150 IOPS simplesmente são o que um único disco na configuração dada é capaz de fornecer em termos de E / S aleatória, o resultado ainda realmente me surpreende como seria de esperar MySQL ser capaz de executar E / S assíncrona para leituras e buscar um grande quantidade de blocos simultaneamente, em vez de lê-los e avaliá-los um por um, negligenciando efetivamente os ganhos de paralelização disponíveis nas configurações de RAID. Que decisão de design ou opção de configuração é responsável por isso? Esse é um problema específico da plataforma?

Embora tenha testado isso com tabelas MyISAM grandes, vejo efeitos semelhantes com as mesmas tabelas convertidas no InnoDB (embora não seja tão ruim, a consulta de amostra ainda leva 20 a 30 segundos, passando a maior parte do tempo na leitura do disco com o InnoDB). uma fila de 1) depois que eu reinicio o daemon mysql e, portanto, os buffer pools estão vazios. Também verifiquei que o mesmo problema persiste no 5.6 GA e no marco 5.7 atual 14 - desde que eu esteja usando um único encadeamento de consulta, o MySQL parece incapaz de paralelizar as operações de E / S necessárias para o processamento de consultas.


Conforme a solicitação, alguns detalhes adicionais sobre o cenário. O comportamento pode ser observado com vários tipos de consulta. Eu escolhi arbitrariamente um para testes adicionais, que é mais ou menos assim:

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;

Eu sei que ele tem algum espaço para otimização, mas decidi deixar por vários motivos e me concentrar em encontrar uma explicação geral para o padrão inesperado de E / S que estou vendo.

As tabelas usadas têm um monte de dados:

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)

Agora, quando estou executando a consulta acima nessas tabelas, estou obtendo tempos de execução superiores a 1 minuto, enquanto o sistema parece estar continuamente ocupado lendo dados do disco com um único encadeamento.

O perfil para uma execução de consulta de amostra (que levou 1 min e 9,17 segundos neste exemplo) tem a seguinte aparência:

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
fonte
Você tem um caso de teste repetível (idealmente simples) que poderia explicar em mais detalhes? Por exemplo, uma consulta que gera esse comportamento? Sob que circunstâncias? Você começou nesse caminho com "echo 3> ..." e "restart the mysql daemon", mas não entrou em detalhes.
Scott Leadley
@ ScottLeadley obrigado por investigar isso. Eu não acho que seria capaz de torná-lo "simples" - o problema só seria observável se uma quantidade enorme de dados precisasse ser lida para uma única consulta e fosse E / S aleatória. As tabelas e consultas são razoavelmente diretas e, embora eu possa postar os textos DDL e Consulta, duvido que alguém possa reproduzi-lo imediatamente, a menos que os dados da tabela / índice tenham aumentado para centenas de megabytes.
Syneticon-dj 5/05
Como você mencionou, os 5 ms à espera de leitura são consistentes com a latência rotacional média de um disco de 5400 RPM. Procure a contenção ao ler "uma grande quantidade de dados ... principalmente E / S aleatória". Quanto ao RAID, você mencionou, mas não forneceu detalhes dessa configuração específica.
Scott Leadley
Não tenho certeza se posso ajudá-lo diretamente, porque não corro sua configuração. Mas a regra prática do StackExchange é que uma pergunta realmente boa recebe mais atenção do que uma recompensa. Escrevendo a pergunta perfeita
Scott Leadley
@ScottLeadley os 5 ms aguardam principalmente devido à latência do sistema de armazenamento empregado. Eu testei isso em diferentes cenários - de um RAID10 simples de 4 discos a um arquivador de armazenamento em camadas com uma prateleira de 16 discos e suporte SSD, os resultados mostram consistentemente que a carga de E / S não é paralela e, portanto, vinculada à latência. O que eu sinto é fundamentalmente errado. Eu adicionei os detalhes da consulta à pergunta, mas ainda não estou convencido de que eles seriam de grande ajuda.
Syneticon-dj

Respostas:

8

Primeiro, deixe-me esclarecer confirmando que o MyISAM não faz E / S assíncrona, mas que o InnoDB faz e continuará por padrão no MySQL 5.5. Antes do 5.5, ele usava "AIO simulado" usando threads de trabalho.

Eu acho que também é importante distinguir entre três situações:

  1. Várias consultas executando ao mesmo tempo
  2. Uma única consulta executando em paralelo
  3. Algum tipo de leitura lógica a seguir para varreduras de tabelas / casos claros em que as próximas páginas são bem conhecidas.

Pois (1) a E / S poderá executar em paralelo para isso. Existem alguns limites no MyISAM: bloqueio de tabela e bloqueio global que protegem o key_buffer(cache de índice). O InnoDB no MySQL 5.5+ realmente brilha aqui.

Para (2) isso atualmente não é suportado. Um bom caso de uso seria com o particionamento, onde você poderia procurar cada tabela particionada em paralelo.

Para (3) o InnoDB possui uma leitura linear antecipada para ler em toda a extensão (grupo de 64 páginas) se mais de 56 páginas forem lidas (isso é configurável), mas há espaço para aprimoramentos adicionais. O Facebook escreveu sobre a implementação do cabeçote de leitura lógica em sua ramificação (com um ganho de 10x em varreduras de tabelas).

Morgan Tocker
fonte
Obrigado, isso me dá uma compreensão adicional do que estou vendo. Isso geralmente significa que o MyISAM não pode utilizar mais de um disco de IOPS para uma carga de thread único? Não consigo encontrar referências a isso nos documentos. Você tem algo útil?
Syneticon-dj
Sim. Não consigo pensar em um lugar nos documentos onde isso seria.
Morgan Tocker
2

Espero que missing_datanão seja o MyISAM, porque uma tabela vazia do MyISAM geralmente possui 1024 bytes .MYI. Um tamanho de byte diferente de zero é esperado para um MyISAM. Um byte zero .MYIsoa um pouco assustador para mim.

Se você executar esta consulta de metadados

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 o mecanismo dessa tabela é MyISAM, você precisa repará-lo.

NOTA LATERAL: Se enginefor NULL, é uma visualização. Se for uma visualização ou não for MyISAM, ignore o restante da minha postagem e adicione essas informações à pergunta. Se a tabela for MyISAM, continue a ler ...

De acordo com sua consulta de metadados, missing_data.MYDé de cerca de 46 milhões.

Primeiro, execute isso

SHOW CREATE TABLE mydb.missing_data\G

Você receberá a descrição da tabela ou uma mensagem de erro dizendo algo como

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

Se você obtiver a descrição da tabela e for MyISAM, execute

OPTIMIZE TABLE mydb.missing_data;

Ele recriará a tabela sem fragmentação e calculará novas estatísticas de índice. Se isso não funcionar, tente:

REPAIR TABLE mydb.missing_data;

Isso deve gerar novamente as páginas de índice para o MyISAM.

Para garantir a segurança (se você estiver usando o MySQL 5.6), execute isso após o reparo

FLUSH TABLES mydb.missing_data;

Sua pergunta

Os índices da sua tabela podem não ser carregados na memória se o MySQL Query Optimizer decidir não usar. Se sua cláusula WHERE ditar que uma quantidade significativa de linhas deve ser lida nos índices, o MySQL Query Optimizer verá isso ao construir o plano EXPLAIN e decidirá usar uma varredura de tabela completa.

As operações de E / S paralelas em uma tabela MyISAM são inatingíveis porque são inconfiguráveis.

O InnoDB pode ser ajustado para aumentar o desempenho assim.

RolandoMySQLDBA
fonte
Devo enfatizá-lo novamente: se mydb.missing_data for MyISAM e tiver um índice de zero byte, algo está definitivamente errado.
RolandoMySQLDBA
Atualizei os dados para serem mais coerentes - agora mostra resultados apenas do MyISAM de um único host para que as pessoas não fiquem confusas.
Syneticon-dj