Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast

Neste artigo falaremos sobre como e por que desenvolvemos Sistema de Interação – um mecanismo que transfere informações entre aplicativos clientes e servidores 1C:Enterprise - desde a definição de uma tarefa até a reflexão sobre a arquitetura e os detalhes de implementação.

O Sistema de Interação (doravante denominado SV) é um sistema de mensagens distribuído, tolerante a falhas e com entrega garantida. SV foi projetado como um serviço de alta carga com alta escalabilidade, disponível tanto como um serviço online (fornecido pela 1C) quanto como um produto produzido em massa que pode ser implantado em suas próprias instalações de servidor.

SV usa armazenamento distribuído Castanha de avelã e mecanismo de busca ElasticSearch. Também falaremos sobre Java e como dimensionamos horizontalmente o PostgreSQL.
Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast

Formulação do problema

Para deixar claro porque criamos o Sistema de Interação, contarei um pouco sobre como funciona o desenvolvimento de aplicações de negócios em 1C.

Para começar, um pouco sobre nós para quem ainda não sabe o que fazemos :) Estamos fazendo a plataforma tecnológica 1C:Enterprise. A plataforma inclui uma ferramenta de desenvolvimento de aplicativos de negócios, bem como um tempo de execução que permite que os aplicativos de negócios sejam executados em um ambiente multiplataforma.

Paradigma de desenvolvimento cliente-servidor

Os aplicativos de negócios criados em 1C:Enterprise operam em três níveis servidor cliente arquitetura “SGBD – servidor de aplicação – cliente”. Código do aplicativo escrito em linguagem 1C integrada, pode ser executado no servidor de aplicação ou no cliente. Todo o trabalho com objetos da aplicação (diretórios, documentos, etc.), bem como a leitura e gravação do banco de dados, é realizado apenas no servidor. A funcionalidade de formulários e interface de comando também é implementada no servidor. O cliente realiza recebimento, abertura e exibição de formulários, “comunicação” com o usuário (avisos, dúvidas...), pequenos cálculos em formulários que exigem resposta rápida (por exemplo, multiplicação do preço pela quantidade), trabalho com arquivos locais, trabalhando com equipamentos.

No código da aplicação, os cabeçalhos dos procedimentos e funções devem indicar explicitamente onde o código será executado - utilizando as diretivas &AtClient / &AtServer (&AtClient / &AtServer na versão em inglês da linguagem). Os desenvolvedores 1C agora me corrigirão dizendo que as diretivas são na verdade больше, mas para nós isso não é importante agora.

Você pode chamar o código do servidor a partir do código do cliente, mas não pode chamar o código do cliente a partir do código do servidor. Esta é uma limitação fundamental que fizemos por uma série de razões. Em particular, porque o código do servidor deve ser escrito de forma que seja executado da mesma maneira, não importa onde seja chamado - do cliente ou do servidor. E no caso de chamar o código do servidor de outro código do servidor, não existe cliente como tal. E porque durante a execução do código do servidor, o cliente que o chamou poderia fechar, sair da aplicação e o servidor não teria mais ninguém para quem ligar.

Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast
Código que lida com um clique de botão: chamar um procedimento de servidor do cliente funcionará, chamar um procedimento de cliente do servidor não funcionará

Isso significa que se quisermos enviar alguma mensagem do servidor para a aplicação cliente, por exemplo, que a geração de um relatório de “longa execução” foi concluída e o relatório pode ser visualizado, não temos tal método. Você precisa usar truques, por exemplo, pesquisar periodicamente o servidor a partir do código do cliente. Mas esta abordagem carrega o sistema com chamadas desnecessárias e geralmente não parece muito elegante.

E há também uma necessidade, por exemplo, quando chega uma chamada telefónica SIP- ao fazer uma chamada, notifique a aplicação cliente sobre isso para que ela possa usar o número do chamador para localizá-lo no banco de dados da contraparte e mostrar ao usuário informações sobre a contraparte chamadora. Ou, por exemplo, quando um pedido chegar ao armazém, avise a aplicação cliente do cliente sobre isso. Em geral, há muitos casos em que tal mecanismo seria útil.

A produção em si

