Serviço de cache inteligente baseado em ZeroMQ e Tarantool

Ruslan Aromatov, desenvolvedor chefe, CID



Olá Habr! Trabalho como desenvolvedor back-end no Moscow Credit Bank e, durante o meu trabalho, adquiri alguma experiência que gostaria de compartilhar com a comunidade. Hoje vou contar como criamos nosso próprio serviço de cache para os servidores frontais de nossos clientes usando o aplicativo móvel MKB Online. Este artigo pode ser útil para aqueles que estão envolvidos no design de serviços e estão familiarizados com a arquitetura de microsserviços, o banco de dados Tarantool na memória e a biblioteca ZeroMQ. No artigo, praticamente não haverá exemplos de código e explicação do básico, mas apenas uma descrição da lógica dos serviços e sua interação com um exemplo específico que vem trabalhando em nossa batalha há mais de dois anos.

Como tudo começou


Cerca de 6 anos atrás, o esquema era simples. Como um legado da empresa de terceirização, temos dois clientes de banco móvel para iOS e Android, além de um servidor de frente para atendê-los. O servidor em si foi escrito em java, foi para o back-end de diferentes maneiras (principalmente soap) e se comunicou com os clientes transmitindo xml via https.

Os aplicativos clientes foram capazes de autenticar de alguma forma, mostrar uma lista de produtos e ... eles pareciam capazes de fazer algumas transferências e pagamentos, mas na verdade eles não o faziam muito bem e nem sempre. Portanto, o servidor frontal não teve um grande número de usuários ou cargas sérias (o que, no entanto, não impediu que ele caísse uma vez a cada dois dias).

É claro que nós (e na época nossa equipe era composta por quatro pessoas), como responsáveis ​​pelo banco móvel, não se encaixavam nessa situação e, para começar, colocamos em ordem os aplicativos atuais, mas o servidor frontal acabou sendo muito ruim, por isso tinha que ser reescreva rapidamente o todo, substituindo simultaneamente xml por json e movendo-se para o servidor de aplicativos WildFly . Por alguns anos, a refatoração não se baseia em um post separado, pois tudo foi feito principalmente para garantir que o sistema funcionasse de maneira estável.

Gradualmente, os aplicativos e o servidor desenvolvidos começaram a trabalhar mais estáveis, e suas funcionalidades estavam em constante expansão, o que valeu a pena - havia cada vez mais usuários.

Ao mesmo tempo, começaram a surgir questões como tolerância a falhas, redundância, replicação e - assustador pensar - a carga alta.

Uma solução rápida para o problema foi adicionar um segundo servidor WildFly, e os aplicativos aprenderam a alternar entre eles. O problema do trabalho simultâneo com sessões do cliente foi resolvido pelo módulo Infinispan integrado ao WildFly.

Como era antes

Parecia que a vida estava melhorando ...

Você não pode viver assim


No entanto, essa opção de trabalhar com sessões não foi isenta de desvantagens. Vou mencionar aqueles que não nos agradam.

  1. Perda de sessões. O menos importante. Por exemplo, um aplicativo envia duas solicitações para o servidor-1: a primeira solicitação é autenticação e a segunda é uma solicitação para uma lista de contas. A autenticação é bem-sucedida, uma sessão é criada no servidor-1. No momento, a segunda solicitação do cliente é interrompida repentinamente devido à falta de comunicação e o aplicativo alterna para o servidor-2, reenviando o encaminhamento da segunda solicitação. Mas, em uma determinada carga de trabalho, o Infinispan pode não ter tempo para sincronizar dados entre os nós. Como resultado, o servidor-2 não pode verificar a sessão do cliente, envia uma resposta irritada ao cliente, o cliente está triste e termina sua sessão. O usuário precisa fazer login novamente. Triste
  2. Reiniciar o servidor também pode causar perda de sessões. Por exemplo, após uma atualização (e isso acontece com bastante frequência). Quando o servidor-2 é iniciado, ele não funcionará até que os dados sejam sincronizados com o servidor-1. Parece que o servidor foi iniciado, mas na verdade não deve aceitar solicitações. Isso é inconveniente.
  3. Este é um módulo incorporado do WildFly que nos impede de sair deste servidor de aplicativos em direção a microsserviços.

