Ao pesar o risco e a recompensa de substituir a arquitetura, podem ser necessárias várias tentativas para encontrar uma solução que funcione para você.
Os desenvolvedores são famosos por querer reescrever o software, especialmente quando “herdaram” o software e não se dão ao trabalho de entender como ele funciona. Gerentes e engenheiros experientes sabem que as reescritas devem ser evitadas, a menos que sejam realmente necessárias, pois geralmente envolvem muita complexidade e podem introduzir novos problemas ao longo do caminho.
Esta é uma história sobre tentar repensar sistemas complexos: os desafios que você enfrenta ao tentar reconstruí-los, os fardos que enfrenta à medida que crescem e como a própria inação pode causar seus próprios problemas. Ao pesar o risco e a recompensa de substituir a arquitetura, podem ser necessárias várias tentativas para encontrar uma solução que funcione para você.
Sou um engenheiro sênior na Pusher, uma empresa focada na construção de APIs de mensagens em tempo real. O Pusher Channels , nosso serviço WebSocket pub / sub usado para criar funcionalidade de dados em tempo real escalonável, já existe há um bom tempo . Até bem recentemente, todos os canais eram executados em instâncias do AWS EC2. As máquinas foram provisionadas e inicializadas com scripts Python que envolviam os manuais do Ansible . O gerenciamento da configuração e do processo foi feito principalmente pela Puppet , com a ajuda de Upstart , God e várias ferramentas escritas internamente.
Gerenciamos instâncias EC2 como animais de estimação . Se uma máquina tivesse que ser substituída, um engenheiro migrava manualmente o tráfego / serviços da máquina antiga para a nova e desligava a antiga. Se um cluster precisar de mais capacidade, um engenheiro provisiona algumas máquinas novas e as conecta ao cluster. Essa abordagem tinha suas desvantagens:
Havia uma grande quantidade de trabalho manual envolvido em manter as coisas funcionando
As ferramentas internas tornavam a integração de novos membros da equipe complicada
… Mas funcionou muito bem por um bom tempo e grandes partes desse sistema ainda estão nos empurrando para novos patamares . Os membros da equipe mudaram, lançamos o Pusher Beams , encerramos o Pusher Chatkit e nossa base de usuários continuou crescendo. Enquanto isso, a infraestrutura dos Canais Pusher permaneceu praticamente a mesma, enquanto aumentava seriamente em escala. E é aí que os problemas começaram.
Manutenção
À medida que os clusters de canais ficavam maiores, a carga de manutenção de operação parecia escalar quase linearmente. Em pouco tempo, uma quantidade significativa de tempo de engenharia a cada semana era gasta apenas para manter as coisas funcionando.
Ficou claro que precisávamos fazer algo para reduzir a carga de manutenção; tentar manter um serviço altamente confiável para nossos clientes, entretanto, significou que passamos os últimos anos lutando contra o inevitável aumento de dívidas de tecnologia e infraestrutura legada que vem com o gerenciamento e manutenção de sistemas complexos em escala.
Fizemos várias tentativas de tentar modernizar nossa infraestrutura e aplicativo neste momento, mas encontramos muitos dos problemas comuns associados à tentativa de reescrever ou reescrever sistemas. Isso significa que ainda enfrentamos os mesmos desafios. Muitos desses problemas resultaram da tentativa de modernizar a infraestrutura e o aplicativo ao mesmo tempo, ao invés de tentar focar no maior desafio que enfrentamos, que é o gerenciamento e manutenção da própria infraestrutura.
A descoberta
Havíamos passado algum tempo indo e voltando entre as várias abordagens. Tentamos introduzir mais automação em nosso sistema de provisionamento de infraestrutura existente e tentamos reescrever totalmente nossos principais serviços de aplicativo. No entanto, muitas das soluções estavam emaranhadas ou dependentes umas das outras de maneiras não óbvias.
A maioria dessas soluções exiigia novos tipos de infraestrutura que ainda não tínhamos. Isso, então, tinha uma dependência “leve” de algumas das habilidades que tínhamos na equipe para adicionar novos tipos de infraestrutura com nossas ferramentas existentes.
Depois de uma série de contratempos, decidimos voltar aos primeiros princípios e olhar novamente para as prioridades mais importantes. Queríamos selecionar uma solução que atingisse os principais objetivos com escopo para continuar iterando no futuro. Resumimos que o objetivo geral deve ser terminar com uma plataforma que seja mais simples, consistente, fácil de manter e escalar. Isso nos permitiria gastar menos tempo mantendo e gerenciando plataformas e mais tempo criando e desenvolvendo novos produtos e recursos.
Depois de fazer uma auditoria completa do estado atual de nossa infraestrutura e aplicativo, chegamos às seguintes conclusões:
Descarregue o máximo possível de complexidade de infraestrutura para serviços gerenciados
Migre o máximo possível do aplicativo existente para esta nova infraestrutura sem ter que fazer grandes reescritas na base de código do aplicativo
Identifique os componentes que podem ser repensados, re-arquitetados ou reescritos sem adicionar atrito à migração da infraestrutura e invista em transformá-los e simplificá-los.
A infraestrutura
Há algum tempo que estávamos ansiosos para começar a alavancar grupos de escalonamento automático . Depois de analisar o trabalho necessário para usar grupos de escalonamento automático, determinamos que isso exigiria um investimento significativo para:
Estenda nossas ferramentas de implantação personalizadas - que já dependem do software EOL - para trabalhar com o cloudinit e migrar do upstart para o systemd. Isso também aumentaria o tempo de integração para novos contratados devido à natureza personalizada da solução.
Migre para uma tecnologia padronizada pelo setor, como contêineres.
Optamos por adotar contêineres porque investir tempo / energia / dinheiro em nossa solução interna não fazia sentido.
Containerising
Para migrar para contêineres, precisaríamos:
conteinerizar os principais serviços de aplicativos
atualizar o processo de construção para serviços de aplicativo para construir e armazenar imagens de contêiner,
escolha alguma forma de executar esses contêineres na produção,
altere o processo de roteamento do tráfego de serviços para lidar com o encerramento do contêiner de maneira mais elegante
Quando dividimos o trabalho necessário para colocar em contêiner nossa configuração EC2 existente para grupos de escalonamento automático, estávamos quase no caminho para algo como o Kubernetes , que estávamos usando intensamente em outras partes da empresa e nos oferecia muito mais funcionalidade.
Haveria trabalho extra, por exemplo, adicionar serviços de gerenciamento adicionais para mover nossos principais serviços de aplicativo para o Kubernetes. No final, decidimos que valeu a pena pelos seguintes motivos:
Objetivo criado para o nosso problema - o Kubernetes foi criado para resolver o problema que tínhamos - gerenciamento resiliente de uma carga de trabalho em muitos nós.
Experiência interna - tínhamos muita experiência com o Kubernetes internamente e já estávamos executando vários clusters.
Contratação - o Kubernetes é uma das ferramentas dominantes no mercado. Contratar engenheiros com experiência em Kubernetes, ou o desejo de aprendê-lo, foi significativamente mais fácil para nós do que contratar pessoas que quisessem trabalhar com o Puppet / Ansible.
Onboarding - Nossa solução existente era bastante sob medida, então qualquer novo marceneiro tinha que gastar uma boa parte do tempo aprendendo os meandros de todas as nossas ferramentas caseiras. O Kubernetes tem uma excelente documentação para que os iniciantes possam começar a trabalhar mais rapidamente, mesmo que não tenham experiência com ele.
Descarregando a complexidade da infraestrutura - essa mudança nos permitiria mudar a complexidade para um serviço gerenciado como o EKS.
A aplicação
Uma das armadilhas em que caímos antes ao tentar melhorar os Canais foi tentar reescrever grandes partes do aplicativo ao mesmo tempo em que tentamos reduzir a carga de manutenção da execução da infraestrutura. Essa abordagem fortemente acoplada levou a alguns contratempos e tentativas abandonadas. Com este novo plano, estávamos principalmente encontrando soluções para portar grandes partes de nossos serviços de aplicativo para o Kubernetes sem fazer grandes reescritas de aplicativos.
Ainda estávamos ansiosos para melhorar estrategicamente partes do aplicativo, no entanto, desde que isso não adicionasse atrito fundamental à migração para uma nova infraestrutura. Descobrimos que essa abordagem deu aos engenheiros a liberdade de fazer melhorias técnicas no aplicativo e oportunidades para reduzir a carga de manutenção dos componentes do aplicativo que frequentemente apresentam problemas que não seriam resolvidos com a transferência para o Kubernetes.
Como exemplo, escrevemos recentemente sobre nosso sistema de webhook de EventMachine no EC2 para Go no Kubernetes. Uma coisa que isso não abordou foi reescrever o componente que realmente retira as tarefas do SQS e envia webhooks. Este componente havia começado recentemente a causar um grande número de alertas e alarmes e precisava ser refatorado.
Reescrevendo o remetente do webhook
Sabíamos que era importante portar nosso componente de remetente de webhook do EC2 para o Kubernetes, pois tínhamos começado a enfrentar problemas operacionais crescentes com esse componente. Devido à falta de escalonamento automático, os processos responsáveis pelo envio de webhooks estavam sendo executados em máquinas EC2 dedicadas que chamamos de máquinas remetentes. Em um de nossos clusters, tínhamos quatro máquinas de remetente, cada uma executando 12 processos de remetente de webhook (chamados de Clowns, porque eles “manipulam” trabalhos de uma fila). Isso foi confortavelmente suficiente para o pico de carga do cluster, então tínhamos algum espaço para picos inesperados.
Como já havíamos reescrito em grande parte a maior parte do pipeline do webhook no Go, também fazia sentido concluir o processo reescrevendo o próprio remetente. O remetente do webhook é um software bastante simples. Ele lê trabalhos de uma fila SQS e faz solicitações HTTP POST. O trabalho que o processo lê do SQS contém tudo o que o processo precisa para enviar a solicitação HTTP POST ao servidor do cliente.
Como o remetente do webhook é um processo embaraçosamente paralelo, ele se ajusta perfeitamente às vantagens dos benefícios de dimensionamento de um sistema como o Kubernetes. Reescrevendo isso em Go, também poderíamos perceber alguns benefícios de desempenho em relação ao EventMachine, assim como tivemos com o empacotador e editor de webhook.
Como o remetente do webhook é um serviço sem estado, foi fácil implantar o novo remetente ao lado do remetente antigo e permitir que ambos competissem por empregos. Isso significava que poderíamos implantar gradualmente o novo remetente e contar com o remetente antigo para continuar atendendo à fila em caso de problemas inesperados. Na verdade, o que descobrimos em alguns clusters menores foi que o novo remetente era tão eficiente que o remetente antigo basicamente não tinha trabalho a fazer.
Em suma, isso foi um grande sucesso. Outro componente de nossa arquitetura EC2 e Eventmachine riscou a lista e outras caixas N foram removidas.
Resumo
Histórias bem documentadas sobre tentativas de reconstruir, redesenhar e repensar sistemas são incontáveis e geralmente contêm um aviso sobre o embarque em tais projetos. Aqueles de nós com experiência anterior em revisões de arquitetura quase sempre evitarão reescrever a qualquer custo porque muitas vezes elas dão errado. Além disso, os sistemas complexos geralmente são assim por um motivo. Conforme explicado pela cerca de Chesterton , geralmente é melhor presumir que a pessoa que veio antes de nós sabia algo que nós não sabíamos.
Após algumas falhas e desafios ao longo do caminho, no entanto, encontramos um caminho para o progresso e uma abordagem que nos permite reescrever o código de maneira eficaz e eficiente, ao mesmo tempo que reduz nossa carga de manutenção. Para ser justo, nossa reescrita provavelmente não era o que Joel Spolsky quis dizer quando chamou reescrever uma das coisas que você nunca deve fazer.
Mas o que descobrimos é que, ao identificar componentes com limites bem definidos, é possível reescrevê-los sem jogar sistemas inteiros fora. Se você puder identificar fronteiras e interfaces bem definidas, as coisas se tornam muito mais fáceis. No caso do pipeline de webhooks, ele tinha um limite lógico na forma de uma fila. Poderíamos reescrever partes disso com o tempo, o que nos deu grande capacidade de testar e verificar novos componentes no pipeline, ao mesmo tempo em que ainda tínhamos a capacidade de reverter em caso de falha.
Também descobrimos que, com cada um desses projetos concluídos, estamos percebendo os benefícios de menos carga operacional, o que significa que podemos realmente acelerar o ritmo de progresso. Fazer essa descoberta inicial foi o maior desafio e é importante manter o ritmo para evitar escorregar de volta para um reino insustentável. Agora que quebramos a barreira, estamos entusiasmados e confiantes em ter um sistema mais simples que nos permite nos concentrar em fazer as coisas que realmente amamos - construir produtos excelentes e recursos interessantes, em vez de apenas manter as luzes acesas.
O Avance Network é uma comunidade fácil de usar que fornece segurança de primeira e não requer muito conhecimento técnico. Com uma conta, você pode proteger sua comunicação e seus dispositivos. O Avance Network não mantém registros de seus dados; portanto, você pode ter certeza de que tudo o que sai do seu dispositivo chega ao outro lado sem inspeção.