Armazenamento em cache. Parte 2: 60 dias antes do lançamento

Olá! Eu já escrevi para você sobre como promover iniciativas em uma corporação. Mais precisamente, como (às vezes) isso é bem-sucedido e que dificuldades podem surgir: Uma retrospectiva do rake. Como uma solução feita por você acabou sendo mais fria que a paga e Como escolhemos um sistema de cache. Parte 1 .

Hoje, quero continuar e falar sobre o momento psicologicamente mais estressante desse projeto, sobre o qual os dois primeiros artigos - quando o resultado do projeto foi determinado não tanto pelas habilidades técnicas da equipe quanto pela confiança em seus cálculos e vontade de chegar ao fim.

Eu tenho que dizer - eu acho que para levar o projeto a um momento tão intenso - é um erro muito usado sobre lshaya do que qualquer heroísmo, estendendo o projeto para esse problema ...
Mas não escondo essa experiência e a compartilho de bom grado - porque considero:

  • precisamente áreas problemáticas são pontos de crescimento
  • os maiores problemas "chegam" precisamente de onde você não espera

A combinação desses pontos - apenas obriga a compartilhar a maravilhosa experiência de "como ganhar uma sarjeta do nada". Mas, deve-se notar, uma situação semelhante é excepcional na empresa Sportmaster. Ou seja, é possível que essa situação ocorra novamente - planejamento e definição de responsabilidade agora - em um nível completamente diferente.

Então, parece que a introdução é suficiente, se você estiver pronto - bem-vindo ao gato.



Junho 2017 Estamos modificando o painel de administração. O painel de administração não é apenas um conjunto de formulários e tabelas na interface da Web - os valores inseridos precisam ser colados com dezenas de outros dados que obtemos de sistemas de terceiros. Além disso, de alguma forma, transforme e, finalmente, envie para os consumidores (o principal deles é o site ElasticSearch da Sportmaster).

A principal dificuldade é apenas para converter e enviar. Nomeadamente:

  1. você precisa fornecer dados na forma de json, que pesa 100 KB cada, e alguns aparecem por 10 MB (verifique a disponibilidade e os critérios de entrega de mercadorias às lojas)
  2. json com uma estrutura que possui anexos recursivos de qualquer nível de aninhamento (por exemplo, um menu dentro de um item de menu, no qual há itens de menu novamente, etc.)
  3. a declaração final não é aprovada e está em constante mudança (por exemplo, o trabalho com mercadorias por modelos é substituído por uma abordagem quando trabalhamos por modelos coloridos). Constantemente - isso é várias vezes por semana, com uma taxa de pico de 2 vezes por dia durante uma semana.

Se os dois primeiros pontos são puramente técnicos e são ditados pela própria tarefa, então com o terceiro ponto, é claro, você precisa lidar com isso organizacionalmente. Mas, como o mundo real está longe do ideal, trabalhamos com o que temos.

Ou seja, eles descobriram como rebitar rapidamente formulários da web e seus objetos no lado do servidor.

Uma pessoa da equipe foi nomeada para o papel de um "tapa de formulário" profissional e, usando componentes da Web preparados, lançou uma demonstração para a interface do usuário mais rapidamente do que os analistas corrigiram os desenhos dessa interface.

Mas, para mudar o esquema das transformações, a complexidade surgiu aqui.

Primeiro, seguimos o caminho usual - para realizar a transformação na consulta sql para Oracle. Havia um especialista em DB na equipe. Ele durou até o momento em que a solicitação era de 2 páginas de texto sql contínuo. Eu podia continuar, mas quando as mudanças vieram dos analistas - objetivamente, a coisa mais difícil foi encontrar o lugar onde fazer as mudanças.

Analistas expressaram regra nos regimes, que, apesar de terem sido pintadas em algo separado do código (uma espécie de visio / draw.io / Gliffy), mas não foram tãosemelhante a quadrados e setas nos sistemas ETL (por exemplo, Pentaho Kettle, que na época era usado para fornecer dados ao site da Sportmaster). Agora, se não tivéssemos uma consulta SQL, mas um esquema ETL! Em seguida, a instrução e a solução seriam expressas topologicamente de forma idêntica, o que significa que a edição do código pode levar tanto tempo quanto a edição da instrução!

