top of page

Performance Schema engolindo a sua RAM em Aurora MySQL

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.

 
 
 

Posts recentes

Ver tudo

Comentários


© 2022 por Siltech Consult

  • LinkedIn
bottom of page