Cirurgia cardíaca: como reescrevemos o principal componente de um sistema DLP

Reescrever o código legado como uma visita ao dentista parece ser que todo mundo entende que deve ir, mas mesmo assim procrastina e tenta atrasar o inevitável, porque sabe que isso vai doer. No nosso caso, as coisas eram ainda piores: tivemos que reescrever a parte principal do sistema e, devido a circunstâncias externas, não pudemos substituir os trechos de código antigos por trechos de código novos por partes, apenas todos de uma vez e totalmente. E tudo isso em condições de falta de tempo, recursos e documentação, mas com a exigência de gerenciamento de que, como resultado da "operação", nenhum cliente deve sofrer.

Sob o corte, a história de como reescrevemos o componente principal do produto com uma história de 17 anos (!) De Scheme para Clojure, e tudo funcionou imediatamente (bem, quase :)).



17 anos no "relógio"


Solar Dozor é um sistema DLP com uma história muito longa. A primeira versão apareceu em 2001 como um serviço relativamente pequeno para filtrar o tráfego de mensagens. Ao longo de 17 anos, o produto cresceu para um grande pacote de software que coleta, filtra e analisa informações heterogêneas em execução na organização e protege os negócios dos clientes contra ameaças internas.

Ao desenvolver a sexta versão do Solar Dozor, sacudimos o produto com determinação, jogamos fora muletas velhas do código e as substituímos por novas , atualizamos a interface, revisamos a funcionalidade na direção das realidades modernas - em geral, tornamos o produto arquitetônico e conceitualmente mais holístico.

Naquela época, sob o capô do Solar Dozor atualizado, havia uma enorme camada de código legado monolítico - o serviço de filtragem que, durante todos esses 17 anos, gradualmente se transformou em nova funcionalidade, incorporando soluções de longo prazo e tarefas de negócios de curto prazo, mas conseguiu permanecer dentro da arquitetura original paradigmas.


Serviço de filtragem

Escusado será dizer que a introdução de quaisquer alterações em um código tão antigo exigia especial delicadeza. Os desenvolvedores tiveram que ser extremamente cuidadosos para não arruinar acidentalmente a funcionalidade criada há uma década. Além disso, novas soluções interessantes foram forçadas a se espremer no leito da arquitetura procrusteana, inventado no início da era.

O entendimento de que surgiu a necessidade de atualizar o sistema surgiu há algum tempo. Mas faltava claramente o espírito de tocar em um serviço enorme e antigo do sistema.

Não tentando atrasar o inevitável


Produtos com uma longa história de desenvolvimento têm um recurso interessante. Por mais estranha que pareça uma funcionalidade, se ela sobreviveu até os dias atuais, isso significa que ela foi criada não a partir das idéias teóricas dos desenvolvedores, mas em resposta às necessidades específicas dos clientes.

Nesta situação, não se pode falar em substituição em fases. Era impossível cortar e refazer a funcionalidade em peças, porque todas essas peças estavam sendo demandadas pelos clientes e não podíamos "fechá-las para reconstrução". Era necessário remover cuidadosamente o serviço antigo e fornecer uma substituição com todos os recursos. Somente no todo, apenas de uma vez.

Melhorar o processo de desenvolvimento do produto, a velocidade de fazer alterações e melhorar a qualidade como um todo era uma condição necessária, mas não suficiente. A gerência imaginou que benefícios as mudanças trariam para nossos clientes. A resposta foi expandir o conjunto de interfaces para interagir com novos sistemas de interceptação, o que forneceria feedback rápido e permitiria aos interceptadores responderem mais rapidamente aos incidentes.

Também tivemos que lutar para reduzir o consumo de recursos, mantendo (e idealmente aumentando) a taxa de processamento atual.

Um pouco sobre o recheio


Ao longo do caminho de desenvolvimento do produto, a equipe da Solar Dozor tendeu a uma abordagem funcional. Isso leva a uma escolha não padronizada de linguagens de programação para uma indústria madura. Em diferentes estágios da vida do sistema, eles foram Scheme, OCaml, Scala, Clojure, além dos tradicionais C (++) e Java.

O principal serviço de filtragem e outros serviços que ajudam a receber e transmitir mensagens foram escritos e desenvolvidos na linguagem Scheme em suas várias implementações (o último foi usado pelo Racket). Por mais que se queira cantar os louvores à simplicidade e elegância dessa linguagem, não se pode deixar de admitir que seu desenvolvimento atende a interesses acadêmicos mais que os industriais. O atraso é especialmente notável em comparação com outros serviços mais modernos da Solar Dozor, que são desenvolvidos principalmente em Scala e Clojure. O novo serviço também foi decidido para ser implementado no Clojure.

Clojure ?!


Aqui, é claro, devo dizer algumas palavras sobre por que escolhemos o Clojure como a principal linguagem de implementação.

Em primeiro lugar, não queria perder a experiência única da equipe desenvolvendo no Scheme. Clojure também é um membro moderno da família de idiomas Lisp, e alternar de um Lisp para outro geralmente é bastante simples.