Mas com sistemas ETL, há outra dificuldade. A mesma Pentaho Kettle - é ótima quando você precisa criar um novo índice no ElasticSearch, no qual escrever todos os dados colados de várias fontes (observação: na verdade, é a Pentaho Kettle que não funciona muito bem, porque não usa javascript nas transformações relacionado a classes java através das quais o consumidor acessa dados - por isso, é possível anotar algo que não pode ser transformado nos objetos necessários do pojo, mas esse é um tópico separado, longe do curso principal do artigo).

Mas o que fazer quando, no painel de administração, o usuário corrigiu um campo em um documento? Para entregar essa alteração no site ElasticSearch da Sportmaster, não crie um novo índice no qual preencher todos os documentos desse tipo, incluindo um atualizado!

Queria que, quando um objeto nos dados de entrada fosse alterado, enviasse uma atualização para o ElasticSearch do site apenas para o documento de saída correspondente.

Ok, o próprio documento de entrada, mas, afinal, de acordo com o esquema de transformação, ele pode ser anexado a documentos de um tipo diferente por meio da junção! Portanto, é necessário analisar o esquema de transformação e calcular quais documentos de saída serão afetados pela alteração nos dados nas fontes.

A busca de produtos in a box para solucionar esse problema não levou a nada. Não encontrado.
E quando se desesperavam por descobrir, descobriram, mas como deveria funcionar por dentro e como isso pode ser feito?

A idéia surgiu imediatamente.

Se o ETL final puder ser dividido em suas partes constituintes, cada uma com um tipo específico de um conjunto finito (por exemplo, filtro, junção etc.), será possível criar o mesmo conjunto finito de nós especiais que correspondem aos originais, mas com a diferença de que eles trabalham não com os dados em si, mas com a mudança deles?

Em grandes detalhes, com exemplos e pontos-chave na implementação, nossa solução - quero abordar em um artigo separado. Para lidar com posições de apoio - isso exigirá uma imersão séria, a capacidade de pensar abstratamente e confiar no que ainda não foi manifestado. De fato, será interessante precisamente do ponto de vista matemático e é interessante apenas para os habrovitas interessados ​​em detalhes técnicos .
Aqui só posso dizer que criamos um modelo matemático no qual descrevemos 7 tipos de nós e mostramos que esse sistema está completo - ou seja, usando esses 7 tipos de nós e as conexões entre eles - qualquer esquema de transformação de dados pode ser expresso. A implementação é baseada no uso ativo de obter e registrar dados por chave (ou seja, por chave, sem condições adicionais).

Assim, nossa solução teve um ponto forte em relação a todas as dificuldades introdutórias:

  1. os dados devem ser fornecidos na forma de json -> trabalhamos com objetos pojo (objeto java antigo simples, se alguém não encontrou os horários em que essa designação estava em uso), que são fáceis de ultrapassar no json
  2. existe json com uma estrutura que possui incorporações recursivas de qualquer nível de aninhamento -> novamente, pojo (o principal é que não há loops, mas quantos níveis de aninhamento não são importantes, é fácil processar em java através da recursão)
  3. a declaração final está constantemente mudando -> excelente, pois estamos mudando o esquema de transformação mais rapidamente do que os analistas elaboram (nos diagramas) os desejos de experimentos

Dos momentos de risco, apenas um - nós escrevemos a solução do zero, por conta própria.

Na verdade, as armadilhas não demoraram a chegar.

Momento especial N1. Armadilha. "Bem extrapolado"


Outra surpresa de natureza organizacional foi que, ao mesmo tempo em que o nosso desenvolvimento, o repositório principal principal estava sendo transferido para uma nova versão e o formato no qual esse repositório fornece dados foi alterado. E seria bom se nosso sistema funcionasse imediatamente com o novo armazenamento, e não com o antigo. Mas o novo armazenamento ainda não está pronto. Mas, então, as estruturas de dados são conhecidas e elas podem nos fornecer um suporte de demonstração no qual uma pequena quantidade de dados relacionados será derramada. Está indo?

Aqui na abordagem do produto, ao trabalhar com o fluxo de fornecimento de valor, um aviso é inequivocamente martelado em todos os otimistas: existe um bloqueador -> a tarefa não funciona, ponto final.

Mas, então, essa dependência nem sequer levantou suspeitas. De fato, ficamos eufóricos com o sucesso do protótipo de processador Delta - um sistema para processar dados sobre deltas (implementação de um modelo matemático quando as alterações nos dados de saída são calculadas usando o esquema de transformação como resposta a uma alteração nos dados de entrada).

