Como quebramos a velha cabana e construímos um arranha-céu em seu lugar

Zurab Bely, líder da equipe de Java, conta sua história de trabalho em um projeto para uma grande empresa e compartilha sua experiência.

Como eu me acomodei ...


Entrei no projeto no final do verão de 2017 como desenvolvedor comum. Não posso dizer que naquela época eu gostei muito: as tecnologias usadas no projeto eram antigas, a comunicação dentro da equipe era minimizada, a comunicação com o cliente era difícil e improdutiva. Então o projeto me conheceu. Naquela época, eu tinha apenas um desejo: sair rapidamente disso.

Vou falar um pouco sobre o projeto como um todo. Este é o portal oficial de uma grande empresa com informações gerais, notícias, promoções e outros conteúdos. Todos os boletins de marketing contêm links para determinadas páginas do site, ou seja, a carga é razoavelmente média, mas em determinados momentos, pode atingir valores altos. A estabilidade e a acessibilidade do aplicativo Web requerem atenção especial - cada minuto de inatividade do serviço leva a grandes perdas para o cliente.

Favela que apertou os olhos ao vento


No começo, estudei principalmente as condições técnicas do projeto, corrigi pequenos bugs e fiz pequenas melhorias. Do ponto de vista técnico, o aplicativo parecia horrível: uma arquitetura monolítica construída em torno da versão comercial desatualizada do dotCMS, código escrito na versão Java 6, quando a nona renderização do servidor da parte do cliente na estrutura Velocity, que não tinha vários anos até então, já havia sido lançada foi suportado. Cada instância foi lançada no JBoss AS e roteada usando o Nginx. Vazamentos de memória levaram ao reinício constante dos nós e a falta de armazenamento em cache normal levou a um aumento na carga do servidor. Mas a maior lasca foram as alterações feitas no código CMS. Eles descartaram a possibilidade de uma atualização indolor para uma versão mais recente. Uma boa ilustração disso foi a transição da versão 3.2 para a 3.7, que estava terminando naquele momento. A transição para a próxima versão menor levou mais de um ano. Nenhuma solução popular, como Spring Framework, React.js, arquitetura de microsserviço, Docker etc., estava fora de questão. Aprofundando o projeto, as consequências de tal condição técnica se tornaram visíveis. O mais agudo deles foi a incapacidade de executar o aplicativo localmente para desenvolvimento e depuração. Toda a equipe de 8 pessoas trabalhou em um estande de desenvolvimento, onde uma cópia da versão de produção do aplicativo foi implantada. Portanto, apenas um desenvolvedor pode depurar seu código ao mesmo tempo, e a rolagem do código atualizado bloqueia toda a equipe. O apogeu foi uma falha na venda, durante a qual milhões de cartas, SMS e notificações push foram enviadas a diferentes usuários através de dezenas de canais - dezenas de milhares de sessões foram abertas ao mesmo tempo. Os servidores não aguentavam e na maioria das vezes o portal estava indisponível. O aplicativo não escala bem. Só havia uma maneira de fazer isso: implantar outra cópia lado a lado e equilibrar as cargas entre elas usando o Nginx. E cada entrega do código de produção envolvia muito trabalho manual e levava várias horas.

Seis meses após meu envolvimento no projeto, quando a situação já estava começando a ficar fora de controle, uma decisão foi tomada para mudar radicalmente a situação. O processo de transição começou. As mudanças afetaram todas as áreas: composição da equipe, processos de trabalho, arquitetura e componente técnico do aplicativo.

Nós construímos, construímos ...


Primeiro de tudo, as mudanças de pessoal ocorreram. Substituídos por vários desenvolvedores, eles me tornaram um líder de equipe. A transição para soluções modernas começou com o envolvimento de pessoas na equipe que tinham experiência em trabalhar com elas.

As mudanças processuais foram mais globais. Até então, o desenvolvimento já estava em andamento na metodologia Agile + Scrum, sprints de duas semanas com entrega no final de cada iteração. Mas, de fato, isso não apenas não aumentou a velocidade do trabalho, mas, pelo contrário, diminuiu a velocidade. Comícios diários se arrastaram por uma hora e meia a duas horas e não produziram nenhum resultado. Planejamento e preparação se transformaram em disputas, juramentos ou comunicação simples. Havia algo a ver com isso. Inicialmente, era muito difícil mudar alguma coisa nesse sentido - em nome do cliente, a equipe quase perdeu a confiança, principalmente após uma venda malsucedida. Cada mudança teve que ser substanciada, discutida e comprovada por um longo tempo. Curiosamente, mas a iniciativa veio do cliente. Por sua parte, um scrum-master estava envolvido para controlar a aplicação correta de abordagens e metodologias, depurar processos e definir a equipe para trabalhar. Embora ele tenha sido atraído por apenas alguns sprints, isso nos ajudou a montar adequadamente a fundação. A abordagem de comunicação com o cliente mudou muito. Começamos a discutir problemas nos processos com mais frequência, as retrospectivas começaram a ocorrer de forma mais produtiva, os desenvolvedores estavam mais dispostos a dar feedback e o cliente, por sua vez, avançou e apoiou o processo de transição de todas as maneiras.