A partir daqui, uma lista do que gostaríamos era de alguma forma formada por ela mesma.

  1. Queremos armazenar sessões do cliente para que qualquer servidor (não importa quantos existam) imediatamente após o lançamento tenha acesso a eles.
  2. Queremos armazenar quaisquer dados do cliente entre solicitações (por exemplo, parâmetros de pagamento e tudo mais).
  3. Queremos salvar todos os dados arbitrários em uma chave arbitrária em geral.
  4. E também queremos receber dados do cliente antes da autenticação passar. Por exemplo, o usuário é autenticado e todos os seus produtos estão ali, frescos e quentes.
  5. E queremos escalar de acordo com a carga.
  6. E execute na janela de encaixe, escreva logs em uma única pilha e conte métricas, etc.
  7. Ah, sim, e para que tudo funcione rapidamente.

Farinha de escolha


Anteriormente, não implementávamos a arquitetura de microsserviços, então, para começar, nos sentamos para ler, assistir e experimentar opções diferentes. Ficou claro imediatamente que precisávamos de um repositório rápido e algum tipo de complemento que lidasse com a lógica comercial e seja a interface de acesso ao repositório. Além disso, seria bom agilizar o transporte rápido entre os serviços.

Eles escolheram por um longo tempo, discutiram muito e experimentaram. Agora não descreverei os prós e os contras de todos os candidatos, isso não se aplica ao tópico deste artigo, apenas digo que o armazenamento será tarantool , escreveremos nosso serviço em java e o ZeroMQ funcionará como transporte. Nem vou argumentar que a escolha é muito ambígua, mas foi amplamente influenciada pelo fato de não gostarmos de estruturas grandes e pesadas (por seu peso e lentidão), soluções in a box (por sua versatilidade e falta de personalização), mas ao mesmo tempo Gostamos de controlar todas as partes do nosso sistema, tanto quanto possível. E para controlar o trabalho dos serviços, escolhemos o servidor de coleta de métricas do Prometheus com seus agentes convenientes que podem ser incorporados a praticamente qualquer código. Os logs de tudo isso vão para a pilha ELK.

Bem, parece-me que já havia muita teoria.

Iniciar e terminar


O resultado do projeto foi aproximadamente esse esquema.

Como queremos

Armazenamento

Deve ser o mais estúpido possível, apenas para armazenar dados e seus estados atuais, mas sempre funciona sem reiniciar. Projetado para atender diferentes versões de servidores frontais. Mantemos todos os dados na memória, recuperação em caso de reinicialização através de arquivos .snap e .xlog.

Tabela (espaço) para sessões do cliente:

  • ID da sessão
  • ID do cliente;
  • versão (serviço)
  • hora da atualização (timestamp);
  • tempo de vida (ttl);
  • dados de sessão serializados.

Tudo é simples aqui: o cliente é autenticado, o servidor frontal cria uma sessão e a salva no armazenamento, lembrando a hora. A cada solicitação de dados, o tempo é atualizado, para que a sessão seja mantida ativa. Se, mediante solicitação, os dados estiverem desatualizados (ou não haverá nenhum), retornaremos um código de retorno especial, após o qual o cliente encerrará sua sessão.

Tabela de cache simples (para qualquer dado da sessão):

  • chave;
  • ID da sessão
  • tipo de dados armazenados (número arbitrário);
  • hora da atualização (timestamp);
  • tempo de vida (ttl);
  • dados serializados.

Tabela de dados do cliente que precisam ser aquecidos antes do login:
  • ID do cliente;
  • ID da sessão
  • versão (serviço)
  • tipo de dados armazenados (número arbitrário);
  • hora da atualização (timestamp);
  • condição;
  • dados serializados.

Um campo importante aqui é condição. Na verdade, existem apenas dois deles - ocioso e atualizado. Eles são definidos por um serviço sobreposto que vai ao back-end para dados do cliente, para que outra instância desse serviço não faça o mesmo trabalho (já inútil) e não carregue o back-end.

Tabela de dispositivos:

  • ID do cliente;
  • ID do dispositivo
  • hora da atualização (timestamp);

A tabela de dispositivos é necessária para que, mesmo antes de o cliente se autenticar no sistema, descubra seu ID e comece a receber seus produtos (aquecendo o cache). A lógica é a seguinte: a primeira entrada é sempre fria, pois antes da autenticação não sabemos que tipo de cliente é proveniente de um dispositivo desconhecido (os clientes móveis sempre transmitem IDs de dispositivo em qualquer solicitação). Todas as entradas subseqüentes deste dispositivo serão acompanhadas de um cache de aquecimento para o cliente associado a ele.