Em segundo lugar, graças ao compromisso com os princípios funcionais e com várias soluções arquitetônicas exclusivas, o Clojure oferece uma facilidade sem precedentes de manipulação de fluxos de dados. Também é importante que o Clojure opere na plataforma JVM, o que significa que você pode usar um banco de dados conjunto com outros serviços em Java e Scala, além de usar várias ferramentas para criação de perfil e depuração.

Em terceiro lugar, Clojure é uma linguagem concisa e expressiva. Isso facilita a leitura do código de outra pessoa e a transferência de código para um colega de equipe.

Por fim, valorizamos o Clojure por sua facilidade de prototipagem e o chamado desenvolvimento centrado no REPL. Em quase qualquer situação em que haja dúvida, você pode simplesmente criar um protótipo e continuar a discussão de maneira mais substantiva, com novos dados. O desenvolvimento orientado ao REPL fornece um retorno rápido, porque para verificar a funcionalidade de uma função, não é apenas necessário recompilar o programa, mas também reiniciá-lo (mesmo que o programa seja um serviço localizado em um servidor remoto).

Olhando para o futuro, posso dizer: acho que não perdemos a escolha.

Colocando a funcionalidade pouco a pouco


Quando falamos sobre uma substituição com todos os recursos, a primeira pergunta que surge é a coleta de informações sobre a funcionalidade existente.

Isso se tornou uma tarefa bastante interessante. Parece que aqui está um sistema em funcionamento, aqui está a documentação para ele, aqui estão pessoas - especialistas que trabalham em estreita colaboração com o sistema e ensinam outras pessoas sobre ele. Mas, para obter uma imagem completa de toda a variedade, e mais ainda, os requisitos para o desenvolvimento acabaram não sendo tão simples.

A coleta de requisitos não é em vão considerada uma disciplina de engenharia separada. A implementação existente paradoxalmente acaba sendo o papel de algum "padrão corrompido". Ele mostra como e como deve funcionar, mas, ao mesmo tempo, espera-se que os desenvolvedores que a nova versão seja melhor que a original. É necessário separar os momentos necessários para a implementação (geralmente relacionados a interfaces externas) daqueles que podem ser aprimorados de acordo com as expectativas dos usuários.


Processo de Filtragem de Mensagens

A documentação não é suficiente


Qual é a funcionalidade real do sistema? A resposta a esta pergunta é dada por várias descrições, como documentação do usuário, manuais e documentos arquitetônicos, refletindo a estrutura do serviço em vários aspectos. Mas quando se trata disso, você entende muito bem o quanto as idéias e a realidade divergem, quantas nuances e inexplicáveis ​​possibilidades o código antigo contém.

Quero entrar em contato com todos os desenvolvedores. Cuide do seu código! Este é o seu ativo mais importante. Não confie na documentação. Confie apenas no código fonte.

Felizmente para nós, o código do esquema, devido à própria natureza da linguagem criada para o ensino de programação, é bastante fácil de ler, mesmo para uma pessoa não treinada. O principal é se acostumar com algumas formas individuais que carregam um leve toque de Lisp-arcaico.

Construa um processo


O volume de trabalho foi enorme e a equipe é muito pequena. Portanto, não foi sem dificuldades organizacionais. O fluxo de trabalho de bugs e solicitações de correção (e pequenas melhorias) para o antigo serviço de filtragem nem parou. Os desenvolvedores regularmente tinham que se distrair com essas tarefas.

Felizmente, foi possível evitar pedidos de incorporação de novas peças de ótima funcionalidade no filtro antigo. É verdade, com a promessa de incorporar essa funcionalidade em um novo serviço. No entanto, o conjunto de tarefas de liberação estava crescendo lenta mas seguramente.

Outro fator que causou muitos problemas foram as dependências externas do serviço. Sendo um componente central, o serviço de filtragem utiliza vários serviços para descompactar e analisar o conteúdo (textos, imagens, impressões digitais, etc.). O trabalho com eles foi parcialmente orientado por antigas decisões arquitetônicas. Durante o processo de desenvolvimento, também foi necessário reescrever alguns componentes de uma maneira moderna (e alguns em uma linguagem moderna).

Em tais condições, um sistema de teste funcional passo a passo foi construído. Nós meio que aumentamos o serviço para um determinado estado, que foi reforçado por testes ativos, e depois passamos a implementar um novo.

Iniciar desenvolvimento


Primeiro, a estrutura principal do serviço, os mecanismos básicos para receber mensagens e descompactar arquivos foram implementados. Esse era o mínimo absoluto necessário para começar a testar a velocidade e a correção do serviço futuro.

Aqui, deve ser esclarecido que a descompactação se refere ao processo recursivo de obter partes de um arquivo e extrair delas informações úteis. Assim, por exemplo, um documento do Word pode conter não apenas texto, mas também imagens, um documento incorporado do Excel, objetos OLE e muito mais.

O mecanismo de desempacotamento não distingue entre o uso de bibliotecas internas, programas externos ou serviços de terceiros, fornecendo uma interface única para organizar pipelines de desempacotamento.