Entre todos os esquemas de transformação, um era o mais importante. Além do fato de o circuito em si ser o maior e mais complexo, havia também um requisito estrito para que a transformação fosse realizada de acordo com esse circuito - um prazo para a execução de toda a quantidade de dados.

Portanto, a transformação deve ser realizada 15 minutos e não mais um segundo. A entrada principal é uma tabela com 5,5 milhões de registros. No estágio de desenvolvimento, a tabela ainda não está preenchida. Mais precisamente, ele é preenchido com um pequeno conjunto de dados de teste no valor de 10 mil linhas.

Bem, vamos começar. Na primeira implementação, o processador Delta trabalhou no HashMap como o armazenamento de valor-chave (deixe-me lembrá-lo, precisamos ler e escrever objetos muito por chave). Obviamente, nos volumes de produção, todos os objetos intermediários não cabem na memória - portanto, em vez do HashMap, passamos para o Hazelcast.

Por que exatamente Hazelcast - então, porque este produto era familiar, foi usado no back-end do site do Sportmaster. Além disso, este é um sistema distribuído e, como nos pareceu - se um amigo faz algo errado com o desempenho - adicionamos mais instâncias a algumas máquinas e o problema foi resolvido. Em casos extremos - uma dúzia de carros. Escala horizontal e tudo mais.

E assim, estamos lançando nosso processador Delta para uma transformação direcionada. Funciona quase instantaneamente. Isso é compreensível - os dados são de apenas 10 mil em vez de 5,5 milhões, portanto multiplicamos o tempo medido por 550 e obtemos o resultado: algo em torno de 2 minutos. Bem! De fato - uma vitória!

Isso foi no início do trabalho do projeto - exatamente quando você precisa decidir sobre a arquitetura, confirmar as hipóteses (realizar testes que as confirmam), integrar a solução piloto verticalmente.

Como os testes mostraram um excelente resultado - ou seja, confirmamos todas as hipóteses, revertemos rapidamente o piloto - montamos um "esqueleto" verticalmente integrado para uma pequena parte da funcionalidade. E eles começaram a codificação principal - enchendo o "esqueleto com carne".

O que com sucesso e vigorosamente envolvidos. Até aquele belo dia, quando um conjunto completo de dados foi carregado na loja principal .

Execute o teste neste conjunto.

Após 2 minutos não funcionou. Também não trabalhei após 5, 10, 15 minutos. Ou seja, eles não se encaixavam na estrutura necessária. Mas, com quem isso não acontece, será necessário ajustar algo em detalhes e em forma.

Mas o teste não funcionou uma hora depois. E mesmo depois de duas horas, havia esperança de que ele funcionasse, e procuraremos o que apertar. Restos de esperança foram mesmo após 5 horas. Mas, depois de 10 horas, quando eles foram para casa, mas o teste ainda não deu certo - não havia mais esperança.

O problema era que, no dia seguinte, quando chegaram ao escritório, o teste ainda continuava diligentemente a funcionar. Como resultado, rolou por 30 horas, não esperou, desligou.
Catástrofe!

O problema foi localizado com rapidez suficiente.

O Hazelcast - ao trabalhar com uma pequena quantidade de dados - rolou tudo na memória. Mas quando foi necessário despejar dados em um disco, o desempenho caiu milhares de vezes.

A programação seria uma ocupação chata e sem gosto, se não fosse pelas autoridades e pela obrigação de entregar o produto acabado. Então, literalmente, um dia depois, depois de recebermos um conjunto completo de dados - precisamos ir às autoridades com um relatório sobre como passou o teste nos volumes de produção.

Esta é uma escolha muito séria e difícil:

  1. diga "como está" = abandone o projeto
  2. diga "como eu gostaria" = arriscar, talvez, não se saiba se podemos resolver o problema

Para entender quais sentimentos surgem nesse caso, só é possível investir totalmente na idéia, realizar o plano por meio ano, criar um produto que ajude os colegas a resolver uma enorme camada de problemas.

E assim, desistir de sua amada criação é muito difícil.
Isso é característico de todas as pessoas - nós amamos aquilo em que colocamos muito esforço. Portanto, é difícil ouvir críticas - você deve conscientemente fazer esforços para perceber adequadamente o feedback.