O trabalho com dados é isolado do serviço java pelos procedimentos do servidor. Sim, tive que aprender lua, mas não demorou muito tempo. Além do próprio gerenciamento de dados, os procedimentos lua também são responsáveis ​​pelo retorno de estados atuais, seleções de índices, limpeza de registros obsoletos em processos em segundo plano (fibras) e a operação do servidor da web embutido pelo qual o acesso direto aos dados é realizado. Aqui está - a beleza de escrever tudo com as mãos - a possibilidade de controle ilimitado. Mas o menos é o mesmo - você precisa escrever tudo sozinho.

O próprio Tarantool trabalha em um contêiner de encaixe, todos os arquivos lua necessários são colocados lá no estágio de montagem da imagem. Toda a montagem através de scripts gradle.

Replicação mestre-escravo. No outro host, o mesmo contêiner é executado exatamente como a réplica do armazenamento principal. É necessário no caso de uma falha de emergência do mestre - os serviços java passam para o escravo e se tornam o mestre. Há um terceiro escravo por precaução. No entanto, mesmo uma perda completa de dados no nosso caso é triste, mas não fatal. De acordo com o pior cenário, os usuários precisarão efetuar login e recuperar todos os dados que voltarão ao cache.

Serviço Java

Projetado como um microsserviço sem estado típico. Ele não possui configuração, todos os parâmetros necessários (e existem 6) são passados ​​pelas variáveis ​​de ambiente ao criar o contêiner do docker. Ele funciona com o servidor frontal através do transporte ZeroMQ (org.zeromq.jzmq - a interface java do libzmq.so.5.1.1 nativo, que nós próprios criamos) usando nosso próprio protocolo. Ele funciona com uma tarântula através de um conector java (org.tarantool.connector).

A inicialização do serviço é bastante simples:

  • Iniciamos um logger (log4j2);
  • A partir das variáveis ​​de ambiente (estamos na janela de encaixe), lemos os parâmetros necessários para o trabalho;
  • Iniciamos o servidor de métricas (jetty);
  • Conecte-se à tarântula (assincronamente);
  • Iniciamos o número necessário de manipuladores de threads (trabalhadores);
  • Iniciamos um broker (zmq) - um ciclo interminável de processamento de mensagens.

De todas as alternativas acima, apenas o mecanismo de processamento de mensagens é interessante. Abaixo está um diagrama do microsserviço.

Lógica do Message Broker

Vamos começar com o início do corretor. Nosso broker é um conjunto de zmq-sockets do tipo ROUTER, que aceita conexões de vários clientes e é responsável pelo envio de mensagens provenientes deles.

No nosso caso, temos um soquete de escuta na interface externa que recebe mensagens de clientes usando o protocolo tcp e o outro recebe mensagens de threads de trabalho usando o protocolo inproc (é muito mais rápido que o tcp).

