O SLA do Dewu Zookeeper também pode ser de 99,99% |

1. Fundo

ZooKeeper (ZK) é um serviço distribuído de coordenação de aplicativos nascido em 2007. Embora por algumas razões históricas especiais, muitos cenários de negócios ainda dependam dele. Por exemplo, Kafka, agendamento de tarefas, etc. Especialmente quando a implantação mista do Flink e o desacoplamento do ETCD, o lado comercial exigia estabilidade absoluta e recomendava fortemente não usar o ZooKeeper autoconstruído. Por questões de estabilidade, é usado o MSE-ZK do Alibaba. Desde que começamos a usá-lo em setembro de 2022, não encontramos nenhum problema de estabilidade e a confiabilidade do SLA atingiu de fato 99,99%.

Em 2023, algumas empresas usaram clusters ZooKeeper (ZK) autoconstruídos e, em seguida, o ZK experimentou várias flutuações durante o uso. Então, Dewu SRE começou a assumir o controle de alguns clusters autoconstruídos e fez várias rodadas de tentativas de reforço de estabilidade. Durante a aquisição, descobrimos que após um período de execução do ZooKeeper, o uso de memória continuará a aumentar, o que pode facilmente levar a problemas de falta de memória (OOM). Estávamos muito curiosos sobre este fenômeno e por isso participamos do processo de exploração para resolver este problema.

2. Exploração e análise

determinar a direção

Ao solucionar o problema, tivemos muita sorte de encontrar um local de falha em um ambiente de teste. Dois nós no cluster estavam em um estado extremo de OOM.

 

Com a cena da falha, normalmente resta apenas 50% antes do ponto final bem-sucedido.

A memória está alta. De acordo com a experiência anterior, ela não é heap ou há um problema no heap. Pode ser confirmado pelo gráfico em degradê e pelo jstat que é um problema no heap.

Conforme mostrado na figura: Isso significa que um determinado recurso no heap da JVM ocupa uma grande quantidade de memória e o FGC não pode liberá-lo.

Análise de memória

Para explorar a distribuição do uso de memória no heap da JVM, fizemos imediatamente um dump de heap da JVM. A análise descobriu que a memória JVM está fortemente ocupada por childWatches e dataWatches.

dataWatches: rastreie alterações nos dados do nó znode.

childWatches: rastreie alterações na estrutura do nó znode (árvore).

childWatches e dataWatches têm a mesma origem que WatcherManager.

Após investigação dos dados, descobrimos que o WatcherManager é o principal responsável pelo gerenciamento dos Watchers. O cliente ZooKeeper (ZK) primeiro registra os Inspetores no servidor ZooKeeper e, em seguida, o servidor ZooKeeper usa o WatcherManager para gerenciar todos os Inspetores. Quando os dados de um Znode mudam, o WatchManager acionará o Watcher correspondente e se comunicará com o soquete do cliente ZooKeeper inscrito no Znode. Posteriormente, o gerenciador Watch do cliente acionará o retorno de chamada do Watcher relevante para executar a lógica de processamento correspondente, completando assim todo o processo de publicação/assinatura de dados.

Uma análise mais aprofundada do WatchManager mostra que a proporção de memória das variáveis-membro Watch2Path e WatchTables é tão alta quanto (18,88+9,47)/31,82 = 90%.

WatchTables e Watch2Path armazenam o relacionamento de mapeamento exato entre ZNode e Watcher, conforme mostrado no diagrama da estrutura de armazenamento:

WatchTables [tabela de pesquisa direta]

HashMap<ZNode, HashSet<Watcher>>

Cenário: Quando um ZNode muda, o Watcher inscrito no ZNode receberá uma notificação.

Lógica: Use este ZNode para encontrar todas as listas de Watchers correspondentes através de WatchTables e, em seguida, enviar notificações uma por uma.

Watch2Paths [tabela de pesquisa reversa]

HashMap<Observador, HashSet>

Cenário: Contando quais ZNodes um determinado Watcher assinou

Lógica: Use este Watcher para encontrar todas as listas ZNode correspondentes através de Watch2Paths

Watcher é essencialmente NIOServerCnxn, que pode ser entendido como uma sessão de conexão.

Se houver um grande número de ZNodes e Watchers, e o cliente assinar um grande número de ZNodes, ele poderá até ser totalmente inscrito. O relacionamento registrado nessas duas tabelas Hash crescerá exponencialmente e eventualmente atingirá um valor enorme!

Quando totalmente inscrito, conforme mostrado na figura:

Quando o número de ZNodes: 3, o número de Watchers: 2,    WatchTables e Watch2Paths terão 6 relacionamentos cada.