Outro elogio a Clojure: recebemos um protótipo funcional, no qual descrevemos os contornos da funcionalidade futura, no menor tempo possível.

DSL para a política


A segunda etapa foi adicionar a validação de mensagens usando políticas de filtragem.

Para descrever as políticas, foi criada uma DSL especial - uma linguagem simples e sem frescuras, que nos permitiu apresentar as regras e condições da política de uma forma mais ou menos legível por humanos. É chamado MFLang.

O script no MFLang "on the fly" é interpretado no código Clojure, armazena em cache os resultados das verificações na mensagem, mantém um registro detalhado do trabalho (e, francamente, merece um artigo separado).

O uso do DSL atraiu os testadores. Abaixo as escavações no banco de dados ou no formato de exportação! Agora era possível simplesmente enviar a regra gerada para verificação e imediatamente ficou claro quais condições foram verificadas. Também foi possível obter um log detalhado de verificação de mensagens, do qual fica claro quais dados foram obtidos para verificação e quais resultados foram retornados pela função de comparação.

Podemos dizer com confiança que o MFLang acabou sendo uma ferramenta absolutamente inestimável para a funcionalidade de depuração.

Com força total


No terceiro estágio, foi adicionado um mecanismo para aplicar as ações definidas pela política de segurança à mensagem, além de ligações de serviço que permitem a inclusão de novos componentes no complexo Solar Dozor. Por fim, conseguimos lançar o serviço e observar o resultado do trabalho em toda a sua diversidade.

A questão principal, é claro, era como a funcionalidade implementada corresponde ao que era esperado e até que ponto a implementa.

Observo que, se a necessidade de teste de unidade não for posta em causa por um longo tempo (embora as próprias práticas de TDD ainda causem um debate animado), a introdução de testes automatizados da funcionalidade do sistema geralmente enfrenta resistência aberta.

O desenvolvimento de autotestes ajuda todos os membros da equipe a entender melhor o processo do produto, economiza energia na regressão, instila certa confiança no desempenho do produto. Mas o processo de sua criação está repleto de várias dificuldades - coleta dos dados necessários, determinação dos indicadores de interesse e opções de teste. Os programadores inevitavelmente percebem a criação de autotestes como um trabalho opcional opcional, que é melhor evitar se possível.

Mas se você conseguir superar a resistência, é criada uma base bastante sólida que permite criar uma idéia da saúde do sistema.

Nós substituímos


E então chegou um momento importante: incluímos o serviço no pacote de entrega. Até agora, junto com o antigo. Assim, uma equipe poderia realizar uma alteração de versão e comparar o comportamento dos serviços.

Nesse modo paralelo, o novo serviço de filtragem durou uma versão. Durante esse período, conseguimos coletar estatísticas adicionais sobre o trabalho, delinear e implementar as melhorias necessárias.

Por fim, reunindo forças, removemos o antigo serviço de filtragem do produto. O estágio final da aceitação interna foi, os bugs foram corrigidos, os desenvolvedores começaram a mudar gradualmente para outras tarefas. De alguma forma imperceptível, sem alarde e aplausos, um produto foi lançado com um novo serviço.

E somente quando as perguntas começaram a surgir da equipe de implementação, chegou o entendimento - o serviço em que trabalhamos há tanto tempo já estava nos locais e ... trabalhando!

Obviamente, houve bugs e pequenas melhorias; no entanto, após um mês de uso ativo, os clientes emitiram um veredicto: a introdução de um produto com uma nova versão do serviço de filtragem causou menos problemas do que as versões anteriores. Ei! Parece que conseguimos!

No final


O desenvolvimento de um novo serviço de filtragem levou cerca de um ano e meio. Mais do que se pensava inicialmente, mas não crítico, especialmente porque a intensidade real do trabalho coincidiu com a avaliação inicial. Mais importante, fomos capazes de atender às expectativas da gerência e dos clientes e estabelecer as bases para futuras melhorias no produto. Já no estado atual, você pode ver uma redução significativa no consumo de recursos - apesar do produto ainda ter amplas oportunidades de otimização.

Eu posso adicionar algumas impressões pessoais.

Substituir um componente central por uma longa história é uma lufada de ar fresco para o desenvolvimento. Pela primeira vez em muito tempo, há confiança de que o controle do produto está voltando para nossas mãos.

É difícil superestimar os benefícios de um processo de comunicação e desenvolvimento adequadamente organizado. Nesse caso, era importante estabelecer um trabalho não muito dentro da equipe, assim como com vários consumidores do produto, que tinham há muito tempo preferências e expectativas claras do sistema e desejos bastante vagos.

Para nós, essa foi a primeira experiência no desenvolvimento de um projeto de grande escala no Clojure. Inicialmente, havia preocupações relacionadas à natureza dinâmica da linguagem, velocidade e tolerância a erros. Felizmente, eles não se materializaram.

Resta apenas desejar que o novo componente funcione pelo tempo e com sucesso que seu antecessor.

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


All Articles