Em geral, decidimos que ainda existem muitos sistemas diferentes que podem ser usados ​​como armazenamento de valor-chave e, se o Hazelcast não se encaixar, algo definitivamente funcionará. Ou seja, eles decidiram arriscar. Para nossa justificativa, podemos dizer que ainda não era um "prazo sangrento" - em geral, ainda havia uma margem de tempo para "mudar" para uma solução de backup.

Naquela reunião com os chefes, nosso gerente indicou que "o teste mostrou que o sistema trabalha de forma estável em volumes de produção, não falha". De fato, o sistema funcionou de maneira estável. 60 dias

para liberar .

Momento especial N2. Não é uma armadilha, mas não é uma descoberta. "Menos é mais"


Para encontrar um substituto para o Hazelcast pela função de data warehouse de valor-chave, compilamos uma lista de todos os candidatos - obtivemos uma lista de 31 produtos. Isso é tudo o que eu consegui pesquisar no google e descobrir com meus amigos. Além disso, o Google forneceu algumas opções absolutamente obscenas, como o trabalho de um estudante.

Para testar os candidatos mais rapidamente, preparamos um pequeno teste que, em alguns minutos de lançamento, mostrava desempenho nos volumes certos. E eles paralelizaram o trabalho - todos pegaram o próximo sistema da lista, configuraram, executaram o teste, fizeram o próximo.
Eles trabalharam rapidamente, instalaram vários sistemas por dia.

No 18º sistema, ficou claro que isso não fazia sentido. Sob o nosso perfil de carga - nenhum desses sistemas é aprimorado. Eles têm muitos babados e reverências para facilitar o uso, muitas abordagens bonitas para o dimensionamento horizontal - mas isso não nos dá nenhum lucro.

Precisamos de um sistema que _fast_ salve a chave em um objeto no disco e leia rapidamente a chave.

Nesse caso, descrevemos o algoritmo de como isso pode ser implementado. Em geral, parece bastante viável - se ao mesmo tempo: a) sacrificar a quantidade de dados que ocupará o disco, b) tiver estimativas aproximadas da quantidade e tamanho característico dos dados em cada tabela.
Algo no estilo, aloque memória (em disco) para objetos com margem, partes de um volume máximo fixo. Em seguida, usando as tabelas de índice ... e assim por diante ...
Foi uma sorte que não tenha chegado a isso.

A salvação veio na forma de RocksDB.
Este é um produto do Facebook desenvolvido para leitura rápida e salvamento de uma matriz de bytes em disco. Ao mesmo tempo, o acesso aos arquivos é fornecido por meio de uma interface semelhante ao armazenamento do Key-Value. De fato, a chave é uma matriz de bytes, o valor é uma matriz de bytes. Otimizado para fazer esse trabalho de maneira rápida e confiável. Todos. Se você precisar de algo mais bonito e de alto nível - enrosque-se.
Exatamente o que precisamos!

O RocksDB, responsável pelo armazenamento de Key-Value - elevou o indicador de teste de destino ao nível de 5 horas. Estava longe de 15 minutos, mas o principal foi feito. O principal era entender o que estava acontecendo, entender que gravar no disco era o mais rápido possível, mais rápido do que impossível. No SSD, em testes refinados, o RocksDB espremeu 400 Mb / s, e isso foi suficiente para a nossa tarefa. Atrasos - em algum lugar do nosso, em um código de ligação.

No nosso código, o que significa que podemos lidar com isso. Vamos desmontá-lo, mas podemos lidar com isso.

Momento especial N3. Apoio, suporte. "Cálculo teórico"


Temos um algoritmo e entrada. Tomamos o intervalo de dados de entrada, calculamos quantas ações o sistema deve executar, como essas ações são expressas nos custos de tempo de execução da JVM (atribuir um valor a uma variável, inserir um método, criar um objeto, copiar um conjunto de bytes etc.), além de quantas chamadas para RocksDB deve ser mantido.

De acordo com os cálculos, eles devem cumprir 2 minutos (aproximadamente, como o teste do HashMap mostrou no início, mas isso é apenas uma coincidência - o algoritmo mudou desde então).

E, no entanto, o teste é executado por 5 horas.

E agora, antes do lançamento de 30 dias.

Esta é uma data especial - agora será impossível recolher - não teremos tempo para mudar para a opção de backup.
É claro que, neste dia, o gerente de projeto é convocado às autoridades. A questão é a mesma - tenha tempo, está tudo bem?