Quando o número de ZNodes: 4, o número de Watchers: 3,    WatchTables e Watch2Paths terão 12 relacionamentos cada.

Através do monitoramento, encontramos um ZK-Node anormal. O número de ZNodes é de cerca de 20W e o número de Watchers é de 5.000. O número de relacionamentos entre Watcher e ZNode atingiu 100 milhões.

Se for necessário um HashMap&Node (32Byte) para armazenar cada relacionamento, já que existem duas tabelas de relacionamento, dobre-o. Então não calcule mais nada. Somente este "shell" requer 2*10000^2*32/1024^3 = 5,9GB de sobrecarga de memória inválida.

Neste ponto da análise, todos deveriam entender. Por que nossa memória ZK está sempre andando na "corda bamba" e muitas vezes OOMs.

descoberta inesperada

Agora que identificamos a causa do problema, devemos pensar em como corrigi-lo.

Pela análise acima, podemos saber que precisamos evitar que o cliente assine integralmente todos os ZNodes. No entanto, a realidade é que muitos códigos de negócios possuem essa lógica para percorrer todos os ZNodes começando no nó raiz do ZTree e assiná-los totalmente.

Podemos conseguir convencer algumas partes comerciais a fazer melhorias, mas não podemos impor os padrões de uso de todas as partes comerciais. Portanto, a nossa abordagem para resolver este problema reside na monitorização e prevenção. No entanto, infelizmente, o próprio ZK não suporta tal função, o que requer modificação do código-fonte do ZK.

Através do rastreamento e análise do código-fonte, descobrimos que a origem do problema apontava para o WatchManager e estudamos cuidadosamente os detalhes lógicos desta classe. Após um entendimento aprofundado, descobrimos que a qualidade desse código parecia ter sido escrita por um recém-formado e havia muito uso inadequado de threads e bloqueios. Observando os registros do Git, descobrimos que esse problema remonta a 2007. Porém, o que é interessante é que nesse período apareceu o WatchManagerOptimized (2018). Ao pesquisar as informações da comunidade ZK, encontramos [ZOOKEEPER-1177], ou seja, em 2011, a comunidade ZK já havia percebido que um grande número de Watch. causou problemas de uso de memória e finalmente forneceu uma solução em 2018. É justamente por causa deste WatchManagerOptimized  que parece que a comunidade ZK já o otimizou.

Curiosamente, o ZK não habilita esta classe por padrão, mesmo na versão 3.9.X mais recente, o WatchManager ainda é usado por padrão. Talvez porque ZK seja tão antigo, as pessoas gradualmente prestam menos atenção a ele. Ao perguntar aos colegas do Alibaba, confirmamos que o MSE-ZK também habilitou o WatchManagerOptimized, o que confirmou ainda mais que nosso foco estava na direção certa. Portanto, achamos necessário aprofundar o potencial desta classe.

Otimize a exploração

Otimização de bloqueio

Na versão padrão, o HashSet usado não é seguro para threads. Nesta versão, métodos de operação relacionados como addWatch, removeWatcher e triggerWatch são todos implementados adicionando bloqueios pesados ​​sincronizados aos métodos. Na versão otimizada, usamos uma combinação de ConcurrentHashMap e ReadWriteLock para usar o mecanismo de bloqueio de forma mais refinada. Desta forma, operações mais eficientes podem ser alcançadas durante o processo de adição do Watch e acionamento do Watch.

Otimização de armazenamento

Este é o nosso foco. A partir da análise do WatchManager, podemos ver que a eficiência de armazenamento do uso de WatchTables e Watch2Paths não é alta. Se o ZNode tiver muitos relacionamentos de assinatura, uma grande quantidade de memória inválida adicional será consumida.

Para nossa surpresa, WatchManagerOptimized usa "tecnologia preta" -> bitmap aqui.

O armazenamento relacional é fortemente compactado usando bitmaps para obter otimização de redução de dimensionalidade.

Principais recursos do Java BitSet:

  • Eficiente em termos de espaço: BitSet usa matrizes de bits para armazenar dados, exigindo menos espaço do que matrizes booleanas padrão.

  • Processamento rápido: a execução de operações bit a bit (como AND, OR, XOR, inversão) costuma ser mais rápida do que as operações lógicas booleanas correspondentes.

  • Expansão dinâmica: O tamanho de um BitSet pode crescer dinamicamente conforme necessário para acomodar mais bits.

BitSet usa palavras long[] para armazenar dados. O tipo long ocupa  8 bytes e tem 64 bits . Cada elemento da matriz pode armazenar  64  dados. A ordem de armazenamento dos dados na matriz é da esquerda para a direita, do menor para o maior .