/** //   (   ,   ) ZContext zctx = new ZContext(); //    ZMQ.Socket clientServicePoint = zctx.createSocket(ZMQ.ROUTER); //    ZMQ.Socket workerServicePoint= zctx.createSocket(ZMQ.ROUTER); //     clientServicePoint.bind("tcp://*:" + Config.ZMQ_LISTEN_PORT); //     workerServicePoint.bind("inproc://worker-proc"); 

Após inicializar os soquetes, iniciamos um loop de eventos sem fim.

 /** *      */ public int run() { int status;  try {   ZMQ.Poller poller = new ZMQ.Poller(2);    poller.register(workerServicePoint, ZMQ.Poller.POLLIN);    poller.register(clientServicePoint, ZMQ.Poller.POLLIN);    int rc;    while (true) {      //        rc = poller.poll(POLL_INTERVAL);      if (rc == -1) {        status = -1;        logger.errorInternal("Broker run error rc = -1");        break; //  -     }    //     ()    if (poller.pollin(0)) {       processBackendMessage(ZMsg.recvMsg(workerServicePoint));    }    //        if (poller.pollin(1)) {       processFrontendMessage(ZMsg.recvMsg(clientServicePoint));    }    processQueueForBackend(); }  } catch (Exception e) {    status = -1;  } finally {    clientServicePoint.close();    workerServicePoint.close();  }  return status; } 

A lógica do trabalho é muito simples: recebemos mensagens de lugares diferentes e fazemos algo com eles. Se algo gravemente falhou conosco, saímos do loop, que causa o travamento do processo, que será reiniciado automaticamente pelo daemon do docker.

A idéia principal é que o broker não lide com nenhuma lógica comercial, ele apenas analisa o cabeçalho da mensagem e distribui tarefas para os threads de trabalho que foram lançados anteriormente quando o serviço foi iniciado. Nisso, uma única fila de mensagens com priorização de um comprimento fixo o ajuda.

Vamos analisar o algoritmo usando o exemplo do esquema e código acima.

Após o início, os trabalhadores do encadeamento que iniciaram depois do intermediário são inicializados e enviam uma mensagem de prontidão para o intermediário. O intermediário os aceita, analisa-os e adiciona cada trabalhador à lista.

Um evento acontece no soquete do cliente - recebemos a mensagem1. O broker chama o manipulador de mensagens recebidas, cuja tarefa é:

  • análise do cabeçalho da mensagem;
  • colocar uma mensagem em um objeto titular com uma dada prioridade (com base na análise do cabeçalho) e tempo de vida;
  • colocando o titular na fila de mensagens;
  • se a fila não estiver cheia, a tarefa do manipulador terminou;
  • se a fila estiver cheia, chamamos o método para enviar uma mensagem de erro ao cliente.

Na mesma iteração do loop, chamamos o manipulador da fila de mensagens:

  • solicitamos a mensagem mais atual da fila (a fila decide isso sozinha, com base na prioridade e ordem de adição da mensagem);
  • verifique a vida útil da mensagem (se ela expirou, chame o método para enviar uma mensagem de erro ao cliente);
  • se a mensagem para processamento for relevante, tente preparar o primeiro trabalhador livre para trabalhar;
  • se não houver, coloque a mensagem de volta na fila (mais precisamente, apenas não a exclua de lá, ela ficará pendurada até a vida útil expirar);
  • se temos um trabalhador pronto para o trabalho, marcamos como ocupado e enviamos uma mensagem para processamento;
  • exclua a mensagem da fila.

Fazemos o mesmo com todas as mensagens subseqüentes. O próprio operador de encadeamentos foi projetado da mesma maneira que um broker - ele tem o mesmo ciclo de processamento de mensagens sem fim. Mas, como não precisamos mais de processamento instantâneo, ele foi projetado para executar tarefas demoradas.

Depois que o trabalhador concluiu sua tarefa (por exemplo, foi ao back-end dos produtos do cliente ou na tarântula da sessão), ele envia uma mensagem ao corretor, que o corretor envia de volta ao cliente. O endereço do cliente para quem a resposta deve ser enviada é lembrado desde o momento em que a mensagem chega do cliente no objeto titular, que é enviado ao trabalhador como uma mensagem em um formato ligeiramente diferente e depois retorna.

O formato das mensagens que menciono constantemente é nossa própria produção. Pronto, o ZeroMQ nos fornece as classes ZMsg - a própria mensagem e o ZFrame - parte dessa mensagem, essencialmente apenas uma matriz de bytes, que eu posso usar livremente se houver esse desejo. Nossa mensagem consiste em duas partes (dois ZFrames), o primeiro dos quais é um cabeçalho binário e o segundo são dados (o corpo da solicitação, por exemplo, na forma de uma string json representada por uma matriz de bytes). O cabeçalho da mensagem é universal e viaja de cliente para servidor e de servidor para cliente.

De fato, não temos o conceito de "solicitação" ou "resposta", apenas mensagens. O cabeçalho contém: versão do protocolo, tipo de sistema (qual sistema é endereçado), tipo de mensagem, código de erro no nível de transporte (se não for 0, algo aconteceu no mecanismo de transferência de mensagens), ID da solicitação (identificador de passagem proveniente do cliente - necessário para rastreamento), ID da sessão do cliente (opcional), bem como um sinal de erro no nível dos dados (por exemplo, se a resposta de back-end não puder ser analisada, definimos esse sinalizador para que o analisador no lado do cliente não desserialize a resposta, mas receba dados de erro de outra maneira).

Graças a um protocolo único entre todos os microsserviços e esse cabeçalho, podemos simplesmente manipular os componentes de nossos serviços. Por exemplo, você pode levar o intermediário para um processo separado e transformá-lo em um único intermediário de mensagens no nível de todo o sistema de microsserviço. Ou, por exemplo, execute trabalhadores não na forma de encadeamentos dentro do processo, mas como processos independentes separados. E enquanto o código dentro deles não muda. Em geral, há margem para criatividade.

Um pouco sobre desempenho e recursos


O próprio broker é rápido e a largura de banda total do serviço é limitada pela velocidade de back-end e pelo número de trabalhadores. Convenientemente, toda a quantidade necessária de memória é alocada imediatamente no início do serviço e todos os threads são iniciados imediatamente. O tamanho da fila também é fixo. No tempo de execução, apenas as mensagens estão sendo processadas.

Como exemplo: além do encadeamento principal, nosso serviço de combate em cache atual lança outros encadeamentos de 100 trabalhadores, e o tamanho da fila é limitado a três mil mensagens. Em operação normal, cada instância processa até 200 mensagens por segundo e consome cerca de 250 MB de memória e cerca de 2-3% da CPU. Às vezes, nas cargas de pico, ele salta para 7-8%. Tudo funciona em algum tipo de xeon virtual de núcleo duplo.

O trabalho regular do serviço implica o emprego simultâneo de 3 a 5 trabalhadores (de 100) com o número de mensagens na fila 0 (ou seja, eles passam para o processamento imediato). Se o back-end começar a diminuir, o número de trabalhadores ocupados aumentará proporcionalmente ao tempo de sua resposta. Nos casos em que ocorre um acidente e o back-end aumenta, todos os funcionários primeiro terminam, após o que a fila de mensagens começa a entupir. Quando entope completamente, começamos a responder aos clientes com recusas de processar. Ao mesmo tempo, não começamos a consumir recursos de memória ou CPU, fornecendo métricas de forma estável e respondendo claramente aos clientes o que está acontecendo.

A primeira captura de tela mostra o funcionamento regular do serviço.

O trabalho regular do serviço

E no segundo, ocorreu um acidente - por algum motivo, o back-end não respondeu em 30 segundos. Vê-se que, a princípio, todos os trabalhadores acabaram, após o que a fila de mensagens começou a entupir.

Acidente

Testes de desempenho


Os testes sintéticos na minha máquina de trabalho (CentOS 7, Core i5, 16 GB de RAM) mostraram o seguinte.

Trabalhe com o repositório (gravando na tarântula e lendo imediatamente esse registro de 100 bytes de tamanho - simulando o trabalho com a sessão) - 12000 rps.

Da mesma forma, apenas a velocidade foi medida não entre o serviço - pontos de tarântula, mas entre o cliente e o serviço. Claro, eu tive que escrever um cliente para testar o estresse. Dentro de uma máquina, era possível obter 7000 rps. Em uma rede local (e temos muitas máquinas virtuais diferentes que não estão claras quanto fisicamente conectadas), os resultados variam, mas é possível até 5000 rps para uma instância. Deus sabe que tipo de desempenho, mas mais de dez vezes cobre nossos picos de carga. E isso é apenas se uma instância do serviço estiver em execução, mas tivermos várias delas, e a qualquer momento você poderá executar quantas você precisar. Quando os serviços bloquearem a velocidade de armazenamento, será possível escalar a tarântula horizontalmente (fragmento com base no ID do cliente, por exemplo).

Inteligência de Serviço


O leitor atento provavelmente já faz a pergunta - qual é a “esperteza” deste serviço, mencionada no título. Já mencionei isso de passagem, mas agora vou lhe contar mais.

Uma das principais tarefas do serviço era reduzir o tempo necessário para emitir seus produtos aos usuários (listas de contas, cartões, depósitos, empréstimos, pacotes de serviços etc.) enquanto reduz a carga no back-end (reduz o número de solicitações no Oracle grande e pesado) devido ao armazenamento em cache na tarântula.

E ele fez isso muito bem. A lógica para aquecer o cache do cliente é a seguinte:

  • o usuário inicia o aplicativo móvel;
  • Uma solicitação do AppStart contendo o ID do dispositivo é enviada ao servidor frontal;
  • o servidor frontal envia uma mensagem com esse ID para o serviço de cache;
  • o serviço procura na tabela do dispositivo o ID do cliente para este dispositivo;
  • se não estiver lá, nada acontece (a resposta nem é enviada, o servidor não espera);
  • se o ID do cliente estiver localizado, o trabalhador criará um conjunto de mensagens para receber listas de produtos do usuário que entram imediatamente em processamento pelo intermediário e são distribuídos aos trabalhadores no modo normal;
  • cada trabalhador envia uma solicitação para um determinado tipo de dados ao usuário, colocando o status de "atualização" no banco de dados (esse status protege o back-end de repetir as mesmas solicitações se vierem de outras instâncias do serviço);
  • depois de receber os dados, eles são registrados na tarântula;
  • o usuário efetua login no sistema e o aplicativo envia solicitações para receber seus produtos, e o servidor envia essas solicitações na forma de mensagens para o serviço de cache;
  • se os dados do usuário já foram recebidos, simplesmente os enviamos do cache;
  • se os dados estiverem sendo recebidos (status de "atualização"), um ciclo de espera de dados será iniciado dentro do trabalhador (é igual ao tempo limite da solicitação para o back-end);
  • assim que os dados são recebidos (ou seja, o status desse registro (tupla) na tabela passa para "ocioso", o serviço os fornece ao cliente;
  • se os dados não forem recebidos dentro de um determinado intervalo de tempo, um erro será retornado ao cliente.

Assim, na prática, conseguimos reduzir o tempo médio de recebimento de produtos para o servidor frontal de 200 ms para 20 ms, ou seja, cerca de 10 vezes, e o número de solicitações para o back-end em cerca de 4 vezes.

Os problemas


O serviço de cache trabalha em batalha há cerca de dois anos e atualmente satisfaz nossas necessidades.

Obviamente, ainda existem problemas não resolvidos, às vezes ocorrem problemas. Os serviços Java na batalha ainda não caíram. A tarântula caiu algumas vezes no SIGSEGV, mas era uma versão antiga e, após a atualização, não aconteceu novamente. Durante o teste de estresse, a replicação cai, ocorreu um cano quebrado no mestre, após o qual o escravo caiu, embora o mestre continuasse a trabalhar. Foi decidido reiniciando o escravo.

Uma vez houve algum tipo de acidente no data center, e o sistema operacional (CentOS 7) parou de ver discos rígidos. O sistema de arquivos entrou no modo somente leitura. O mais surpreendente foi que os serviços continuaram funcionando, pois mantemos todos os dados na memória. A tarântula não conseguiu gravar arquivos .xlog, ninguém registrou nada, mas de alguma forma tudo funcionou. Mas a tentativa de reiniciar não teve êxito - ninguém poderia começar.

Há um grande problema não resolvido, e eu gostaria de ouvir a opinião da comunidade sobre esse assunto. Quando a tarântula principal falha, os serviços java podem mudar para o escravo, que continua a funcionar como mestre. No entanto, isso só acontece se o mestre travar e não puder funcionar.

Problema não resolvido

Suponha que tenhamos 3 instâncias de um serviço que funcionem com dados em uma tarântula mestre. Os serviços em si não caem, a replicação do banco de dados está acontecendo, está tudo bem. Mas, de repente, temos uma rede desmoronando entre o nó 1 e o nó 4, onde o assistente funciona. O Serviço 1 após várias tentativas malsucedidas decide alternar para o banco de dados de backup e começa a enviar solicitações para lá.

Imediatamente após isso, o escravo da tarântula começa a aceitar solicitações de modificação de dados, como resultado da replicação do mestre desmoronar e obtemos dados inconsistentes. Ao mesmo tempo, os serviços 2 e 3 funcionam perfeitamente com o mestre e o serviço 1 se comunica bem com o ex-escravo. É claro que, neste caso, começamos a perder sessões do cliente e outros dados, embora tudo funcione do lado técnico. Ainda não resolvemos um problema tão potencial. Felizmente, isso não aconteceu em dois anos, mas a situação é bastante real. Agora, cada serviço sabe o número da loja para onde vai e temos um alerta para essa métrica, que funcionará ao mudar de mestre para escravo. E você tem que reparar tudo com as mãos. Como você resolve esses problemas?

Planos


Planejamos trabalhar no problema descrito acima, limitando o número de trabalhadores ocupados simultaneamente com um tipo de solicitação, seguro (sem perder os pedidos atuais) para interromper o serviço e aperfeiçoar ainda mais.

Conclusão


Talvez isso seja tudo, embora eu tenha discutido o assunto superficialmente, mas a lógica geral do trabalho deve ser clara. Portanto, se possível, estou pronto para responder nos comentários. Descrevi brevemente como um pequeno subsistema auxiliar dos servidores dianteiros do banco funciona para atender clientes móveis.

Se o tópico for de interesse da comunidade, posso falar sobre várias de nossas soluções que contribuem para melhorar a qualidade do serviço ao cliente do banco.

Source: https://habr.com/ru/post/pt471740/


All Articles