Dev & EngARTICLE

Linux kernel folio optimization can trigger I/O bursts in Galera/PXC clusters

A case study published by Percona shows how the Linux kernel's adoption of large folios, combined with the Galera cache's use of mmap(), can multiply disk writes even though no MySQL configuration has changed.

Linux kernel folio optimization can trigger I/O bursts in Galera/PXC clusters
Image: Roberto Diniz

Um case publicado pela Percona mostra como a adoção de large folios pelo kernel Linux, combinada com o uso de mmap() pelo cache do Galera, pode multiplicar a escrita em disco sem que nenhuma configuração do MySQL tenha mudado.

A Percona publicou nesta sexta-feira (16 de outubro de 2026) um relato de caso assinado por Przemysław Malkowski que vale a atenção de quem opera Percona XtraDB Cluster (PXC) ou qualquer outro cluster Galera. O gatilho foi um upgrade aparentemente trivial: a versão do PXC subiu de 5.7 para 8.0 num cliente da consultoria. A carga de trabalho não mudou, as configurações do MySQL não mudaram, mas todos os nós do cluster passaram a exibir rajadas esparsas, porém regulares, de escrita massiva em disco.

O detalhe que tornou o caso intrigante é que as estatísticas de workload do próprio MySQL, reunidas durante essas rajadas, não explicavam o volume delas de forma alguma.

Onde o tempo de disco estava sendo gasto

A investigação usou pidstat e iostat para confirmar que o próprio processo mysqld era a origem das escritas excessivas. Cavando mais fundo, as threads responsáveis apareceram: a Galera receiver thread (que recebe o replication stream), as connection threads no nó escritor e as applier threads nos nós secundários. Havia uma disparidade enorme entre o que o InnoDB escrevia (redo, undo, tablespace) e o que era contabilizado nas threads do Galera.

Um breakdown por thread, para uma rajada de poucos segundos, mostrou a distribuição a seguir:

Grupo de threadsEscrita contabilizadaParticipação
applier (threads de aplicação de write-sets)13.0 GB61%
galera_recv-0 (receiver thread)5.2 GB24%
ib_pg_flush* (page cleaners do InnoDB)2.0 GB9%
ib_log_writer (redo log)0.76 GB4%
Galera_srvc (serviço do Galera)0.29 GB1%

A hipótese inicial foi que transações alterando uma única coluna estivessem inflando os eventos de binlog por causa de colunas blob/text/json grandes, já que binlog_row_image=full aplica o formato ROW tanto ao write-set do Galera quanto ao binlog. Mas a análise do binlog com a ferramenta binlogsum mostrou volume moderado e completamente plano, sem relação com as rajadas.

O vilão: mmap() e as folios grandes do kernel

O ponto de virada veio quando se percebeu que o upgrade do PXC havia vindo acompanhado de um upgrade do sistema operacional: de Debian 11, com kernel Linux 5.10 LTS, para Debian 12, com kernel 6.1 LTS. Não havia mudança no código do plugin Galera nem nas extensões wsrep que explicasse o comportamento, então a suspeita migrou para o gcache, o arquivo de cache de write-sets do Galera, e para a forma como o kernel passou a tratá-lo.

A explicação está em um recurso chamado large folios, introduzido no kernel 5.16 para lidar com grandes volumes de dados em máquinas com bastante RAM e disco. O suporte a folios grandes no page cache chegou primeiro ao sistema de arquivos XFS, no kernel 5.18, e o cluster afetado mantinha o gcache justamente numa partição XFS.

Em resumo: diferente de qualquer outro arquivo de dados ou log do MySQL (exceto a storage engine TempTable), o cache do Galera não usa o caminho write()/pwrite(). Ele usa mmap(). Esse detalhe de implementação, que existe desde a origem do Galera, é o que faz o cache reagir de forma muito diferente à evolução do kernel.

Em kernels mais recentes (5.18 a 6.5), escritas via write()/pwrite() também sofriam algum efeito das folios grandes, mas o InnoDB normalmente usa O_DIRECT para os tablespaces (não passa pelo page cache) e o redo log e o binlog fazem appends pequenos, que geram folios de ordem zero (4 KB). No kernel 6.6, um patch introduziu rastreamento de dirty por bloco: o iomap passou a registrar os blocos de filesystem sobrepostos, em vez de marcar a folio inteira como suja. Isso beneficiou diretamente quem escreve via write()/pwrite().

O caminho mmap(), porém, não tem como se beneficiar dessa melhoria. Qualquer atualização via memory mapping marca a folio inteira como dirty, então o writeback do page cache sincroniza a folio completa (até 2 MB) em vez do bloco exato de 4 KB que de fato mudou. Uma escrita pequena e aleatória num arquivo mapeado por mmap() pode, assim, disparar uma rajada de I/O desproporcional.

I am not a Linux kernel expert, so I hope there are not too many errors.

Przemysław Malkowski, Percona

Por que o cache do Galera sofre mais que o InnoDB

Três fatores combinados tornam o gcache um alvo particularmente exposto a esse fenômeno. Primeiro, o tamanho: o cache padrão é de 128 MB, mas na prática costuma ser configurado bem maior para garantir cobertura suficiente de IST (Incremental State Transfer). Segundo, o padrão de acesso: além dos appends sequenciais de novos write-sets, há muitas atualizações pequenas e aleatórias que marcam entradas como RELEASED quando a transação já foi aplicada e o espaço pode ser reaproveitado.

