sh4.tech

Nota 4 min de leitura MySQL · tuning

127x sem trocar uma linha de código

Um MySQL com uns 2 GB de dados quentes rodava com o buffer pool padrão de 128 MB. Seis parâmetros, aplicados em três ondas de risco, levaram uma consulta de sete minutos para 3,3 segundos. Parte do ganho veio de uma limpeza, e eu digo quanto.

Quando aconteceu
Publicado
Medido
, produção, MySQL 8.0 com InnoDB
Stack
MySQL 8.0 (InnoDB), jobs em Python em VMs Linux
Também em
English

O que eu encontrei

Em agosto um pipeline de ocupação quebrou e eu passei alguns dias dentro dele. Quase tudo isso é outra história. Um dos achados laterais foi o próprio banco: um MySQL com uns 2 GB de dados lidos o dia inteiro e o innodb_buffer_pool_size em 128 MB, o valor que vem na instalação. Isso é muito comum. O servidor funciona, ninguém reclama, e o padrão fica lá por anos.

Com um pool desse tamanho, a maior parte das leituras ia para o disco: uns 12 milhões de leituras de disco por dia. Uma consulta que o time usava todo dia levava uns sete minutos e estourava o timeout.

Confira o seu

Duas consultas dizem se você está no mesmo lugar. A primeira mostra o tamanho dos dados por schema; a segunda, com que frequência o InnoDB precisou ir ao disco:

check.sql
SELECT table_schema,
       ROUND(SUM(data_length + index_length) / 1024 / 1024) AS mb
FROM information_schema.tables
GROUP BY table_schema
ORDER BY mb DESC;

SELECT @@innodb_buffer_pool_size / 1024 / 1024 AS pool_mb,
       1 - r.v / q.v AS hit_ratio
FROM (SELECT VARIABLE_VALUE AS v FROM performance_schema.global_status
      WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') r,
     (SELECT VARIABLE_VALUE AS v FROM performance_schema.global_status
      WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') q;

Se o pool for muito menor que o volume lido no dia a dia, comece por ele. Os contadores acumulam desde o último reinício, então, se quiser a taxa atual, olhe duas vezes com alguns minutos de intervalo.

Três ondas

Mudei seis parâmetros, e para mim a ordem importa mais que os valores, porque cada grupo tem um dono diferente para o risco.

tuning.sql
-- onda 1: com o banco no ar, nada a perder
SET PERSIST innodb_buffer_pool_size  = 4294967296;  -- 128 MB -> 4 GB, dois dias depois 8 GB
SET PERSIST innodb_redo_log_capacity = 1073741824;  -- 100 MB -> 1 GB (8.0.30+)
SET PERSIST innodb_io_capacity       = 1000;        -- padrão 200
SET PERSIST innodb_io_capacity_max   = 4000;        -- padrão 2000

-- onda 2: troca durabilidade por velocidade, precisa do ok do negócio
SET PERSIST innodb_flush_log_at_trx_commit = 2;     -- padrão 1
SET PERSIST sync_binlog                    = 100;   -- padrão 1

-- onda 3: só vale depois do reinício
SET PERSIST_ONLY innodb_flush_method = 'O_DIRECT';

A onda 1 não tem contrapartida num servidor com RAM sobrando. O buffer pool muda de tamanho com o banco no ar desde o 5.7; leva um tempo em segundo plano, e dá para acompanhar em Innodb_buffer_pool_resize_status.

A onda 2 é outra conversa. Com innodb_flush_log_at_trx_commit = 2, o commit entrega o log ao sistema operacional e a gravação em disco acontece mais ou menos uma vez por segundo. Se só o mysqld cair, não se perde nada; se a máquina perder energia, dá para perder até cerca de um segundo de commits. sync_binlog = 100 é a mesma ideia para o binlog, até 100 transações. Aqui as tabelas são recalculadas pelo pipeline a cada ciclo, então um segundo perdido custa um ciclo. Mesmo assim, esse tipo de decisão não cabe a quem opera o banco sozinho: perguntei a quem responde pelo negócio, e a resposta foi sim. O servidor fazia umas 267 transações pequenas por segundo em autocommit, e os fsyncs caíram de ~500/s para ~4/s.

Por último vem o O_DIRECT, para o InnoDB parar de guardar as mesmas páginas duas vezes (no pool e no cache do sistema operacional). Ele exige reinício, então deixei armado com PERSIST_ONLY e esperei uma janela.

O reinício

Domingo, depois da meia-noite, com o cron do pipeline pausado. O MySQL voltou em 11 segundos, e voltou somente leitura. Esse eu esperava, estava no checklist: algumas configurações persistidas de um arranjo antigo de replicação (super_read_only e group replication no boot) continuavam lá. Antes de qualquer reinício, vale olhar o que está persistido:

sql
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.persisted_variables;

Eu tinha os dois comandos prontos, SET GLOBAL super_read_only = OFF e SET GLOBAL read_only = OFF, e a escrita ficou parada por uns 50 segundos. Não removi essas configurações persistidas porque elas são do time de infraestrutura; deixei registrado que qualquer reinício futuro repete o mesmo efeito, e como sair dele.

O número honesto

Sete minutos para 3,3 segundos dá 127x. Só que, nos mesmos dias, removi cerca de 1,66 milhão de linhas de lixo que o pipeline quebrado tinha deixado, e aquela tabela ficou 40% menor. Não consigo separar com exatidão quanto veio do tuning e quanto veio da limpeza. Os outros números são só do tuning: hit ratio de 99,99%, o disco passou de milhões de leituras por dia para quase só escrita, e o servidor aguentou uns 3.900 inserts por segundo onde antes o teto ficava entre 500 e 1.000.

Esses parâmetros não saíram da documentação. Saíram dos anos em que eu cuidava de uns 60 servidores próprios. Eu ditei para o agente, que executou e conferiu um por um. Naquela época eu costumava dar ao pool algo como 75% da RAM; aqui parei em 8 GB, porque para esses dados já bastava.

Arquivado pelo problema que resolve

Luciano Serra
Luciano Serra Founder-engineer · cofundador e CTO, construiu do zero uma rede de 15.000 telas, 2015–2024 · São Paulo, remoto

Desenho sistemas de produção do zero e assumo os que já estão rodando. Nos dois casos eles precisam aguentar quando o negócio cresce. Cofundei uma empresa de mídia out-of-home e construí a tecnologia dela por dez anos; meus sócios captaram o dinheiro e cuidaram do comercial.

Seu negócio está crescendo mais rápido que os sistemas? Vamos conversar →