Por exemplo, o BitSet na figura abaixo tem uma capacidade de palavras de 4. Palavras[0] de baixo para cima indicam se os dados 0~63 existem, palavras[1] de baixo para alto indicam se os dados 64~127 existem, e assim sobre. Entre eles, palavras[1] = 8, e o bit binário correspondente 8 é 1, indicando que há dados {67} armazenados no BitSet neste momento.

WatchManagerOptimized usa BitMap para armazenar todos os Watchers. Desta forma, mesmo que exista um 1W Watcher. O consumo de memória do bitmap é de apenas 8Byte*1W/64/1024= 1,2KB . Se você mudar para HashSet, precisará de pelo menos 32Byte*10000/1024=305KB e a eficiência de armazenamento será quase 300 vezes diferente.

WatchManager.java:private final Map<String, Set<Watcher>> watchTable = new HashMap<>();private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>();
WatchManagerOptimized.java:private final ConcurrentHashMap<String, BitHashSet> pathWatches = new ConcurrentHashMap<String, BitHashSet>();private final BitMap<Watcher> watcherBitIdMap = new BitMap<Watcher>();

O armazenamento de mapeamento do ZNode para o Watcher é alterado de Map<string, set> para ConcurrentHashMap<string,  BitHashSet >. Ou seja, o conjunto não é mais armazenado, mas o bitmap é usado para armazenar o valor do índice do bitmap.

Usamos 1W ZNode, 1W Watcher e, no extremo extremo, usamos assinatura completa (todos os Watchers assinam todos os ZNodes) para fazer PK de eficiência de armazenamento:

Você pode ver que  11,7 MB PK 5,9 GB , a diferença de eficiência de armazenamento de memória é: 516 vezes .

Otimização lógica

  • Adicionando um monitor: ambas as versões são capazes de concluir operações em tempo constante, mas a versão otimizada oferece melhor desempenho de simultaneidade usando ConcurrentHashMap .

  • Excluindo um monitor: A versão padrão pode precisar percorrer toda a coleção de monitores para localizar e excluir o monitor, resultando em uma complexidade de tempo de O(n). A versão otimizada usa BitSet e ConcurrentHashMap para localizar e excluir rapidamente monitores em O(1) na maioria dos casos.

  • Acionamento de monitores: a versão padrão é mais complexa porque requer operações em todos os monitores e em todos os caminhos. A versão otimizada otimiza o desempenho dos monitores de disparo por meio de estruturas de dados mais eficientes e redução do uso de bloqueios.

3. Teste de estresse de desempenho

Microbenchmark JMH

Compilação do código-fonte do Zookeeper 3.6.4, teste de estresse JMH micor WatchBench.

pathCount: Indica o número de caminhos ZNode usados ​​no teste.

watchManagerClass: Representa a classe de implementação WatchManager usada no teste.

watcherCount: Indica a quantidade de observadores (Watchers) utilizados no teste.

Modo: Indica o modo de teste, aqui está avgt, que indica o tempo médio de execução.

Cnt: Indica o número de execuções de teste.

Pontuação: Indica a pontuação da prova, ou seja, o tempo médio de execução.

Erro: Indica a faixa de erro da pontuação.

Unidades: A unidade que representa a pontuação, aqui é milissegundos/operação (ms/op).

  • Existem 1 milhão de assinaturas entre ZNode e Watcher. A versão padrão usa 50 MB e a versão otimizada requer apenas 0,2 MB e não aumentará linearmente.

  • Adicionando Watch, a versão otimizada (0,406 ms/op) é 6,5 vezes mais rápida que a versão padrão (2,669 ms/op).

  • Um grande número de relógios é acionado, e a versão otimizada (17,833 ms/op) é 5 vezes mais rápida que a versão padrão (84,455 ms/op).

Teste de estresse de desempenho

Em seguida, construímos um conjunto de zookeeper 3.6.4 de 3 nós em uma máquina (32C 60G), usando a versão otimizada e a versão padrão para realizar comparação de testes de estresse de capacidade.

Cenário 1: caminho curto do znode de 20 W

Caminho curto do Znode: /demo/znode1 

Cenário 2: caminho longo do nó znode de 20 W

Caminho longo do Znode: /sentinel-cluster/dev/xx-admin-interfaces/lock/_c_bb0832d5-67a5-48ab-8fe0-040b9ddea-lock/12

  • O uso da memória do relógio está relacionado ao comprimento do caminho do ZNode.

  • O número de relógios aumenta linearmente na versão padrão e funciona muito bem na versão otimizada, o que é uma melhoria muito óbvia para otimização do uso de memória.

