Performance Schema engolindo a sua RAM em Aurora MySQL
- Siltech Consult
- há 9 horas
- 9 min de leitura
O performance_schema do MySQL 8.0 / Aurora MySQL 3.x dimensiona automaticamente dezenas de estruturas internas baseando-se em max_connections, número de schemas, tabelas e contas de usuário. Em ambientes multi-tenant com centenas a milhares de schemas e usuários, esse auto-sizing pode consumir 4 a 10 GB de RAM sem você perceber e em muitos casos coletando estatísticas que ninguém consulta. Em instâncias com classe fixa (sem possibilidade de upgrade), essa memória “fantasma” pode ser a diferença entre aurora_oom_status = NORMAL e aurora_oom_status = LOW_MEMORY.
O cenário que motivou este artigo
Um cluster Aurora MySQL 3.08.1 (compatível com 8.0.39) hospedando uma plataforma multi-tenant de e-commerce começou a apresentar sinais persistentes de pressão de memória:
• aurora_oom_status=LOW_MEMORY no SHOW GLOBAL STATUS.
• aurora_oom_avoidance_recovery_state=ACTIVE (mecanismo de defesa engatado).
• FreeableMemory no CloudWatch estabilizado em torno de 4 GB, sem oscilação significativa em 24h.
• Innodb_buffer_pool_pages_free=0 (zero páginas livres no buffer pool).
Características do ambiente:
• 1.494 schemas (*_loja), um por tenant.
• Aproximadamente 1.500 contas de usuário (uma por schema).
• Dezenas de milhares de tabelas (cada tenant replica a estrutura da aplicação).
• Pico de 2.804 conexões simultâneas contra max_connections = 3000.
• Instância de classe média sem possibilidade imediata de upgrade.
Buffer pool configurado em 43 GB, com hit ratio cumulativo de 99,9992%, número aparentemente excelente. Mas com pressão de RAM persistente. Algo grande estava consumindo memória fora do buffer pool.
A investigação levou ao performance_schema consumindo 4,61 GB, sendo 76% dele em apenas sete estruturas que ninguém na operação usava.
Por que o auto-sizing do Performance Schema é perigoso
A partir do MySQL 5.7, várias estruturas do performance_schema aceitam o valor -1 (ou autoscaled), que delega ao próprio servidor o dimensionamento no boot. As fórmulas internas combinam:
• max_connections
• Número de tabelas no servidor
• Número de schemas
• Número de contas (user@host) distintas
• Valores de outras estruturas auto-sizadas (efeito cascata)
Em uma instância “padrão” com 200 conexões, 50 tabelas e 5 usuários, o auto-sizing produz arrays modestos. Em ambientes multi-tenant, os multiplicadores explodem:
Estrutura | Cálculo aproximado | Em multi-tenant (1.500 contas, 45k tabelas, 3k conn) |
events_statements_summary_by_account_by_event_name | accounts × statement_classes × ~80 bytes | 1500 × 224 × 80 = 27 MB por evento; total >700 MB |
events_errors_summary_by_account_by_error | accounts × error_count × ~16 bytes | ~800 MB |
table_io_waits_summary_by_index_usage | table_instances × index_avg × ~120 bytes | ~450 MB |
table_shares | table_definition_cache × ~10 KB | ~470 MB |
events_waits_summary_by_account_by_event_name | accounts × wait_classes × ~80 bytes | ~330 MB |
memory_summary_by_account_by_event_name | accounts × memory_classes × ~110 bytes | ~500 MB |
O agravante: a maioria das ferramentas de observabilidade não consulta as agregações by_account, usam globalby_event_name, by_digest ou by_thread. Você está pagando por dados que ninguém lê.
Outro agravante específico do Aurora: o storage engine é diferente do InnoDB clássico, e várias estruturas que serviriam para diagnóstico do log/buffer no MySQL community (Innodb_log_writes, Innodb_os_log_written) ficam zeradas. As métricas substitutas ficam no CloudWatch e Performance Insights, não no performance_schema. Resultado: a “fatura” de RAM do performance_schema é paga, mas os dados raramente retornam valor proporcional.
Diagnosticando: você está pagando essa fatura?
Passo 1 — Verificar o estado de pressão de memória (Aurora)
SHOW GLOBAL STATUS LIKE 'aurora_oom%';
Saída esperada em uma instância saudável:
aurora_oom_status = NORMAL
aurora_oom_avoidance_recovery_state = INACTIVE
Saída de uma instância sob pressão:
aurora_oom_status = LOW_MEMORY
aurora_oom_avoidance_recovery_state = ACTIVE
Quando aurora_oom_status = LOW_MEMORY, a engine já está aplicando contenção: abortando queries grandes, limitando alocações temporárias e liberando buffers preventivamente. O próximo nível é RESERVED_MEM_LOW (drops de conexão) e depois CRITICAL.
Complemente com:
SELECT ROUND( (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_free') / (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_total') * 100, 2 ) AS bp_free_pct;
bp_free_pct = 0 por períodos sustentados indica que o LRU do buffer pool nunca tem folga, sinal de RAM apertada.
E no CloudWatch:
aws cloudwatch get-metric-statistics \ --namespace AWS/RDS \ --metric-name FreeableMemory \ --dimensions Name=DBInstanceIdentifier,Value=<sua-instancia> \ --start-time $(date -u -d '-24 hours' +%Y-%m-%dT%H:%M:%SZ) \ --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \ --period 300 \ --statistics Minimum Average \ --region <sua-regiao>
Sinais preocupantes: - Minimum consistentemente abaixo de 1 GB. - Amplitude (máximo − mínimo) menor que 200 MB em 24h, indica memória presa, não workload variável. - Tendência decrescente em janelas semanais.
Passo 2 — Medir o consumo total do Performance Schema
A query mais importante deste artigo:
SELECT ROUND(SUM(CURRENT_NUMBER_OF_BYTES_USED)/1024/1024/1024, 2) AS gb_total_pfsFROM performance_schema.memory_summary_global_by_event_nameWHERE EVENT_NAME LIKE 'memory/performance_schema/%';
Valores de referência em um ambiente multi-tenant grande:
Total | Interpretação |
< 500 MB | Normal — auto-sizing comportado |
500 MB – 2 GB | Esperado em ambientes grandes |
2 – 5 GB | Investigar — quase certamente há gordura |
> 5 GB | Problema confirmado — agir |
Passo 3 — Identificar quais estruturas estão consumindo
SELECT EVENT_NAME, ROUND(CURRENT_NUMBER_OF_BYTES_USED/1024/1024, 2) AS mb_usado, CURRENT_COUNT_USED AS instanciasFROM performance_schema.memory_summary_global_by_event_nameWHERE EVENT_NAME LIKE 'memory/performance_schema/%' AND CURRENT_NUMBER_OF_BYTES_USED > 1048576ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESCLIMIT 30;
Padrões típicos de problema:
• summaryby_account_by_ no top 5: indica performance_schema_accounts_size = -1 com muitos user@host distintos. Padrão clássico de multi-tenant.
• table_shares, table_io_waits_summary_by_index_usage, table_lock_waits_summary_by_table grandes: indica performance_schema_max_table_instances = -1 e performance_schema_max_index_stat = -1 inflados pelo número de tabelas.
• events_*_history_long ocupando >100 MB cada: histórico longo desnecessariamente grande (default 10.000 entradas por consumer).
• mutex_instances / rwlock_instances / cond_instances somando >500 MB: auto-sizing baseado em conexões e tabelas.
Passo 4 — Auditar os parâmetros em modo auto
SHOW VARIABLES WHERE Variable_name LIKE 'performance_schema%';
Procure por valores -1 (auto). Em um parameter group default do Aurora MySQL 3.x, é normal ver mais de 15 parâmetros em -1:
performance_schema_accounts_size= -1
performance_schema_users_size = -1
performance_schema_hosts_size = -1
performance_schema_max_cond_instances = -1
performance_schema_max_file_instances = -1
performance_schema_max_index_stat = -1
performance_schema_max_metadata_locks = -1
performance_schema_max_mutex_instances = -1
performance_schema_max_prepared_statements_instances = -1
performance_schema_max_program_instances = -1
performance_schema_max_rwlock_instances = -1
performance_schema_max_socket_instances = -1
performance_schema_max_table_handles = -1
performance_schema_max_table_instances = -1
performance_schema_max_table_lock_stat = -1
performance_schema_max_thread_instances = -1
Em ambiente multi-tenant, cada um desses -1 é uma alavanca de tuning.
Quais parâmetros importam e como dimensionar
Os parâmetros se dividem em quatro categorias por origem do consumo. Vou apresentar cada um com a fórmula que sugere o valor adequado.
Categoria 1 — Arrays “by_account / by_user / by_host”
Estes três são os maiores ofensores em multi-tenant:
performance_schema_accounts_size = 200performance_schema_users_size = 100performance_schema_hosts_size = 200
Critério: você raramente quer estatísticas por conta individual em 1.494 schemas. Se precisar investigar um tenant específico, usa filtros nos bythread ou bydigest com SCHEMA_NAME. Dimensionar para accounts_size = max(usuarios_realmente_monitorados, 200).
Ganho típico: 1,5 a 3 GB.
Risco: você perde as agregações by_account (várias tabelas com sufixo byaccount_by_event_name ficarão truncadas). Aceitável se sua observabilidade já está em outras camadas (Performance Insights, CloudWatch, APM da aplicação).
Categoria 2 — Estruturas dimensionadas por número de tabelas
performance_schema_max_table_instances = 4000
performance_schema_max_table_handles = 2000
performance_schema_max_table_lock_stat = 2000
performance_schema_max_index_stat = 5000
Critério: quantas tabelas você toca simultaneamente durante operação normal, não quantas existem. Em multi-tenant, mesmo com 45.000 tabelas no schema dictionary, apenas algumas centenas são acessadas em uma janela de tempo curta. Dimensione 2x o table_open_cache se o workload for previsível, ou monitore Opened_tables:
SHOW GLOBAL STATUS LIKE 'Opened_tables';
Se Opened_tables cresce a < 10/segundo, você pode reduzir esses limites com segurança. Se cresce a > 100/s, considere subir o table_open_cache em vez de reduzir esses limites.
Ganho típico: 500 MB a 1 GB.
Risco: tabelas pouco acessadas podem cair do cache do PFS e perder histórico de I/O, mas o events_statements_summary_by_digest continua funcionando.
Categoria 3 — Histórico de eventos
performance_schema_events_statements_history_long_size = 1000
performance_schema_events_waits_history_long_size = 1000
performance_schema_events_stages_history_long_size = 100
performance_schema_events_transactions_history_long_size = 1000
Critério: histórico é útil para investigação reativa (“o que aconteceu há 5 minutos?”). 10.000 entradas por consumer (default) é ferramenta de pesquisa, não de operação. Para Aurora, o Performance Insights já mantém histórico próprio com retenção em dias, não em milhares de eventos. Reduzir o histórico do PFS não cega o operador.
Ganho típico: 200 a 500 MB.
Mantenha alto: performance_schema_digests_size = 10000. Este é o agregador mais útil (top SQL por digest) e é barato. Não reduza abaixo de 5000.
Categoria 4 — Limites de mutexes, rwlocks, conds, files
performance_schema_max_mutex_instances = 50000
performance_schema_max_rwlock_instances = 30000
performance_schema_max_cond_instances = 5000
performance_schema_max_thread_instances = 1500
performance_schema_max_file_instances = 20000
performance_schema_max_metadata_locks = 8192
Critério: em modo -1, esses valores escalam com max_connections × N. Em uma instância com max_connections = 3000, isso vira centenas de milhares. Dimensionar para o pico real observado:
• thread_instances ≥ pico de threads (Max_used_connections em SHOW GLOBAL STATUS).
• mutex_instances ≈ 15-20 × thread_instances.
• metadata_locks ≈ 2-3 × pico de conexões.
Ganho típico: 300 a 800 MB.
Risco: se subestimar, o PFS para de instrumentar novos objetos a partir do limite (visível em performance_schema.global_status → Performance_schema_*_lost). Monitore após a mudança:
SHOW GLOBAL STATUS LIKE 'Performance_schema_%_lost';
Valores incrementando > 0 indicam que você subdimensionou, basta subir o parâmetro correspondente no próximo reboot.
Categoria 5 — Tamanho de strings armazenadas
performance_schema_max_sql_text_length = 512
performance_schema_max_digest_length = 512
Critério: o default de 1024 bytes por SQL armazenado, multiplicado por dezenas de milhares de slots de histórico e digests, soma. Reduzir para 512 bytes corta queries longas (relatórios SQL gigantes ficam truncados), mas mantém digests legíveis para 95% dos casos.
Ganho típico: 100 a 300 MB.
Aplicando: do parameter group ao reboot
1. Crie um parameter group de rollback antes
Esta é a operação mais crítica. Antes de qualquer mudança:
# Copie o parameter group atual com nome temporalaws rds copy-db-cluster-parameter-group \ --source-db-cluster-parameter-group-identifier <pg-atual> \ --target-db-cluster-parameter-group-identifier <pg-atual>-rollback-$(date +%Y%m%d) \ --target-db-cluster-parameter-group-description "Rollback pre-PFS-tuning" \ --region <regiao>
Se algo der errado após o reboot, basta reassociar o cluster ao parameter group de rollback e reiniciar.
2. Verifique se innodb_buffer_pool_size é modificável
Em Aurora, alguns parâmetros do parameter group default vêm como fórmula ({DBInstanceClassMemory*3/4}) e podem não aceitar override. Confirme:
aws rds describe-db-cluster-parameters \ --db-cluster-parameter-group-name <seu-pg> \ --query "Parameters[?ParameterName=='innodb_buffer_pool_size'].[ParameterValue,IsModifiable,ApplyType]" \ --region <regiao> --output table
3. Aplique em duas ondas
Onda 1 — parâmetros dinâmicos (sem reboot, baixíssimo risco):
# Buffers de sessão
sort_buffer_size = 131072
join_buffer_size = 131072
read_buffer_size = 131072
read_rnd_buffer_size = 262144
aurora_temptable_max_ram_allocation = 268435456
# Aurora-específicos
aurora_parallel_query = ON
# Outros
table_definition_cache = 8000
long_query_time = 1
Aplique com --apply-method immediate. Aguarde 24h para observar FreeableMemory e confirmar que nada quebrou.
Onda 2 — parâmetros estáticos (precisam reboot):
Aplique todos os parâmetros do Performance Schema (categorias 1 a 5 acima) com --apply-method pending-reboot. Aproveite o mesmo reboot para aplicar:
innodb_buffer_pool_size = <valor calculado>
# se cabe reduzir
innodb_buffer_pool_instances = 16 # se estava em 8
Um reboot único aplica toda a Onda 2.
4. Valide pós-reboot
Query de validação consolidada:
SELECT ROUND((SELECT SUM(CURRENT_NUMBER_OF_BYTES_USED)/1024/1024/1024 FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME LIKE 'memory/performance_schema/%'), 2) AS pfs_gb, ROUND( (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_free') / (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_total') * 100, 2) AS bp_free_pct, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='aurora_oom_status') AS oom_state;
Verifique também se nenhuma estrutura está perdendo eventos por subdimensionamento:
SHOW GLOBAL STATUS LIKE 'Performance_schema_%_lost';
Qualquer valor maior que zero indica que aquele limite precisa subir no próximo ciclo.
A solução final: números que funcionaram
Para o caso real que motivou o artigo (Aurora MySQL 3.08.1, 1.494 schemas, ~64 GB de RAM, pico 2.804 conexões), o parameter group final ficou assim para a parte do Performance Schema:
# Account / user / host arrays (categoria 1) — maior ganho
performance_schema_accounts_size = 200
performance_schema_users_size = 100
performance_schema_hosts_size = 200
# Tabelas (categoria 2)
performance_schema_max_table_instances = 4000
performance_schema_max_table_handles = 2000
performance_schema_max_table_lock_stat = 2000
performance_schema_max_index_stat = 5000
# Histórico (categoria 3) — manter digests, cortar history_longperformance_schema_digests_size = 10000
performance_schema_events_statements_history_long_size = 1000
performance_schema_events_waits_history_long_size = 1000
performance_schema_events_stages_history_long_size = 100
performance_schema_events_transactions_history_long_size = 1000
# Mutexes / rwlocks / outros (categoria 4)performance_schema_max_metadata_locks = 8192
performance_schema_max_mutex_instances = 50000
performance_schema_max_rwlock_instances = 30000
performance_schema_max_cond_instances = 5000
performance_schema_max_thread_instances = 1500
performance_schema_max_file_instances = 20000
performance_schema_max_program_instances = 1000
performance_schema_max_prepared_statements_instances = 1000
performance_schema_max_socket_instances = 3000
# Strings (categoria 5)
performance_schema_max_sql_text_length = 512
performance_schema_max_digest_length = 512
Resultado esperado
Métrica | Antes | Depois |
pfs_gb (Performance Schema total) | 4,61 | ~1,5 |
bp_free_pct (buffer pool livre) | 0,00 | ≥ 5 |
aurora_oom_status | LOW_MEMORY | NORMAL |
FreeableMemory (CloudWatch min) | 4 GB | ≥ 12 GB |
Ganho líquido em RAM: ~3 GB vindos só do Performance Schema. Combinado com redução de buffer pool (no caso real, foi possível de 43 GB para 36 GB com hot set de 31 GB) e parallel query, o total chegou a ~10 GB liberados sem mexer em uma linha da aplicação.
O que você perde — e o que continua funcionando
O que você perde
• Agregações by_account, by_user, by_host: tabelas com esses sufixos passam a refletir apenas os últimos N usuários ativos (onde N é seu novo accounts_size). Para investigações pontuais sobre um tenant específico, isso vira limitação. Mitigação: cruzar events_statements_summary_by_digest com SCHEMA_NAME.
• Histórico longo de eventos: vai mostrar 1000 entradas em vez de 10000. Para análise reativa imediata, ainda é mais que suficiente.
• Possíveis “eventos perdidos” em mutexes/files se subdimensionar, daí a importância de monitorar Performance_schema_*_lost.
O que continua funcionando
• events_statements_summary_by_digest — top SQL, full scans, latências (a base de qualquer SP report).
• events_waits_summary_global_by_event_name — top wait events globais.
• events_statements_summary_by_thread_by_event_name — análise por sessão ativa.
• table_io_waits_summary_by_table — I/O por tabela (este precisa estar dimensionado adequadamente em max_table_instances).
• Todas as métricas via Performance Insights e CloudWatch (independentes do PFS).
• sys.* schema — views derivadas continuam funcionais.
Para 95% dos casos de tuning em produção, o que sobra é mais do que suficiente. Você troca capacidade de investigação granular por estabilidade operacional, em uma instância sob LOW_MEMORY, é uma troca óbvia.
Conclusão
O performance_schema é uma das ferramentas mais valiosas para tuning de MySQL e Aurora, mas o auto-sizing default não foi pensado para ambientes multi-tenant com centenas a milhares de schemas e usuários. Em instâncias sob pressão de RAM e sem possibilidade imediata de upgrade de classe, reavaliar o Performance Schema é uma das poucas alavancas que oferecem múltiplos GBs de ganho sem mexer em código da aplicação e sem comprometer observabilidade essencial.
O processo é direto:
1. Medir o consumo total e identificar os top consumidores.
2. Auditar os parâmetros em modo -1 (auto).
3. Dimensionar com base no workload real, não em fórmulas genéricas.
4. Aplicar em duas ondas (dinâmica primeiro, estática depois).
5. Validar e monitorar Performance_schema_*_lost para ajustes finos.
Em poucos ciclos de tuning, você sai do estado defensivo LOW_MEMORY e devolve à instância a folga de RAM para o que realmente importa: cache de páginas quentes e tabelas temporárias do workload da aplicação.

Comentários