Ajuste de Performance: Cache, Throttling de Compaction e Configurações de JVM/GC
Objective
Transformar os internos do motor de armazenamento (memtables, SSTables, Bloom filters, e compaction) em um conjunto de alavancas que você de fato aciona quando um cluster está lento. O conceito companheiro sobre internos do motor de armazenamento explica o que uma memtable, uma SSTable, um Bloom filter, e uma passagem de compaction são, e por que o design em LSM-tree troca complexidade de leitura por velocidade de escrita. Este conceito é sobre como ajustar os três lugares onde essa troca se torna um botão que você pode girar: os caches do lado da leitura que ficam na frente do acesso a disco, os recursos dados ao processo de compaction em segundo plano que impede que SSTables se acumulem, e o heap e o coletor de lixo da JVM dentro dos quais tudo acima em última análise roda.
Use Cases
- Uma tabela é intensiva em leitura e a latência é dominada por seeks de disco em SSTables: decidir se um key cache (já ligado por padrão), um row cache, ou apoiar-se no cache de chunk/SO é a alavanca certa, em vez de reflexivamente recorrer a mais hardware.
nodetool compactionstatsmostra compactions pendentes se acumulando durante um período intensivo em escrita: decidir se aumentacompaction_throughput_mb_per_sec, aumentaconcurrent_compactors, ou aceita que a tabela está na estratégia de compaction errada (uma decisão de modelagem de dados coberta no conceito de internos do motor de armazenamento, não um botão de ajuste).- Um nó está registrando pausas longas de GC abaixo de
gc_warn_threshold_in_ms, e a pergunta é se aumenta o heap, troca de coletor, ou ambos, e se a orientação do livro sobre o Concurrent Mark Sweep (CMS) é sequer executável na versão de JDK de fato em produção. - Comparar
nodetool proxyhistograms(latência de leitura/escrita/intervalo no nível do coordenador) contranodetool tablehistogramsem uma tabela específica para decidir se um problema de performance é de todo o cluster (apontando para compaction, GC, ou hardware) ou específico de uma tabela (apontando para cache ou estratégia de compaction daquela tabela). - Rodar
cassandra-stressou uma carga de trabalho customizadacqlstress-*.yamlcontra um cluster de staging antes de mudar uma configuração decassandra.yamloujvm.optionsde produção, seguindo a metodologia de ajuste do livro: mude um parâmetro de cada vez e meça.
Deep Dive
Metodologia de ajuste primeiro: uma mudança, uma medição
Antes de qualquer botão específico, o livro é explícito sobre a disciplina que este capítulo inteiro assume: "a metodologia sugerida para ajustar a performance do Cassandra é mudar um parâmetro de configuração de cada vez e testar os resultados. É importante limitar a quantidade de mudanças de configuração que você faz ao ajustar, para que você possa identificar claramente o impacto de cada mudança." Objetivos de performance devem ser enunciados tanto em throughput quanto em latência, em um percentil: "o cluster precisa suportar 30.000 operações de leitura por segundo na tabela available_rooms_by_hotel_date, com uma latência de leitura no percentil 99 de 5 ms" é o próprio exemplo do livro de um objetivo específico o suficiente para ajustar contra ele. Toda configuração abaixo é uma mudança candidata a fazer uma vez, medir com nodetool tablehistograms/proxyhistograms ou cassandra-stress, e manter ou revertê-la.
Cache: key cache, row cache, chunk cache, counter cache
O conceito de internos do motor de armazenamento já cobre o que é um Bloom filter e o chama de "um tipo especial de key cache": uma checagem rápida, em memória, não determinística, que permite que uma leitura pule o arquivo de disco de uma SSTable inteiramente. O key cache é o cache literal que essa checagem alimenta. "O key cache do Cassandra armazena um mapa de partition keys para entradas de índice de linha, facilitando acesso de leitura mais rápido a SSTables armazenadas em disco." Um Bloom filter diz "talvez presente, vá checar o arquivo"; o key cache é o que torna "vá checar o arquivo" barato uma vez que o Bloom filter diz sim, pulando direto para o offset de índice da linha, em vez de varrer o índice da SSTable desde o início.
O livro enquadra todas as decisões de cache em torno de três fatores: "considere suas queries, e use o tipo de cache que melhor se encaixa nelas... considere a proporção entre o tamanho do seu heap e o tamanho do seu cache, e não permita que o cache sobrecarregue seu heap... considere o tamanho das suas linhas contra o tamanho das suas chaves. Tipicamente chaves vão ser muito menores do que linhas inteiras."
Key cache. Habilitado por padrão por tabela: caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} é o que DESCRIBE TABLE mostra para uma tabela que não foi tocada. "Como o key cache aumenta bastante a performance de leitura sem consumir muita memória adicional, ele é habilitado por padrão." Seu orçamento de heap é compartilhado por todo o cluster via key_cache_size_in_mb, com padrão de "5% do heap total da JVM, ou 100 MB, o que for menor." Desabilitá-lo (ALTER TABLE ... WITH caching = {'keys': 'NONE', ...}) raramente é o movimento certo, dado quão pouca memória custa em relação ao ganho de latência de leitura.
Row cache. Faz cache de linhas inteiras, em vez de apenas um apontador de índice, a um custo real de memória, e o livro é incomumente direto ao dizer que frequentemente é a ferramenta errada: "o row caching tende a produzir menos benefícios do que o key caching... em muitos casos, um row cache pode produzir resultados de performance impressionantes para conjuntos de dados pequenos quando todas as linhas estão em memória, só para degradar em conjuntos de dados maiores quando o dado precisa ser lido do disco." A orientação é estreita: "row caching só é recomendado para cargas de trabalho intensivas em leitura, digamos 95% de leituras." Configure-o por tabela via rows_per_partition (uma contagem, ou ALL; o padrão NONE significa desabilitado): "se você está usando um row cache para uma dada tabela, você não vai precisar usar um key cache nela também." A implementação padrão é off-heap (org.apache.cassandra.OHCProvider), o que impede que um row cache grande pressione diretamente o GC.
Chunk cache. Esse é o que o livro avalia acima do row caching na maioria dos casos, precisamente porque não compartilha o problema de invalidação do row cache: "o chunk cache é considerado mais útil do que o row cache na maioria dos casos, já que é mais granular; o conteúdo do row cache é invalidado para uma partição inteira a cada vez que há uma escrita na partição." O chunk cache armazena chunks descomprimidos lidos de arquivos SSTable (tamanho de chunk definido por tabela por chunk_length_in_kb nas opções de compressão), evitando descompressão repetida de dados quentes, acessados com frequência. É habilitado por padrão e dimensionado via file_cache_size_in_mb.
Counter cache. Propósito mais estreito: "melhora a performance de counter reduzindo disputa de lock para os counters mais frequentemente acessados", sem configuração por tabela, apenas um counter_cache_size_in_mb de todo o cluster (padrão "2,5% do heap total da JVM, ou 50 MB, o que for menor").
Operando os caches. Todos os caches podem ser periodicamente persistidos em disco sob saved_caches e recarregados na inicialização para aquecer o cache sem esperar por tráfego orgânico, controlado por tipo de cache pelas propriedades *_save_period e *_keys_to_save. Em tempo de execução, nodetool info reporta taxas de acerto por cache; nodetool invalidatekeycache / invalidaterowcache / invalidatecountercache limpam um cache, e nodetool setcachecapacity / setcachekeystosave sobrescrevem os tamanhos configurados sem um reinício, ainda que "essas configurações voltem aos valores definidos no arquivo cassandra.yaml em um reinício do nó."
Ajuste de compaction: throughput, concorrência, e limiares
O conceito de internos do motor de armazenamento cobre por que a compaction existe e qual estratégia (STCS, LCS, TWCS, ou a mais nova UnifiedCompactionStrategy da 5.0) se encaixa em uma dada carga de trabalho: essa é uma decisão de modelagem de dados tomada uma vez por tabela. O que se segue aqui é a pergunta separada de quanto do orçamento de I/O e CPU do nó a compaction tem permissão para consumir, uma vez que uma estratégia é escolhida, já que, como o livro coloca, "compaction pode ser intensiva em termos de I/O e CPU."
Throttling de throughput. compaction_throughput_mb_per_sec no cassandra.yaml limita a taxa que a compaction tem permissão para escrever, verificável e configurável ao vivo via nodetool getcompactionthroughput / setcompactionthroughput. "Definir esse valor como 0 desabilita o throttling completamente, mas o valor padrão de 16 MBps é suficiente para a maioria dos casos que não são intensivos em escrita." Isso é um trade-off direto: throttle mais forte e o I/O do cliente mantém mais folga, mas SSTables se acumulam mais rápido do que são mescladas; throttle menor e a compaction se recupera mais rápido ao custo de latência de leitura/escrita para clientes compartilhando o mesmo disco.
Concorrência. Se o throughput sozinho não limpa um backlog, concurrent_compactors controla quantas threads de compaction rodam em paralelo, configurável no cassandra.yaml ou ao vivo via CompactionManagerMBean. "Por padrão é o mínimo entre o número de discos e o número de núcleos, com um mínimo de 2 e um máximo de 8."
Limiares de compaction por tabela. Distinto da própria estratégia, o limiar controla quando uma compaction menor dispara: "o número de SSTables que estão na fila para serem compactadas antes que uma compaction menor seja de fato disparada. Por padrão, o mínimo é 4 e o máximo é 32." Definido via CREATE TABLE/ALTER TABLE, ou inspecionado e sobrescrito por nó com nodetool getcompactionthreshold / setcompactionthreshold. Baixo demais e "o Cassandra vai gastar tempo disputando recursos com clientes para realizar muitas compactions frequentes e desnecessárias"; alto demais e um único evento de compaction disputa mais forte por recursos quando finalmente roda.
Monitoramento e controle. nodetool compactionstats lista compactions ativas com uma contagem de bytes completados/total e tempo restante estimado: o primeiro lugar para olhar ao diagnosticar um suspeito backlog de compaction. nodetool compactionhistory retém detalhes de compactions completadas. nodetool stop pode parar uma compaction específica ou todas as ativas (o Cassandra as reagenda), e nodetool disableautocompaction / enableautocompaction dão controle por keyspace/tabela sobre se a compaction roda de forma alguma. Um nodetool compact (compaction major) forçado existe, mas o livro sinaliza ressalvas reais: para SizeTieredCompactionStrategy use a flag -s "para solicitar que o Cassandra crie múltiplos arquivos SSTable menores, em vez de um único arquivo SSTable grande", e se o objetivo real é recuperar espaço de dados deletados, nodetool garbagecollect é a alternativa mais direcionada a uma compaction major completa.
Testando uma mudança de estratégia com segurança. Em vez de mudar uma estratégia de compaction em todo o cluster para ver como ela se comporta, o livro descreve o modo de pesquisa de escrita (write survey mode): inicie um nó com -Dcassandra.write_survey=true -Djoin_ring=false no jvm.options, configure os novos CompactionParameters via JMX naquele nó isolado, então faça nodetool join nele para que receba um stream ao vivo, mas não autoritativo, de escritas ("escritas para o nó de teste colocam uma carga adicional mínima no cluster e não contam para níveis de consistência"). A performance de leitura para a nova estratégia é então testada tirando o nó do ar e consultando-o de forma isolada.
Ajuste de heap e GC da JVM: do CMS em direção ao G1GC
Dimensionamento de heap. O algoritmo padrão de dimensionamento de heap do Cassandra: em uma máquina com menos de 1 GB de RAM, o heap é 50% da RAM; acima de 4 GB de RAM, o heap é 25% da RAM, limitado a 8 GB. Ajuste manual usa -Xms/-Xmx, e o conselho do livro é definir os dois iguais "para permitir que o heap inteiro seja travado em memória e não trocado para disco (swap) pelo SO." -XX:+HeapDumpOnOutOfMemoryError está ligado por padrão no cassandra-env.sh e vale a pena deixar ligado. A partir da versão 3.0, flags relacionadas a GC e heap se moveram para um jvm.options dedicado (agora dividido ainda mais em jvm-server.options mais arquivos por versão de JDK, como jvm11-server.options), consumido pelo cassandra-env.sh.
O padrão do livro: ParNew + CMS. O livro descreve o padrão da 3.x/4.0 como dois coletores trabalhando em gerações diferentes do heap: a geração jovem usa o coletor de cópia paralelo (-XX:+UseParNewGC), ajustado via -XX:SurvivorRatio (o Cassandra define 8, ou seja, uma proporção eden-para-survivor de 1:8, "bastante baixa, porque os objetos vivem mais tempo nas memtables") e -XX:MaxTenuringThreshold (o Cassandra define 1). A geração antiga usa Concurrent Mark Sweep, -XX:+UseConcMarkSweepGC: "essa configuração usa mais RAM e poder de CPU para fazer coletas de lixo frequentes enquanto a aplicação está rodando, a fim de manter o tempo de pausa do GC no mínimo." O livro também observa um teto de heap para esse coletor: "não é recomendado definir o heap maior do que 12 GB se você está usando o coletor de lixo Concurrent Mark Sweep (CMS), já que tamanhos de heap maiores do que esse valor tendem a levar a pausas de coleta de lixo mais longas."
G1GC, segundo o livro, como a alternativa pendente. "O coletor de lixo Garbage-First (também conhecido como G1GC)... tinha a intenção de se tornar o substituto de longo prazo para a coleta de lixo do CMS... o G1GC geralmente exige menos decisões de ajuste; o uso pretendido é que você só precise definir o tamanho mínimo e máximo do heap e um objetivo de tempo de pausa." O livro tem o cuidado de observar que isso não era uma questão resolvida na época da 3ª edição revisada: "o G1GC originalmente era o padrão para a versão 3.0 do Cassandra, mas foi retirado porque não performava tão bem quanto o CMS para tamanhos de heap menores do que 8 GB. O consenso emergente é que o G1GC performa bem sem ajuste, mas a configuração padrão de ParNew/CMS pode resultar em pausas mais curtas quando ajustada adequadamente." O livro também nomeia dois coletores mais novos e mais experimentais disponíveis em JDKs mais recentes: ZGC ("seus objetivos primários são limitar tempos de pausa a 10 ms ou menos, e escalar até mesmo para heaps na faixa de múltiplos terabytes") e Shenandoah ("funciona melhor do que o ZGC em tamanhos de heap menores... mas o SGC pode ter latências de cauda piores"), ambos sinalizados como ainda em maturação na época em que foi escrito.
Book vs today
A recomendação de coletor se inverteu, e o CMS não é mais apenas "não preferido": é inutilizável nas versões de JDK que o Cassandra agora visa. O próprio texto do livro já sugeria que isso estava em fluxo ("houve uma discussão considerável na comunidade Cassandra sobre trocar para o G1GC como padrão"), e desde então foi resolvido:
- Checar os arquivos de configuração de fato distribuídos confirma a mudança: o
conf/jvm11-server.optionsdo Apache Cassandra 4.0 e 4.1 ainda habilita CMS por padrão (-XX:+UseConcMarkSweepGCativo, a seção G1 comentada), combinando exatamente com a descrição do livro. Oconf/jvm11-server.optionse oconf/jvm17-server.optionsdo Apache Cassandra 5.0 invertem isso: o G1GC é o padrão ativo (-XX:+UseG1GCdescomentado, com padrões ajustados como-XX:G1HeapRegionSize=16me-XX:MaxGCPauseMillis=300), e o bloco CMS está presente apenas como uma opção legada comentada. - Isso acompanha um fato duro do JDK, independente da própria preferência do Cassandra: o CMS foi depreciado no JDK 9 (JEP 291) e removido completamente no JDK 14 (JEP 363). O Cassandra 5.0 suporta rodar em JDK 11 ou JDK 17, o que significa que em uma implantação JDK 17, as flags CMS do livro não são apenas não-padrão, elas vão falhar em iniciar a JVM completamente. Não existe uma configuração CMS para
jvm17distribuída, porque não existe CMS para configurar naquela JVM. - Uma inconsistência que vale a pena sinalizar em vez de disfarçar: a página em prosa "Hardware Choices" na documentação atual do Apache Cassandra ainda repete orientação mais antiga ("heaps menores do que 12GB deveriam considerar a coleta de lixo ParNew/ConcurrentMarkSweep"), mesmo na árvore de documentação publicada junto com a 5.0. Os padrões reais distribuídos em
jvm11-server.options/jvm17-server.optionsjá avançaram para o G1GC independentemente do tamanho do heap. Trate o arquivoconf/jvm*-server.optionsdistribuído para sua versão em execução como a fonte da verdade, em vez da documentação em prosa, que pode estar atrasada uma ou duas versões. - Conclusão prática: um time rodando o Cassandra 5.0 de fábrica já está no G1GC sem ter ajustado nada, que é exatamente a propriedade de "menos decisões de ajuste" que o livro credita ao G1GC. Um time atualizando um cluster antigo ajustado para CMS para um JDK onde o CMS não existe mais precisa migrar seu
jvm.optionsdeliberadamente, não assumir que o arquivo antigo ainda se aplica.
Trade-offs
- Cache troca memória heap (ou off-heap) por I/O de disco, e os três tipos de cache não são padrões intercambiáveis. O key cache é quase de graça e quase sempre deveria ficar ligado; o row cache é uma ferramenta direcionada para um caso estreito (95%+ leituras, linhas pequenas e quentes) e um mau padrão porque "a configuração errada pode facilmente levar a mais problemas de performance do que resolve"; o chunk cache beneficia uma gama mais larga de padrões de leitura porque escritas de partição não destroem todo o seu conteúdo em cache, como fazem com o do row cache. Dimensionar qualquer um deles generosamente demais esfoma o heap da JVM do qual o resto do Cassandra depende: o próprio aviso do livro de "não permitir que o cache sobrecarregue seu heap" é toda a história de cache em uma linha.
- Throttling de compaction é um trade-off direto de latência versus recuperação, não um valor "definir uma vez". Um
compaction_throughput_mb_per_secbaixo protege a latência de leitura/escrita visível ao cliente durante operação normal, mas arrisca um backlog crescente de SSTable sob pressão sustentada de escrita: exatamente o backlog que (segundo o conceito de internos do motor de armazenamento) degrada leituras forçando-as a checar mais arquivos. Aumentar o throughput ouconcurrent_compactorslimpa o backlog mais rápido ao custo direto de I/O e CPU pelos quais clientes também estão competindo. Não existe um valor de throttle que esteja correto tanto em carga leve quanto pesada; essa é uma configuração para revisitar sob teste de estresse, não definir uma vez na criação do cluster. - O "exige menos decisões de ajuste" do G1GC é uma vantagem genuína, não uma garantia de que ele sempre vence. A própria história do livro (o G1GC era o padrão do Cassandra 3.0, depois foi retirado por performar pior do que o CMS abaixo de heaps de 8 GB) mostra que a performance do coletor depende do tamanho do heap e da carga de trabalho, não apenas da data de lançamento. O fato de o Cassandra 5.0 agora trazer o G1GC como padrão é uma forte evidência de que o coletor e a JVM maturaram desde aquela retirada da 3.0, mas não é evidência de que o G1GC vence toda configuração possível de CMS ajustado; é evidência de que o G1GC vence o CMS não ajustado para a maioria das cargas de trabalho, que é o que um padrão de fábrica deveria otimizar.
- A recomendação de heap-igual-a-Xms-igual-a-Xmx troca flexibilidade de inicialização por previsibilidade de GC. Travar o heap mínimo e máximo no mesmo valor evita o custo da JVM crescer o heap sob carga, mas também significa que um nó não pode usar temporariamente menos memória durante períodos calmos: o compromisso do heap inteiro é permanente desde o início do processo, o que importa ao colocalizar o Cassandra com outros processos famintos por memória no mesmo host.
- Armazenamento off-heap de memtable e row cache reduz pressão de GC ao custo de um orçamento de memória mais complexo.
offheap_bufferseoffheap_objectsdememtable_allocation_type, e oOHCProvideroff-heap padrão do row cache, todos movem dados para fora do espaço que o coletor de lixo precisa varrer, ajudando diretamente qualquer coletor em uso, CMS ou G1GC igualmente. A troca é que memória off-heap não aparece em monitoramento focado em heap e ainda precisa ser orçada contra a RAM total do sistema junto com o próprio heap; um nó dimensionado só pelo uso de heap ainda pode ficar sem memória.
Documentation Links
- Jeff Carpenter and Eben Hewitt, "Cassandra: The Definitive Guide", Revised 3rd Edition (O'Reilly, 2022) — Chapter 13, "Performance Tuning"
- Apache Cassandra Documentation — Hardware Choices (heap sizing and GC guidance)
- Apache Cassandra source — conf/jvm11-server.options (5.0 branch, G1GC enabled by default)
- Apache Cassandra source — conf/jvm17-server.options (5.0 branch)
- OpenJDK JEP 363 — Remove the Concurrent Mark Sweep (CMS) Garbage Collector
- Apache Cassandra Documentation — nodetool compactionstats