Buffer Pool MySQL em Docker: Dimensionamento na Prática Sem OOM Kill.
"Configuramos como sempre — 70% da RAM." Só que a RAM que o MySQL enxergou não era a RAM que o container tinha. E o OOM killer não avisa duas vezes.
Diário de bordo, DBA sênior, estardalhaço nº 65.
Rodar MySQL em container deixou de ser exceção. O que ainda é exceção — pelo menos entre os incidentes que a gente atende — é rodar MySQL em container com dimensionamento de memória feito com o mesmo cuidado que se faz em bare-metal. A regra dos "70-80% da RAM", que já é ponto de partida imperfeito em servidor dedicado, vira armadilha silenciosa dentro do Docker: o MySQL lê a RAM do host, calcula o buffer pool "certinho", e o container é morto pelo OOM killer na primeira carga de pico. Sem coredump útil, sem log de aviso — só um Exit 137 no docker ps.
Este guia é a versão container do nosso artigo sobre dimensionamento de buffer pool: as regras fundamentais continuam valendo (hit ratio, working set, dirty pages, warm-up), mas mudam três coisas críticas — de onde vem o limite de memória real, como calcular a folga para OOM kill em vez de swap, e como preservar o warm-up entre restarts de container.
01
POR QUE DIMENSIONAR MYSQL EM DOCKER É DIFERENTE DE BARE-METAL
Em servidor dedicado, dimensionar buffer pool errado para cima causa swap: o SO empurra páginas para o disco, tudo fica lento, mas o processo continua vivo — e você tem tempo para reagir. Em container, essa margem não existe. Quando o processo tenta alocar acima do limite do cgroup, o kernel dispara o OOM killer imediatamente e mata o processo mysqld inteiro. Sem swap intermediário, sem alerta gradual, sem "está ficando lento" — só derruba. Isso muda a matemática do dimensionamento: em bare-metal, "errar para mais" é ruim; em container, "errar para mais" é fatal.
Além disso, containers de banco de dados frequentemente convivem com sidecars (exporters de métrica, agentes de backup, proxies) no mesmo pod ou host. Cada sidecar consome memória do mesmo limite do cgroup ou do mesmo host físico. Um Prometheus exporter mal configurado pode competir com o MySQL pelo mesmo teto e derrubar quem chegar primeiro no limite.
02
CGROUP VS. RAM DO HOST — O ERRO QUE DERRUBA CONTAINER SILENCIOSAMENTE
O MySQL determina a RAM disponível lendo /proc/meminfo. Dentro de um container Docker padrão, esse arquivo continua reportando a RAM total do host físico, não o limite de memória do container definido via --memory ou resources.limits.memory. Kernels muito recentes com cgroup v2 melhoram parte disso em alguns cenários, mas o comportamento seguro é assumir que o MySQL não sabe o limite real do container — cabe a você fixar o innodb_buffer_pool_size em número absoluto.
# Dentro do container (host de 128 GB, container limitado a 4 GB): cat /proc/meminfo | head -1 # MemTotal: 131072000 kB ← RAM do host, não do container cat /sys/fs/cgroup/memory.max 2>/dev/null || \ cat /sys/fs/cgroup/memory/memory.limit_in_bytes # 4294967296 ← limite real do cgroup (4 GB)
Se o operador usa a heurística ingênua "buffer pool = 70% da RAM visível", o cálculo será feito sobre os 128 GB do host — resultando em innodb_buffer_pool_size = 89 GB dentro de um container de 4 GB. O container morre no primeiro acesso significativo, e o restart automático do Docker reinicia o mesmo processo com o mesmo erro — o clássico restart storm.
03
DIMENSIONAMENTO PRÁTICO — QUANTO DEIXAR PARA O BUFFER POOL EM CADA FAIXA
Em bare-metal a regra é "70-80% da RAM menos margem para conexões". Em container, a regra que tem se mostrado sustentável em produção é mais conservadora, porque o custo de errar para mais é derrubar o processo inteiro:
- Container ≤ 2 GB: buffer pool 40-50% do limite. Sobra pouca RAM absoluta e o overhead fixo do MySQL (thread pool, performance_schema, engine metadata) já consome centenas de MB.
- Container 2-4 GB: buffer pool 50-60% do limite. Faixa mais comum em ambientes de dev/homolog e microserviços com base pequena.
- Container 4-16 GB: buffer pool 60-65% do limite. Já é possível se aproximar dos números de bare-metal, mas com margem para picos de conexão.
- Container 16-64 GB: buffer pool 65-70% do limite. Muito próximo do dimensionamento de bare-metal, mas ainda com 5-10% de margem extra por conta do cgroup rígido.
- Container acima de 64 GB: os ganhos de rodar em container ficam marginais versus a complexidade adicional. Vale reavaliar se o container ainda é a melhor topologia — ou se uma VM dedicada resolveria melhor.
# my.cnf (ou variáveis de ambiente do container) [mysqld] innodb_buffer_pool_size = 2684354560 # 2.5 GB innodb_buffer_pool_instances = 4 innodb_buffer_pool_chunk_size = 134217728 # 128 MB (default) innodb_redo_log_capacity = 536870912 # 512 MB max_connections = 150 performance_schema = ON
Note que buffer_pool_size precisa ser múltiplo de buffer_pool_chunk_size × buffer_pool_instances. Se não for, o MySQL arredonda para cima silenciosamente — o que em container pode ultrapassar o limite planejado. Sempre calcule com esse arredondamento em mente. A calculadora interativa do artigo pai já respeita essa restrição.
04
EXEMPLOS PRÁTICOS — DOCKER COMPOSE, SWARM E KUBERNETES
Os três exemplos abaixo definem o mesmo container de 4 GB de limite com 2.5 GB de buffer pool, em três orquestradores diferentes. O que importa em todos: limite de memória fixo, volume persistente para os datafiles e para o dump do buffer pool, e healthcheck real — não apenas TCP.
services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root
command: >
--innodb-buffer-pool-size=2684354560
--innodb-buffer-pool-instances=4
--innodb-redo-log-capacity=536870912
--innodb-buffer-pool-dump-at-shutdown=ON
--innodb-buffer-pool-load-at-startup=ON
--max-connections=150
deploy:
resources:
limits:
memory: 4G # LIMITE RÍGIDO — dispara OOM kill se ultrapassado
reservations:
memory: 4G # Garante que o container só sobe se houver 4 GB livres
volumes:
- mysql-data:/var/lib/mysql # datafiles + ib_buffer_pool dump
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
interval: 15s
timeout: 5s
retries: 5
start_period: 60s # tempo para o warm-up do buffer pool
secrets:
- mysql_root
volumes:
mysql-data:
secrets:
mysql_root:
file: ./secrets/mysql_root.txtapiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels: { app: mysql }
template:
metadata:
labels: { app: mysql }
spec:
containers:
- name: mysql
image: mysql:8.4
args:
- "--innodb-buffer-pool-size=2684354560"
- "--innodb-buffer-pool-instances=4"
- "--innodb-redo-log-capacity=536870912"
- "--innodb-buffer-pool-dump-at-shutdown=ON"
- "--innodb-buffer-pool-load-at-startup=ON"
- "--max-connections=150"
resources:
requests:
memory: "4Gi" # request = limit → QoS Guaranteed, evita eviction
cpu: "1"
limits:
memory: "4Gi" # OOM kill acima disso
cpu: "2"
volumeMounts:
- name: data
mountPath: /var/lib/mysql
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost"]
initialDelaySeconds: 60
periodSeconds: 20
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost"]
initialDelaySeconds: 30
periodSeconds: 10
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100GiDetalhes que costumam salvar incidente: request igual ao limit em Kubernetes coloca o pod em QoS Guaranteed, o que reduz drasticamente a chance de o pod ser evicted em pressão de nó; initialDelaySeconds ≥ 60s dá tempo para o warm-up recarregar o buffer pool sem o probe matar o pod no meio do carregamento; e volume persistente tem que cobrir /var/lib/mysql inteiro — não só o subdiretório de datafiles — porque o ib_buffer_pool mora lá e é ele que preserva o warm-up entre restarts.
05
MEDINDO O QUE IMPORTA DE DENTRO DO CONTAINER
Métricas de dentro do MySQL (hit ratio, dirty pages, pending flushes) valem igual em container e em bare-metal — a query de hit ratio continua a mesma. O que muda é o par de métricas de fora que você precisa observar em conjunto: o uso real de memória do container versus o limite do cgroup.
# Uso versus limite (rápido, do host): docker stats --no-stream mysql # NAME MEM USAGE / LIMIT MEM % # mysql 3.42GiB / 4GiB 85.5% # Em Kubernetes: kubectl top pod mysql-0 --containers # OOM kills recentes (aparece em dmesg do host): dmesg | grep -i "killed process" | tail # Killed process 12345 (mysqld) total-vm:5242880kB, anon-rss:4180000kB, ...
Se MEM % se mantém consistentemente acima de 90%, ou se dmesg registra qualquer Killed process ... mysqld, o dimensionamento está no limite — reduza o buffer pool ou aumente o limite do container antes que aconteça em produção sob pico real.
06
OOM KILL, EXIT 137 E O CICLO DE RESTART STORM
Quando o container morre por OOM, o processo termina com exit code 137 (128 + sinal 9). Se a política de restart é always ou unless-stopped, o orquestrador reinicia o container automaticamente — com o mesmo innodb_buffer_pool_size superdimensionado. O ciclo se repete indefinidamente: sobe, carrega, começa a servir tráfego, alcança o limite, é morto, reinicia. Cada iteração agrava o problema porque as réplicas de leitura passam a receber a carga extra do primary derrubado, e os conectores de aplicação abrem enxurradas de novas conexões que aceleram o próximo OOM.
Duas defesas obrigatórias contra restart storm em container: alertar em qualquer exit 137 (não apenas em "container down"), tratando-o como incidente de página; e testar dimensionamento em staging com o mesmo limite de memória de produção antes de aplicar em ambiente crítico — o cenário mais comum de OOM em produção é dimensionamento validado em ambiente com limite maior do que o real.
07
PRESERVANDO O WARM-UP ENTRE RESTARTS DE CONTAINER
Um container reinicia com muito mais frequência que um servidor bare-metal — atualização de imagem, rolling deploy, resize de nó, evicção por pressão. Sem preservar o buffer pool entre restarts, cada reinício zera o cache e a instância volta lenta por minutos ou horas, dependendo do tamanho do working set e da velocidade do storage.
A dupla innodb_buffer_pool_dump_at_shutdown + innodb_buffer_pool_load_at_startup resolve isso — mas em container tem uma condição extra: o arquivo ib_buffer_pool, que armazena a lista de páginas quentes, precisa estar em volume persistente. Se estiver no layer efêmero do container, é perdido a cada restart e a otimização vira letra morta.
-- Confirmar que estão ligados (ambos default ON desde MySQL 5.6): SHOW VARIABLES LIKE 'innodb_buffer_pool_dump_at_shutdown'; SHOW VARIABLES LIKE 'innodb_buffer_pool_load_at_startup'; SHOW VARIABLES LIKE 'innodb_buffer_pool_filename'; -- innodb_buffer_pool_filename = ib_buffer_pool -- Precisa estar em datadir montado como volume persistente. -- Verificar o progresso do warm-up após um restart: SHOW STATUS LIKE 'Innodb_buffer_pool_load_status';
Em produção, o warm-up de um buffer pool de 8-16 GB via load_at_startup costuma completar em 1-3 minutos em storage SSD — ordens de magnitude mais rápido que reconstruir o cache pelo tráfego natural. É o tipo de detalhe que separa um restart transparente para o usuário final de um deploy que gera pico de latência visível por 30 minutos.
CHECKLIST — MYSQL EM DOCKER SEM OOM KILL
innodb_buffer_pool_sizedefinido em número absoluto (bytes), calculado sobre o limite do cgroup — nunca sobre a RAM visível pelo container.- Limite de memória do container/pod fixo (
--memory,limits.memory) — nunca "sem limite". - Em Kubernetes,
requests.memory == limits.memory→ QoS Guaranteed, sem eviction em pressão de nó. - Buffer pool em faixa de 40-70% do limite conforme tamanho do container, mais conservador que em bare-metal.
innodb_buffer_pool_sizemúltiplo dechunk_size × instances, para evitar arredondamento silencioso que ultrapasse o limite.- Volume persistente cobrindo
/var/lib/mysqlinteiro, incluindo oib_buffer_poolpara preservar o warm-up. dump_at_shutdowneload_at_startupconfirmados comoON.- Healthcheck real (
mysqladmin ping) comstart_period/initialDelaySeconds≥ 60s para não matar o container no meio do warm-up. - Alerta em qualquer
exit code 137ouKilled process ... mysqldnodmesgdo host, tratado como incidente. - Dimensionamento validado em staging com o mesmo limite de memória de produção — não com limite maior "por segurança".
QUANDO CHAMAR UM DBA ESPECIALISTA
Container é uma camada a mais entre o MySQL e o hardware — e cada camada extra introduz um conjunto novo de modos de falha silencioso: cgroup mal configurado, sidecar competindo por memória, orquestrador reagindo mais rápido do que o warm-up completa, storage class do Kubernetes com latência maior do que a esperada. Diagnosticar esse tipo de incidente exige olhar simultaneamente para métricas do MySQL, do container e do host — e essa correlação é onde a maioria dos times perde tempo.
Fazemos esse tipo de dimensionamento e diagnóstico em todo Health Check de banco de dados, em conjunto com os quatro pilares já cobertos em Como Ler um EXPLAIN do MySQL, Índices Demais no MySQL, Erros em Queries MySQL e Buffer Pool no MySQL. Em container, esses quatro pilares seguem sendo os mesmos — só ganham uma quinta variável: o limite do cgroup, que precisa ser tratado como parâmetro de dimensionamento tanto quanto innodb_buffer_pool_size.
PRÓXIMO PASSO
MySQL em container só é confiável quando o limite do cgroup entra na conta do buffer pool.
Se você já viu um Exit 137 no docker ps ou um pod entrando em CrashLoopBackOff sem log claro, a causa raiz costuma estar aqui.
PERGUNTAS FREQUENTES
MySQL em Docker enxerga o limite de memória do container?
Historicamente, não. O MySQL lê /proc/meminfo, que em Docker/Kubernetes ainda reporta a RAM total do host físico, não o limite do cgroup do container. É por isso que innodb_buffer_pool_size precisa ser dimensionado explicitamente pelo limite do container — nunca por 'porcentagem da RAM visível'. Kernels modernos com cgroup v2 melhoram parte disso, mas o hábito seguro continua sendo fixar o valor.
Quanto de RAM eu deixo para o buffer pool em um container de 4 GB?
Regra prática segura: buffer pool ≈ 50-60% do limite do container em containers pequenos (≤ 4 GB), subindo para 60-70% em containers médios (8-32 GB). O restante cobre conexões, overhead do próprio MySQL e margem para picos — que em container têm consequência maior porque geram OOM kill imediato em vez de swap.
Posso rodar MySQL de produção em container?
Sim, e é cada vez mais comum. O que muda é a disciplina: limites de memória fixos e testados sob carga, volumes persistentes para os datafiles, healthcheck adequado, política de restart consciente (não 'always' cego), e monitoramento do OOM killer do host. Sem isso, é uma questão de tempo até um restart storm no primeiro pico.
innodb_buffer_pool_size dinâmico funciona em container?
Funciona igual ao MySQL bare-metal: SET GLOBAL innodb_buffer_pool_size é online desde 5.7.5. A diferença é que subir o valor pode levar o processo acima do limite do cgroup e disparar OOM kill do container inteiro — teste sempre em ambiente de staging com o mesmo limite de memória de produção antes de aplicar.
Como sobreviver a um restart do container sem perder o cache?
innodb_buffer_pool_dump_at_shutdown e innodb_buffer_pool_load_at_startup precisam estar ON (default desde 5.6), e o arquivo ib_buffer_pool precisa ficar em volume persistente — não no filesystem efêmero do container. Sem isso, cada 'docker restart' zera o cache e a instância volta lenta por minutos ou horas.