Teste em escala de cinza

Com base no teste de benchmark e teste de capacidade anteriores, a versão otimizada tem otimização de memória óbvia em um grande número de cenários de observação. Em seguida, começamos a realizar observações de teste de atualização em escala de cinza no cluster ZK no ambiente de teste.

O primeiro cluster de tratadores de zoológico e benefícios

Versão padrão

Versão otimizada

Renda de efeito:

  • lection_time (horário da eleição): reduzido em 60%

  • fsync_time (tempo de sincronização da transação): reduzido em 75%

  • Uso de memória: reduzido em 91%

O segundo grupo de tratadores de zoológico e benefícios

 

 

Renda de efeito:

  • Memória: Antes da mudança, a resposta JVM Attach falhou em responder e a coleta de dados falhou.

  • lection_time (horário eleitoral): reduzido em 64%.

  • max_latency (latência de leitura): reduzido em 53%.

  • proposta_latency (atraso da proposta no processamento eleitoral): 1400000 ms --> 43 ms.

  • propagation_latency (atraso de propagação de dados): 1400000 ms --> 43 ms.

O terceiro conjunto de cluster e benefícios de tratadores de zoológico

Versão padrão

Versão otimizada

​​

Renda de efeito:

  • Memória: Economize 89%

  • lection_time (horário da eleição): reduzido em 42%

  • max_latency (latência de leitura): reduzido em 95%

  • proposta_latency (atraso da proposta no processamento eleitoral): 679999 ms --> 0,3 ms

  • propagation_latency (atraso de propagação de dados): 928.000 ms -> 5 ms

4. Resumo

Por meio de testes de benchmark anteriores, testes de estresse de desempenho e testes em escala de cinza, descobrimos o WatchManagerOptimized do Zookeeper. Essa otimização não apenas economiza memória, mas também melhora significativamente indicadores como eleição e sincronização de dados entre nós por meio da otimização de bloqueio, melhorando assim a consistência do Zookeeper. Também tivemos discussões aprofundadas com alunos do Alibaba MSE, cada um simulando testes de estresse em cenários extremos, e chegamos a um consenso: WatchManagerOptimized melhora significativamente a estabilidade do Zookeeper. No geral, essa otimização melhora o SLA do Zookeeper em uma ordem de grandeza.

O ZooKeeper possui muitas opções de configuração, mas na maioria dos casos nenhum ajuste é necessário. Para melhorar a estabilidade do sistema, recomendamos as seguintes otimizações de configuração:

  • Monte dataDir (diretório de dados) e dataLogDir (diretório de log de transações) em discos diferentes, respectivamente, e use armazenamento em bloco de alto desempenho.

  • Para o ZooKeeper versão 3.8, é recomendado usar o JDK 17 e habilitar o coletor de lixo ZGC; para as versões 3.5 e 3.6, é recomendado usar o JDK 8 e habilitar o coletor de lixo G1; Para essas versões, basta configurar -Xms e -Xmx.

  • Ajuste o valor padrão do parâmetro SnapshotCount de 100.000 a 500.000, o que pode reduzir significativamente a pressão do disco quando o ZNode muda em altas frequências.

  • Use a versão otimizada do Watch Manager WatchManagerOptimized.

 

Referência:

https://issues.apache.org/jira/browse/ZOOKEEPER-1177

https://github.com/apache/zookeeper/pull/590

 

 *Texto/ Bruce

Este artigo é original da Dewu Technology. Para artigos mais interessantes, consulte: Site oficial da Dewu Technology .

A reimpressão sem a permissão da Dewu Technology é estritamente proibida, caso contrário, a responsabilidade legal será processada de acordo com a lei!

A primeira grande atualização de versão do JetBrains 2024 (2024.1) é de código aberto. Até a Microsoft planeja pagar por isso. Por que ainda está sendo criticado por ser de código aberto? [Recuperado] O back-end do Tencent Cloud travou: um grande número de erros de serviço e nenhum dado após o login no console. A Alemanha também precisa ser "controlável de forma independente". O governo estadual migrou 30.000 PCs do Windows para o Linux deepin-IDE e finalmente conseguiu inicialização! O Visual Studio Code 1.88 foi lançado. Bom rapaz, a Tencent realmente transformou o Switch em uma "máquina de aprendizagem pensante". A área de trabalho remota RustDesk inicia e reconstrói o cliente Web. O banco de dados de terminal de código aberto do WeChat baseado em SQLite, WCDB, recebeu uma grande atualização.
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/u/5783135/blog/11051902
Recomendado
Clasificación