Aqui está a melhor maneira de descrever essa situação - uma imagem de capa estendida para este artigo. Ou seja, os chefes são mostrados na parte da imagem que é renderizada no título. Mas, na realidade - assim.

Embora, na realidade, é claro - não éramos nada engraçados. E diga que "está tudo bem!" - isso é possível apenas para uma pessoa com uma habilidade muito forte de autodomínio.
Grande, enorme respeito pelo gerente, por acreditar e confiar nos desenvolvedores.

Código realmente disponível - mostra 5 horas. Um cálculo teórico - mostra 2 minutos. Como isso pode ser acreditado?

Mas é possível se: o modelo for formulado claramente, como contar é compreensível e que valores substituir também são compreensíveis. Ou seja, o fato de que, na realidade, a execução leva mais tempo significa que, na realidade, não é exatamente o código que esperamos executar lá que está sendo executado.

A tarefa central é encontrar "lastro" no código. Ou seja, algumas ações são executadas além do fluxo principal de criação dos dados finais.

Apressado. Testes unitários, composições funcionais, fragmentação de funções e localização de locais com uma quantidade desproporcional de tempo gasto na execução. Muitas coisas foram feitas.
Ao longo do caminho, formulamos esses lugares onde você pode apertar seriamente.

Por exemplo, serialização. Utilizado pela primeira vez o java.io padrão. Mas se apertarmos o Cryo, no nosso caso, obteremos um aumento de 2,5 vezes na velocidade de serialização e uma redução de 3 vezes na quantidade de dados serializados (o que significa que o IO é 3 vezes menor, o que consome apenas os principais recursos). Mas, com mais detalhes, este é um tópico para um artigo técnico separado.

Mas o ponto chave, ou "onde o elefante se escondeu" - tentarei descrever em um parágrafo.

Ponto especial 4. Recepção para encontrar uma solução. "Problema = solução"


Quando obtemos / configuramos por chave - nos cálculos, ela foi executada como 1 operação, afeta IO no volume igual a chave + valor do objeto (na forma serializada, é claro).
Mas e se o próprio objeto no qual chamamos get / set for um Mapa, que também obtemos por get / set do disco. Quanto o IO será feito neste caso?

Em nossos cálculos, esse recurso não foi levado em consideração. Ou seja, foi considerado como 1 IO para chave + valor do objeto. Mas de fato?

Por exemplo, no armazenamento de Valor-Chave, pela chave-1, há um objeto obj-1 com o tipo Map, no qual um determinado objeto obj-2 deve ser armazenado na chave chave-2. Aqui pensamos que a operação exigiria um IO para a chave 2 + obj-2. Mas, na realidade, você precisa considerar o obj-1, manipulá-lo e enviá-lo para IO: chave-1 + obj-1. E se for um mapa no qual existem 1000 objetos, o consumo de IO será cerca de 1000 vezes mais. E se 10.000 objetos, então ... Foi assim que eles conseguiram o "lastro".

Quando um problema é identificado, a solução geralmente é óbvia.

No nosso caso, isso se tornou uma estrutura especial para manipulações dentro do Mapa aninhado. Ou seja, esse valor-chave, que para obter / definir leva duas chaves ao mesmo tempo, que devem ser aplicadas seqüencialmente: chave-1, chave-2 - ou seja, para o primeiro nível e para o aninhado. Como implementar essa estrutura - vou contar detalhadamente com prazer, mas novamente, em um artigo técnico separado.
Aqui, neste episódio, enfatizo e promovo esse recurso: um problema extremamente detalhado é uma boa solução.

Conclusão


Neste artigo, tentei mostrar os pontos e armadilhas organizacionais que podem surgir. Tais armadilhas são muito claramente visíveis “de lado” ou ao longo do tempo, mas é muito fácil entrar nelas quando você se encontra ao lado delas. Espero que alguém se lembre dessa descrição e, no momento certo, o lembrete funcione: "Eu já ouvi algo assim em algum lugar antes".

E, o mais importante - agora que tudo é contado sobre o processo, sobre os momentos psicológicos, sobre os organizacionais. Agora que temos uma idéia de quais tarefas e sob quais condições o sistema foi criado. Agora - você pode e deve falar sobre o sistema do lado técnico - que tipo de modelo matemático é esse, e quais truques no código que criamos e quais soluções inovadoras pensamos.

Sobre isso no próximo artigo.

Enquanto isso, Happy New Code!

All Articles