O terceiro fator é o readahead do dispositivo de armazenamento. Quando os dados do MySQL, junto com o galera.cache, ficam num array RAID, o kernel Linux costuma aumentar automaticamente o read_ahead_kb proporcionalmente à geometria do array e ao optimal_io_size anunciado pelo dispositivo. Isso vale tanto para RAID por software quanto, em muitos casos, para stacks com dm/LVM. Readahead maior gera folios maiores, e foi exatamente isso que aconteceu no caso do cliente da Percona.

Há ainda um gatilho adicional: durante o startup de um nó PXC, o arquivo gcache inteiro é varrido sequencialmente no processo de recovery (habilitado por padrão), o que pode popular de imediato grandes áreas de page cache em folios já maximizadas.

A periodicidade das rajadas também tem explicação mecânica. O kernel verifica a cada dirty_writeback_centisecs (5 segundos por padrão) se os dados sujos de um inode já ultrapassaram o dirty_expire_centisecs (30 segundos por padrão). As rajadas observadas no cliente, de aproximadamente 33 segundos, são consistentes com essa combinação de timers, ainda que eles não garantam um período fixo de flush.

Gráfico de escritas no volume do gcache mostra rajadas periódicas a cada cerca de 33 segundos, contrastando com o fluxo mais constante de write-sets recebidos
Chart of writes to the gcache volume shows periodic bursts roughly every 33 seconds, contrasting with the steadier flow of received write-sets. Reprodução: percona.com.

Os contornos possíveis

Atualmente não existe correção do lado do kernel para o caminho mmap() especificamente, e o problema não é exclusivo do Galera: qualquer aplicação que use arquivos mapeados por memória está sujeita ao mesmo efeito. A lista de workarounds reportada pela Percona inclui:

  • Usar EXT4 em vez de XFS, quando a versão do kernel permitir: o suporte a folios chegou ao EXT4 só no kernel 6.16, bem mais tarde que no XFS (5.18). Foi essa a solução aplicada no caso do cliente.
  • Reduzir o read_ahead_kb do dispositivo de bloco, ao custo de desempenho potencialmente menor em leituras sequenciais, limitando o tamanho máximo das folios grandes.
  • Modificar a aplicação para usar a dica madvise(MADV_RANDOM) nas chamadas de mmap(), desabilitando efetivamente o readahead por acesso aleatório forçado.
  • Trocar mmap() por pwrite() em kernels 6.6 ou superiores, já que o rastreamento de dirty por bloco só beneficia esse caminho.
  • Compilar um kernel customizado sem suporte a THP (CONFIG_TRANSPARENT_HUGEPAGE desabilitado), já que folios grandes não são criadas sem THP disponível. Vale o alerta: desabilitar THP só via GRUB não é suficiente para desabilitar as folios grandes.

A Percona reportou o problema no rastreador do PXC, no ticket PXC-5340, mas a solução estrutural depende de mudanças no próprio Galera (abandonar mmap()) ou de evolução do kernel para tratar o caso de memory-mapped files com a mesma granularidade já disponível para write()/pwrite().

Como saber se seu cluster está exposto

Para quem administra PXC, MariaDB Galera Cluster ou qualquer binário derivado, vale fazer essa checagem antes do próximo upgrade de sistema operacional, não depois de um incidente em produção:

  1. Confira a combinação kernel + filesystem: XFS em 5.18 ou superior, ou EXT4 em 6.16 ou superior, já habilitam folios grandes.
  2. Leia o readahead do dispositivo com cat /sys/block/<dev>/queue/read_ahead_kb e optimal_io_size.
  3. Verifique mapeamentos grandes olhando FilePmdMapped em /proc/meminfo, ou o smaps do processo mysqld para o mapeamento do gcache, além do contador thp_file_mapped subindo em /proc/vmstat.
  4. Acompanhe o iostat por rajadas a cada 30 a 35 segundos (salvo configuração diferente de vm.dirty_expire_centisecs e vm.dirty_writeback_centisecs), com pidstat -d mostrando cargas em múltiplos de até 2 MB por thread.

Um problema maior que o Galera

O efeito das folios grandes sobre arquivos mapeados por memória não é exclusividade do Galera. O mesmo artigo cita relatos de pressão de I/O excessiva no journal do systemd, cuja causa raiz segue mal compreendida mesmo dentro da própria comunidade systemd, além de um caso separado envolvendo RocksDB, em que misturar workloads de escrita ativa usando page cache com O_DIRECT ao mesmo tempo gerou stalls inesperados.

Para quem projeta ou opera sistemas de armazenamento, a lição estrutural é clara: uma otimização de kernel pensada para cargas sequenciais com RAM e disco abundantes pode se tornar um multiplicador de I/O justamente nos padrões de acesso aleatório e pequeno que caracterizam caches de replicação como o do Galera. Antes de qualquer upgrade combinado de banco de dados e sistema operacional, vale a pena mapear que caminho de I/O cada componente crítico usa, e não presumir que uma melhoria genérica do kernel beneficia igualmente todo o stack.

Translated from the Brazilian Portuguese original · Read the original