Mas, honestamente, direi honestamente: houve alguns momentos em que algumas mudanças dentro da equipe foram realizadas "às cegas" e, após o aparecimento de resultados positivos, isso foi relatado ao cliente. Durante seis meses, a atitude mudou para uma comunicação de trabalho confortável. Isso foi seguido por várias equipes, reuniões de um e dois dias de toda a equipe de desenvolvimento com a equipe do cliente (comerciante, analista, designer, fornecedor de produtos, gerenciadores de conteúdo etc.), visitas conjuntas ao bar. Depois de um ano, e até hoje, a comunicação pode ser chamada de amigável. A atmosfera tornou-se amigável, descontraída e confortável. Claro, isso não acontece sem conflitos, mas mesmo na família mais feliz, às vezes há brigas.

Nenhuma mudança menos interessante ocorreu durante esse período no código do aplicativo, em sua arquitetura e nas soluções utilizadas. Se você não é tecnicamente experiente, pode pular com segurança o texto inteiro até a conclusão. E se você tiver sorte como eu - seja bem-vindo! Toda a transição pode ser dividida em várias etapas. Sobre cada um com mais detalhes.

Etapa 1. Identificação de áreas problemáticas críticas.

Tudo era o mais simples e claro possível. Antes de tudo, era necessário se livrar da dependência de um produto comercial de terceiros, cortar um monólito e possibilitar a depuração local. Eu queria separar o código do cliente e do servidor, distribuí-lo arquiteturalmente e fisicamente. Outro lugar problemático é a qualificação. O projeto carecia completamente de testes automáticos. Isso tornou a transição um pouco difícil, pois tudo tinha que ser verificado manualmente. Dado que nunca houve tarefas técnicas para o funcional (as especificidades do projeto são afetadas aqui), havia uma alta probabilidade de falta de algo. Tendo pintado as áreas problemáticas, mais uma vez analisamos a lista. Parecia um plano. É hora de construir um arranha-céu!

Etapa 2. Atualizando a base de código.

O estágio mais longo. Tudo começou com a transição para uma arquitetura de serviço (não deve ser confundida com microsserviços). A idéia era a seguinte: dividir o aplicativo em vários serviços separados, cada um deles lidando com sua tarefa específica. Os serviços não deveriam ser “micro”, mas eu também não queria colocar tudo em uma única caldeira. Cada serviço deveria ser um aplicativo Spring Boot escrito em Java SE 8 e executado no Tomcat.

O primeiro foi o chamado. "Serviço de conteúdo", que se tornou uma camada entre o aplicativo futuro e o CMS. Tornou-se uma abstração no caminho para o conteúdo. Supunha-se que todas as solicitações anteriormente feitas diretamente no CMS fossem realizadas por esse serviço, mas já utilizando o protocolo HTTP. Essa solução nos permitiu reduzir a conectividade e criou a possibilidade de substituir subseqüentemente o dotCMS por um analógico mais moderno ou até mesmo eliminar o uso de produtos comerciais e escrever nossa própria solução sob medida para nossas tarefas (olhando para o futuro, direi que é assim que seguimos).

Criou imediatamente o terreno para a separação do código da frente e do back-end. Eles criaram um serviço front-end, que se tornou responsável pela distribuição do código gravado na reação. Nós ferramos o npm, configuramos o nó e depuramos a montagem - tudo está como deveria, de acordo com as tendências modernas da peça do cliente.

Em geral, a funcionalidade foi alocada para o serviço de acordo com o seguinte algoritmo:

  • criou um novo aplicativo Spring Boot com todas as dependências e configurações necessárias;
  • portou toda a lógica básica (geralmente a escreveu do zero, referindo-se ao código antigo, apenas para garantir que você não esquecesse nenhuma nuance), por exemplo, para o serviço de cache, essas são as opções para adicionar ao cache, ler e desabilitá-lo;
  • toda nova funcionalidade sempre foi escrita usando o novo serviço;
  • reescrever gradualmente partes antigas do aplicativo em um novo serviço em ordem de importância.

No começo, tínhamos alguns deles:

  • Serviço de conteúdo. Atuou como uma camada entre o aplicativo e o CMS.
  • Serviço de cache. Repositório simples no Spring Cache.
  • Serviço AA. No início, ele estava envolvido apenas na distribuição de informações sobre um usuário autorizado. O restante permaneceu dentro do dotCMS.
  • Serviço de frente. Responsável pela distribuição do código do cliente.

Etapa 3. Autoteste.

Levando em consideração toda a experiência do projeto, decidimos que a presença de testes funcionais simplifica muito a vida e a possível atualização adicional do aplicativo. É hora de apresentá-los ao projeto. Testes de unidade do código, infelizmente, para dizer isso, pararam quase imediatamente. Eles levaram muito tempo para escrever e dar suporte, e tínhamos muito pouco, porque, além de reescrever o código, as tarefas atuais da nova funcionalidade dependiam de nós e os erros apareciam. Decidiu-se focar apenas no teste da interface do aplicativo usando o Selenium. Isso, por um lado, facilitou a execução de testes de regressão antes da entrega à produção; por outro, tornou-se possível a refatoração no lado do servidor, monitorando o estado no lado do cliente. A equipe não tinha um automatizador, e escrever e manter a relevância dos testes automáticos exige custos adicionais. Eles não treinaram novamente um dos testadores e outra pessoa foi adicionada à equipe.