Crie um mecanismo de mensagens. Rápido, confiável, com entrega garantida, com possibilidade de busca de mensagens com flexibilidade. Com base no mecanismo, implemente um mensageiro (mensagens, videochamadas) rodando dentro de aplicativos 1C.

Projete o sistema para ser escalonável horizontalmente. A carga crescente deve ser coberta aumentando o número de nós.

Implementação

Decidimos não integrar a parte do servidor do SV diretamente na plataforma 1C:Enterprise, mas implementá-la como um produto separado, cuja API pode ser chamada a partir do código das soluções de aplicativos 1C. Isso foi feito por vários motivos, o principal deles era que eu queria possibilitar a troca de mensagens entre diferentes aplicações 1C (por exemplo, entre Gestão Comercial e Contabilidade). Diferentes aplicativos 1C podem ser executados em diferentes versões da plataforma 1C:Enterprise, estar localizados em servidores diferentes, etc. Nessas condições, a implementação do SV como um produto separado localizado “na lateral” das instalações 1C é a solução ideal.

Então decidimos fazer o SV como um produto separado. Recomendamos que as pequenas empresas utilizem o servidor CB que instalamos em nossa nuvem (wss://1cdialog.com) para evitar os custos indiretos associados à instalação e configuração local do servidor. Grandes clientes podem achar aconselhável instalar seu próprio servidor CB em suas instalações. Usamos uma abordagem semelhante em nosso produto SaaS em nuvem 1cFresco – é produzido como um produto em massa para instalação nas instalações dos clientes e também é implantado em nossa nuvem https://1cfresh.com/.

Aplicação

Para distribuir a carga e a tolerância a falhas, implantaremos não um aplicativo Java, mas vários, com um balanceador de carga na frente deles. Se você precisar transferir uma mensagem de nó para nó, use publicar/assinar no Hazelcast.

A comunicação entre o cliente e o servidor é via websocket. É adequado para sistemas em tempo real.

Cache distribuído

Escolhemos entre Redis, Hazelcast e Ehcache. É 2015. O Redis acaba de lançar um novo cluster (muito novo, assustador), existe o Sentinel com muitas restrições. O Ehcache não sabe como montar um cluster (esta funcionalidade apareceu mais tarde). Decidimos experimentar com Hazelcast 3.4.
Hazelcast é montado em um cluster pronto para uso. No modo de nó único, não é muito útil e só pode ser usado como cache - ele não sabe como despejar dados no disco, se você perder o único nó, perderá os dados. Implantamos vários Hazelcasts, entre os quais fazemos backup de dados críticos. Não fazemos backup do cache – não nos importamos.

Para nós, Hazelcast é:

  • Armazenamento de sessões de usuário. Demora muito para ir ao banco de dados para uma sessão todas as vezes, então colocamos todas as sessões no Hazelcast.
  • Cache. Se você estiver procurando um perfil de usuário, verifique o cache. Escreveu uma nova mensagem - coloque-a no cache.
  • Tópicos para comunicação entre instâncias de aplicação. O nó gera um evento e o coloca no tópico Hazelcast. Outros nós de aplicativos inscritos neste tópico recebem e processam o evento.
  • Bloqueios de cluster. Por exemplo, criamos uma discussão usando uma chave exclusiva (discussão singleton no banco de dados 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Verificamos que não há canal. Pegamos o cadeado, verificamos novamente e o criamos. Se você não verificar o bloqueio depois de obtê-lo, há uma chance de que outro tópico também tenha verificado naquele momento e agora tente criar a mesma discussão - mas ela já existe. Você não pode bloquear usando Java Lock sincronizado ou regular. Através do banco de dados - é lento e é uma pena para o banco de dados; através do Hazelcast - é disso que você precisa.

Escolhendo um SGBD

Temos uma vasta e bem-sucedida experiência trabalhando com PostgreSQL e colaborando com os desenvolvedores deste SGBD.

Não é fácil com um cluster PostgreSQL - existe XL, XC, Cítus, mas em geral estes não são NoSQLs que saem da caixa. Não consideramos o NoSQL como armazenamento principal, bastou levarmos o Hazelcast, com o qual não havíamos trabalhado antes.

Se você precisar dimensionar um banco de dados relacional, isso significa fragmentação. Como você sabe, com o sharding dividimos o banco de dados em partes separadas para que cada uma delas possa ser colocada em um servidor separado.

A primeira versão do nosso sharding pressupunha a capacidade de distribuir cada uma das tabelas da nossa aplicação em diferentes servidores em diferentes proporções. Há muitas mensagens no servidor A - por favor, vamos mover parte desta tabela para o servidor B. Essa decisão simplesmente indicava uma otimização prematura, então decidimos nos limitar a uma abordagem multilocatário.

Você pode ler sobre multilocatário, por exemplo, no site Dados do Citus.

SV possui os conceitos de aplicativo e assinante. Um aplicativo é uma instalação específica de um aplicativo comercial, como ERP ou Contabilidade, com seus usuários e dados comerciais. Um assinante é uma organização ou indivíduo em cujo nome o aplicativo está registrado no servidor SV. Um assinante pode ter vários aplicativos registrados e esses aplicativos podem trocar mensagens entre si. O assinante tornou-se inquilino em nosso sistema. Mensagens de vários assinantes podem estar localizadas em um banco de dados físico; se percebermos que um assinante começou a gerar muito tráfego, nós o movemos para um banco de dados físico separado (ou mesmo para um servidor de banco de dados separado).

Temos um banco de dados principal onde é armazenada uma tabela de roteamento com informações sobre a localização de todos os bancos de dados de assinantes.

Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast

Para evitar que o banco de dados principal seja um gargalo, mantemos a tabela de roteamento (e outros dados frequentemente necessários) em um cache.

Se o banco de dados do assinante começar a ficar lento, iremos cortá-lo em partições internas. Em outros projetos usamos pg_pathman.

Como perder mensagens de usuários é ruim, mantemos nossos bancos de dados com réplicas. A combinação de réplicas síncronas e assíncronas permite que você se proteja em caso de perda do banco de dados principal. A perda de mensagens só ocorrerá se o banco de dados primário e sua réplica síncrona falharem simultaneamente.

Se uma réplica síncrona for perdida, a réplica assíncrona se tornará síncrona.
Se o banco de dados principal for perdido, a réplica síncrona se tornará o banco de dados principal e a réplica assíncrona se tornará uma réplica síncrona.

Elasticsearch para pesquisa

Como, entre outras coisas, o SV também é um mensageiro, requer uma pesquisa rápida, cómoda e flexível, tendo em conta a morfologia, através de correspondências imprecisas. Decidimos não reinventar a roda e usar o mecanismo de busca gratuito Elasticsearch, criado com base na biblioteca lucene. Também implantamos o Elasticsearch em um cluster (mestre – dados – dados) para eliminar problemas em caso de falha dos nós da aplicação.

No github encontramos Plugin de morfologia russa para Elasticsearch e use-o. No índice Elasticsearch armazenamos raízes de palavras (que o plugin determina) e N-gramas. À medida que o usuário insere o texto para pesquisar, procuramos o texto digitado entre N-gramas. Quando salva no índice, a palavra “textos” será dividida nos seguintes N-gramas:

[aqueles, tek, tex, texto, textos, ek, ex, ext, textos, ks, kst, ksty, st, chiqueiro, você],

E a raiz da palavra “texto” também será preservada. Essa abordagem permite pesquisar no início, no meio e no final da palavra.

Quadro geral

Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast
Repetição da imagem do início do artigo, mas com explicações:

  • Balanceador exposto na Internet; temos nginx, pode ser qualquer um.
  • As instâncias de aplicativos Java se comunicam entre si por meio do Hazelcast.
  • Para trabalhar com um web socket usamos Netty.
  • O aplicativo Java é escrito em Java 8 e consiste em pacotes configuráveis OSGi. Os planos incluem migração para Java 10 e transição para módulos.

Desenvolvimento e teste

No processo de desenvolvimento e teste do SV, encontramos vários recursos interessantes dos produtos que utilizamos.

Teste de carga e vazamentos de memória

O lançamento de cada versão SV envolve testes de carga. É bem-sucedido quando:

  • O teste funcionou por vários dias e não houve falhas no serviço
  • O tempo de resposta para operações importantes não excedeu um limite confortável
  • A deterioração do desempenho em comparação com a versão anterior não é superior a 10%

Preenchemos o banco de dados de teste com dados - para isso, recebemos informações sobre o assinante mais ativo do servidor de produção, multiplicamos seus números por 5 (número de mensagens, discussões, usuários) e testamos assim.

Realizamos testes de carga do sistema de interação em três configurações:

  1. Teste de stress
  2. Somente conexões
  3. Cadastro de assinante

Durante o teste de estresse, lançamos várias centenas de threads e eles carregam o sistema sem parar: escrevendo mensagens, criando discussões, recebendo uma lista de mensagens. Simulamos as ações de usuários comuns (obter uma lista de minhas mensagens não lidas, escrever para alguém) e soluções de software (transmitir um pacote de configuração diferente, processar um alerta).

Por exemplo, esta é a aparência de parte do teste de estresse:

  • Usuário faz login
    • Solicita suas discussões não lidas
    • 50% de probabilidade de ler mensagens
    • 50% de probabilidade de enviar mensagens de texto
    • Próximo usuário:
      • Tem 20% de chance de criar uma nova discussão
      • Seleciona aleatoriamente qualquer uma de suas discussões
      • Vai para dentro
      • Solicita mensagens, perfis de usuário
      • Cria cinco mensagens endereçadas a usuários aleatórios desta discussão
      • Deixa a discussão
      • Repete 20 vezes
      • Sai, volta ao início do script

    • Um chatbot entra no sistema (emula mensagens do código do aplicativo)
      • Tem 50% de chance de criar um novo canal para troca de dados (discussão especial)
      • 50% de probabilidade de escrever uma mensagem para qualquer um dos canais existentes

O cenário “Somente conexões” apareceu por um motivo. Existe uma situação: os usuários conectaram o sistema, mas ainda não se envolveram. Cada usuário liga o computador às 09h00 da manhã, estabelece conexão com o servidor e permanece em silêncio. Esses caras são perigosos, existem muitos deles - os únicos pacotes que eles têm são PING/PONG, mas eles mantêm a conexão com o servidor (eles não conseguem mantê-la - e se houver uma nova mensagem). O teste reproduz uma situação em que um grande número desses usuários tenta fazer login no sistema em meia hora. É semelhante a um teste de estresse, mas seu foco está justamente nesse primeiro input – para que não haja falhas (a pessoa não usa o sistema, e ele já cai – fica difícil pensar em algo pior).

O script de registro do assinante começa desde o primeiro lançamento. Realizamos um teste de estresse e tivemos certeza de que o sistema não ficou lento durante a correspondência. Mas os usuários chegaram e o registro começou a falhar devido ao tempo limite. Ao registrar usamos / dev / random, que está relacionado à entropia do sistema. O servidor não teve tempo de acumular entropia suficiente e quando um novo SecureRandom foi solicitado, ele congelou por dezenas de segundos. Existem muitas maneiras de sair dessa situação, por exemplo: mudar para o menos seguro /dev/urandom, instalar uma placa especial que gera entropia, gerar números aleatórios antecipadamente e armazená-los em um pool. Encerramos temporariamente o problema com o pool, mas desde então estamos realizando um teste separado para cadastro de novos assinantes.

Usamos como gerador de carga JMeter. Ele não sabe trabalhar com websocket; precisa de um plugin. Os primeiros resultados da pesquisa para a consulta “jmeter websocket” são: artigos do BlazeMeter, que recomenda plugin por Maciej Zaleski.

Foi aí que decidimos começar.

Quase imediatamente após iniciarmos testes sérios, descobrimos que o JMeter começou a vazar memória.

O plugin é uma grande história separada; com 176 estrelas, tem 132 forks no github. O próprio autor não se compromete com isso desde 2015 (nós pegamos em 2015, então não levantou suspeitas), vários problemas no github relacionados a vazamentos de memória, 7 pull requests não fechados.
Se você decidir realizar testes de carga usando este plugin, preste atenção às seguintes discussões:

  1. Em um ambiente multithread, um LinkedList regular foi usado e o resultado foi NPE em tempo de execução. Isso pode ser resolvido mudando para ConcurrentLinkedDeque ou por blocos sincronizados. Nós escolhemos a primeira opção para nós mesmos (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Vazamento de memória; ao desconectar, as informações de conexão não são excluídas (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. No modo streaming (quando o websocket não é fechado no final da amostra, mas é usado posteriormente no plano), os padrões de resposta não funcionam (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Este é um daqueles no github. O que fizemos:

  1. Peguei garfo Elyran Kogan (@elyrank) – corrige os problemas 1 e 3
  2. Problema resolvido 2
  3. Cais atualizado de 9.2.14 para 9.3.12
  4. SimpleDateFormat empacotado em ThreadLocal; SimpleDateFormat não é thread-safe, o que levou ao NPE em tempo de execução
  5. Corrigido outro vazamento de memória (a conexão foi fechada incorretamente ao ser desconectada)

E ainda assim flui!

A memória começou a esgotar-se não em um dia, mas em dois. Não havia mais tempo, então decidimos lançar menos tópicos, mas em quatro agentes. Isso deveria ter sido suficiente por pelo menos uma semana.

Dois dias se passaram...

Agora o Hazelcast está ficando sem memória. Os logs mostraram que, após alguns dias de testes, o Hazelcast começou a reclamar de falta de memória e, depois de algum tempo, o cluster se desfez e os nós continuaram a morrer um por um. Conectamos o JVisualVM ao hazelcast e vimos uma “serra ascendente” - ela chamava regularmente o GC, mas não conseguia limpar a memória.

Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast

Descobriu-se que no hazelcast 3.4, ao excluir um mapa/multiMap (map.destroy()), a memória não é completamente liberada:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

O bug agora foi corrigido no 3.5, mas era um problema naquela época. Criamos novos multiMaps com nomes dinâmicos e os excluímos de acordo com nossa lógica. O código era mais ou menos assim:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Chamar:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap foi criado para cada assinatura e excluído quando não era necessário. Decidimos que iríamos começar o Map , a chave será o nome da assinatura e os valores serão identificadores de sessão (dos quais você poderá obter identificadores de usuário, se necessário).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Os gráficos melhoraram.

Como e por que escrevemos um serviço escalonável de alta carga para 1C:Enterprise: Java, PostgreSQL, Hazelcast

O que mais aprendemos sobre testes de carga?

  1. JSR223 precisa ser escrito em linguagem bacana e incluir cache de compilação - é muito mais rápido. Link.
  2. Os gráficos Jmeter-Plugins são mais fáceis de entender do que os gráficos padrão. Link.

Sobre nossa experiência com Hazelcast

Hazelcast era um produto novo para nós, começamos a trabalhar com ele a partir da versão 3.4.1, agora nosso servidor de produção está rodando a versão 3.9.2 (no momento em que este artigo foi escrito, a versão mais recente do Hazelcast é 3.10).

Geração de ID

Começamos com identificadores inteiros. Vamos imaginar que precisamos de outro Long para uma nova entidade. A sequência no banco de dados não é adequada, as tabelas estão envolvidas no sharding - acontece que existe uma mensagem ID=1 no DB1 e uma mensagem ID=1 no DB2, você não pode colocar esse ID no Elasticsearch, nem no Hazelcast , mas o pior é se você quiser combinar os dados de dois bancos de dados em um (por exemplo, decidir que um banco de dados é suficiente para esses assinantes). Você pode adicionar vários AtomicLongs ao Hazelcast e manter o contador lá, então o desempenho de obtenção de um novo ID é incrementAndGet mais o tempo para uma solicitação ao Hazelcast. Mas o Hazelcast tem algo mais ideal - FlakeIdGenerator. Ao entrar em contato com cada cliente, eles recebem uma faixa de ID, por exemplo, o primeiro – de 1 a 10, o segundo – de 000 a 10 e assim por diante. Agora o cliente pode emitir novos identificadores por conta própria até que o intervalo emitido para ele termine. Funciona rapidamente, mas quando você reinicia o aplicativo (e o cliente Hazelcast), uma nova sequência começa - daí os pulos, etc. Além disso, os desenvolvedores não entendem realmente por que os IDs são inteiros, mas são tão inconsistentes. Pesamos tudo e mudamos para UUIDs.

A propósito, para quem quer ser como o Twitter, existe uma biblioteca Snowcast - esta é uma implementação do Snowflake em cima do Hazelcast. Você pode ver aqui:

github.com/noctarius/snowcast
github.com/twitter/floco de neve

Mas não chegamos mais a isso.

TransactionalMap.replace

Outra surpresa: TransactionalMap.replace não funciona. Aqui está um teste:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Tive que escrever minha própria substituição usando getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Teste não apenas estruturas de dados regulares, mas também suas versões transacionais. Acontece que o IMap funciona, mas o TransactionalMap não existe mais.

Insira um novo JAR sem tempo de inatividade

Primeiramente, decidimos gravar objetos de nossas aulas no Hazelcast. Por exemplo, temos uma classe Application, queremos salvá-la e lê-la. Salvar:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Leia:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Tudo está funcionando. Então decidimos construir um índice no Hazelcast para pesquisar por:

map.addIndex("subscriberId", false);

E ao escrever uma nova entidade, eles começaram a receber ClassNotFoundException. Hazelcast tentou adicionar ao índice, mas não sabia nada sobre nossa classe e queria que um JAR com esta classe fosse fornecido a ela. Fizemos exatamente isso, tudo funcionou, mas apareceu um novo problema: como atualizar o JAR sem parar completamente o cluster? O Hazelcast não coleta o novo JAR durante uma atualização nó por nó. Neste ponto decidimos que poderíamos viver sem pesquisa de índice. Afinal, se você usar o Hazelcast como armazenamento de valores-chave, tudo funcionará? Na verdade. Aqui, novamente, o comportamento do IMap e do TransactionalMap é diferente. Onde o IMap não se importa, o TransactionalMap gera um erro.

IMapa. Escrevemos 5000 objetos e os lemos. Tudo é esperado.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Mas não funciona em uma transação, obtemos uma ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

Na versão 3.8, o mecanismo User Class Deployment apareceu. É possível designar um nó mestre e atualizar o arquivo JAR nele.

Agora mudamos completamente nossa abordagem: nós mesmos serializamos em JSON e salvamos em Hazelcast. O Hazelcast não precisa conhecer a estrutura de nossas aulas e podemos atualizar sem tempo de inatividade. O versionamento de objetos de domínio é controlado pelo aplicativo. Diferentes versões do aplicativo podem estar em execução ao mesmo tempo, e é possível uma situação em que o novo aplicativo grava objetos com novos campos, mas o antigo ainda não conhece esses campos. E, ao mesmo tempo, o novo aplicativo lê objetos escritos pelo aplicativo antigo que não possuem novos campos. Lidamos com tais situações dentro da aplicação, mas para simplificar não alteramos ou excluímos campos, apenas expandimos as classes adicionando novos campos.

Como garantimos alto desempenho

Quatro viagens ao Hazelcast – boas, duas ao banco de dados – ruins

Ir para o cache para obter dados é sempre melhor do que ir para o banco de dados, mas você também não deseja armazenar registros não utilizados. Deixamos a decisão sobre o que armazenar em cache para o último estágio de desenvolvimento. Quando a nova funcionalidade é codificada, ativamos o registro de todas as consultas no PostgreSQL (log_min_duration_statement para 0) e executamos testes de carga por 20 minutos. Usando os logs coletados, utilitários como pgFouine e pgBadger podem criar relatórios analíticos. Nos relatórios, procuramos principalmente consultas lentas e frequentes. Para consultas lentas, construímos um plano de execução (EXPLAIN) e avaliamos se tal consulta pode ser acelerada. Solicitações frequentes para os mesmos dados de entrada cabem bem no cache. Tentamos manter as consultas “planas”, uma tabela por consulta.

exploração

O SV como serviço online foi colocado em operação na primavera de 2017 e, como produto separado, o SV foi lançado em novembro de 2017 (na época em status de versão beta).

Em mais de um ano de funcionamento, não houve problemas graves no funcionamento do serviço online CB. Acompanhamos o atendimento online via Zabbix, coletar e implantar de Bambu.

A distribuição do servidor SV é fornecida na forma de pacotes nativos: RPM, DEB, MSI. Além disso, para Windows, fornecemos um único instalador na forma de um único EXE que instala o servidor, Hazelcast e Elasticsearch em uma máquina. Inicialmente nos referimos a esta versão da instalação como versão “demo”, mas agora ficou claro que esta é a opção de implantação mais popular.

Fonte: habr.com

Adicionar um comentário