Etapa 4. Automação de implantação.

Agora que temos serviços separados, quando o front-end se separa do back-end, quando a principal funcionalidade começa a ser coberta por autotestes, é hora de abrir uma lata de cerveja e automatizar todo o trabalho manual de implantação e suporte local do aplicativo, em servidores de demonstração e prod. Cortar o monólito em pedaços e o uso da bota de mola nos abriu novos horizontes.

Os desenvolvedores conseguiram depurar o código localmente, executando apenas a parte da funcionalidade necessária para isso. Finalmente, os estandes de teste começaram a ser usados ​​para a finalidade pretendida - já havia um código mais ou menos depurado, pronto para os testes iniciais e de qualificação. Mas ainda havia muito trabalho artesanal que desperdiçava nosso precioso tempo e energia. Depois de estudar o problema e classificar as soluções, decidimos por um monte de Gradle + TeamCity. Como o projeto foi construído pela Gradle, a adição de algo novo não fazia sentido, e os scripts que foram escritos eram independentes de plataforma, eles podem ser executados em qualquer sistema operacional, remotamente ou localmente. E isso permitiu não apenas o uso de qualquer solução para CI / CD, mas também a alteração indolor da plataforma para outra. O TeamCity foi escolhido devido à sua rica funcionalidade interna, à presença de uma grande comunidade e uma longa lista de plug-ins para todas as ocasiões, além da integração com o ambiente de desenvolvimento Intellij IDEA.

No momento, existem mais de 100 scripts de otimização e mais de 300 tarefas no sistema de IC para executá-los com parâmetros diferentes. Essa não é apenas uma implantação para testar bancos e entrega à produção, mas também trabalha com logs, gerenciamento de servidores, roteamento e soluções justas para tarefas rotineiras do mesmo tipo. Algumas das tarefas foram removidas de nossos ombros. Os gerenciadores de conteúdo conseguiram liberar o cache por conta própria. Os funcionários do suporte técnico tiveram a oportunidade de contratar serviços individuais por conta própria, realizar ações primárias de ressuscitação. O sono dos desenvolvedores se tornou mais profundo e calmo.

Etapa 5. Próprio CMS.

Depois que foi possível abstrair do CMS comercial, tornou-se possível receber dados de outra fonte, sem reescrever o código do aplicativo. Onde obter esses ou esses dados foi decidido pelo serviço de conteúdo. Depois de procurar soluções prontas e analisá-las, decidimos escrever nosso próprio sistema de gerenciamento de conteúdo, pois nenhuma delas atendia totalmente às nossas necessidades. Escrever o seu próprio CMS é um processo sem fim, novas necessidades e desejos aparecem constantemente. Selecionamos alguns recursos básicos e fomos para o belo mundo do desenvolvimento. Para lançar a primeira versão em prod, tivemos um mês e meio de homens. Como a funcionalidade está pronta no novo CMS, transferimos o conteúdo do antigo para ele. Todas as novas páginas não têm mais nada a ver com dotCMS. Com o tempo, isso tornou possível abandonar a versão paga e mudar para a versão da comunidade e, no futuro, abandonar completamente algo de terceiros.

Etapa 6. Revisão.

Depois de arregaçar as calças, começamos nossa jornada no mundo da programação hipster. Essa etapa do meu projeto foi a final do processo de reestruturação. Continua até hoje. A principal área para a qual esse estágio geralmente apareceu é a escala. O pacote Docker + Kubernetes + Consul permite não apenas facilitar a implantação em diferentes servidores e gerenciar cada serviço separadamente, mas também escalar com flexibilidade todo o aplicativo, fazê-lo apenas nos locais onde for necessário e pelo tempo que for necessário. Mais detalhadamente, só posso pintar quando mudarmos totalmente para esta solução em produção.

... e finalmente construído. Viva!



Um ano se passou desde o início da atualização do aplicativo. Agora, são 26 serviços REST gravados no Spring Boot. Cada um possui documentação detalhada da API na interface do usuário do Swagger. O lado do cliente é escrito em React.js e separado do lado do servidor. Todas as páginas principais do portal são cobertas com testes. Realizou várias vendas importantes. A transição para as tecnologias modernas, a eliminação do código antigo e a operação estável dos servidores motivam fortemente os desenvolvedores. Do “como dissemos, fazemos” o projeto mudou-se para o mainstream, onde todos estão interessados ​​no sucesso, oferece suas próprias opções de melhorias e otimização. A atitude do cliente em relação à equipe mudou, uma atmosfera amigável foi criada.

Este é o resultado do trabalho de toda a equipe, cada desenvolvedor e testador, gerentes e clientes. Foi muito difícil, nervoso e às vezes à beira de uma falta. Mas a coesão da equipe, o planejamento competente e a conscientização dos resultados tornaram possível superar todas as dificuldades.

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


All Articles