Engenharia de Software Moderna – Parte 1: Projeto do Sistema

Crescendo no final dos anos 80 e início dos anos 90, minha exposição aos computadores era praticamente limitada aos consoles (acho que o Atari 800 e o Commodore 64 porque eu só tinha visto jogos rodando neles) ou aos primeiros sistemas X86. Foi só quando eu estava na faculdade, em 2000, que tive uma estação de trabalho Sun Microsystems SPARC, UNIX e Slackware Linux que pude instalar em minha máquina Intel 486 em casa.

Naquela época, o desenvolvimento de software significava principalmente software executado em sua máquina ou, se você tivesse oportunidade, em um computador de tempo compartilhado com significativamente mais poder de processamento do que você poderia... fazer assuntos relacionados aos negócios. Na faculdade, lembro-me de ter ouvido falar de um programa usado por cientistas da computação que exigia um processador multinúcleo para gerar horários de cursos para milhares de alunos; demorava semanas para gerar e imprimir os horários dos cursos. Até hoje, ainda não tenho certeza do que demorou mais: executar o programa e imprimir em papel.

Hoje, a maior parte do software desenvolvido é executado na nuvem, em dispositivos que precisam acessar a nuvem ou alimenta outros softwares que também são executados na nuvem. Os sistemas de software que funcionam em espaços confinados, como os sistemas de software embarcados, são muito raros se uma plataforma de computação mais poderosa não puder ser obtida em outro lugar. Os sistemas de contabilidade agora compactam grandes quantidades de dados, que são hospedados em farms de servidores em data warehouses dentro ou fora da empresa. O relacionamento com os clientes dos sistemas de vendas agora é gerenciado por terceiros e seus plug-ins são desenvolvidos por terceiros ou por desenvolvedores internos.

Mas como esses sistemas de software são construídos hoje para atender centenas a milhões de usuários e, ao mesmo tempo, manter o desempenho e a capacidade de resposta esperados do software que usamos hoje?

Como engenheiro de software há 20 anos, tenho visto muitos sistemas desenvolvidos em todos os níveis da pilha. Desde manipuladores de interrupção da era DOS até animações baseadas em JavaScript e até mesmo geração de relatórios sem código. Algumas semanas atrás eu até pedi ao ChatGPT-4 para gerar algum código Python que eu queria com base em algumas descrições que dei! Mas isso é outra história! Mas isso é outra história.

Neste artigo, escrevo sobre design de sistemas, como ele se tornou uma parte fundamental da prática moderna de engenharia de software e como será uma das principais áreas onde os engenheiros de software humanos ainda poderão agregar valor no curto e médio prazo.

A importância do design do sistema

Há muito tempo, eu era engenheiro de software em uma empresa que tinha problemas para lidar com a carga de sucesso que ela mesma havia gerado. Eu chamo essa empresa de Friendster. Quando entrei na empresa, o projeto em que estava trabalhando estava muito atrasado e apresentava muitos bugs relacionados ao gerenciamento de memória. Seu serviço principal (sim, era um microsserviço antes de chamá-lo assim em 2007) foi escrito em C++, mas apresentava vazamentos de memória, demorava muito para processar solicitações e foi projetado para armazenar em cache e servir dados em sua própria memória. Precisava ser sem estado, mas acabou sendo com estado.

Algumas semanas após o início do projeto, implorei à liderança sênior de engenharia que abandonasse essa iteração do serviço e, em vez disso, escrevesse algo do zero que atendesse aos requisitos; isso seria um substituto imediato para a implementação existente. Temos um prazo porque o serviço só aguenta o crescimento por mais alguns meses antes de não conseguir mais lidar com o tamanho do cache de forma reidratada.

A reinicialização do serviço leva mais tempo do que pode antes que haja vazamento de memória. Foi um momento de “apostar na minha carreira”, mas quase não tive chance. Temos que fazer isso funcionar.

O design do sistema começa. A primeira coisa que fizemos foi definir os requisitos que o sistema deveria atender, qual era o contrato entre os serviços dependentes (código front-end PHP) e esse serviço principal e um plano sobre como atenderíamos aos três principais serviços não- requisitos técnicos: Desempenho, Eficiência e Resiliência.

O projeto do sistema envolve a compreensão das restrições sob as quais o sistema deve desempenhar sua função, qual é a funcionalidade necessária e quais propriedades do sistema são importantes em relação a todas as outras propriedades. Depois de ter essas definições, você poderá começar a projetar um sistema que atenda aos requisitos e planejar sistematicamente a entrega da solução.

Componentes de design do sistema

Quando falamos sobre design de sistema, geralmente existem vários componentes que isso requer:

  • Arquitetura – Como é a solução geral? Envolve vários subsistemas? Existem componentes individuais que constituem um todo? Como eles interagem e quais são seus relacionamentos?
  • Topologia – A solução está em camadas? Se este for um sistema distribuído, onde os serviços de componentes estão localizados física ou logicamente?
  • Design de baixo nível — Quais interfaces você definiu por meio das quais diferentes partes do sistema interagem? Você possui algoritmos específicos que abordam os principais aspectos da solução (desempenho, eficiência, rendimento, resiliência, etc.)?

Comecemos pelo princípio: o sistema é independente (ou seja, não acessa recursos externos) ou distribuído? Terá uma interface de usuário ou será não interativo (por exemplo, gerará um relatório impresso ou exigirá informações de um ser humano ou de outro sistema durante sua operação)? Ele precisa lidar com muito tráfego? Será usado por apenas dez pessoas a qualquer momento ou será usado por 10 milhões de usuários a qualquer momento?

Depois de obter respostas para algumas dessas perguntas, será mais fácil tomar decisões por meio dos princípios de design de sistemas.

Princípios de design de sistema

Nesta sociedade moderna, vários princípios-chave para a concepção de sistemas de software não emergem completamente até que o sistema precise de ser dimensionado – de um sistema de utilizador único para um sistema que deverá ser capaz de lidar com milhares ou mesmo milhões de utilizadores em simultâneo. Aqui estão alguns dos que abordaremos neste artigo:

  • Escalabilidade
  • ConfiabilidadeConfiabilidade
  • ManutençãoManutenção
  • DisponibilidadeDisponibilidade
  • SegurançaSegurança

Escalabilidade

Um sistema é escalável quando pode ser implantado para lidar com o crescimento da carga com um aumento proporcional nos recursos. O fator de expansão de um sistema é definido como o aumento na quantidade de recursos necessários para atender ao aumento na carga do sistema. Encontraremos duas situações típicas de expansão em sistemas de software: expansão vertical e expansão horizontal.

A expansão vertical refere-se ao fornecimento de mais espaço ou recursos independentes para um sistema de software lidar com o aumento da demanda. Considere o caso dos dispositivos de armazenamento conectados à rede. Quanto mais armazenamento você fornecer por meio de um dispositivo, mais dados ele poderá armazenar. Se você precisar lidar com mais conexões simultâneas e operações de E/S (IOPs), normalmente precisará adicionar mais poder de computação e interfaces de rede para lidar com o aumento da carga.

A expansão refere-se à duplicação de um sistema ou de várias máquinas com cópias de software para lidar com o crescimento da demanda. Considere o caso de um servidor de conteúdo da Web estático oculto atrás de um balanceador de carga. Adicionar mais servidores permite que mais clientes se conectem e baixem conteúdo dos servidores web e, quando a carga diminuir, o número de servidores web pode ser reduzido para atender às necessidades atuais.

Alguns sistemas podem lidar com extensões mistas ou diagonais. Por exemplo, algumas arquiteturas de bancos de dados distribuídos permitem o particionamento de nós de computação e armazenamento para que cargas de trabalho com uso intensivo de computação possam usar nós com mais recursos de computação. Por outro lado, cargas de trabalho com muitos IOPs podem ser executadas em nós de armazenamento+computação. Por exemplo, um aplicativo de processamento de fluxo pode isolar cargas de trabalho que exigem mais memória e computação (por exemplo, fornecimento de eventos ou cargas de trabalho analíticas) e dimensionar essas cargas de trabalho de forma adequada e independente de cargas de trabalho pesadas com IOPs (por exemplo, compactação e arquivamento).

ConfiabilidadeConfiabilidade

Um sistema é confiável quando pode tolerar falhas e recuperações parciais sem degradar gravemente a qualidade do serviço. Parte da fiabilidade de um sistema inclui a previsibilidade do seu funcionamento, ou seja, latência, rendimento e adesão aos intervalos de funcionamento acordados.

Os métodos comuns para garantir a confiabilidade do sistema incluem os seguintes aspectos:

  • Configure a redundância do sistema para oferecer suporte a failover transparente ou minimamente perturbador.
  • Estabeleça mecanismos de tolerância a falhas em caso de erros internos ou falhas induzidas por entradas.
  • Defina claramente contratos e metas de latência, rendimento e disponibilidade.
  • Configure capacidade ociosa suficiente para acomodar aumentos repentinos e orgânicos de carga.
  • Medidas de garantia de qualidade de serviço para impor limites de taxas e segregação cliente/empresa.
  • Imponha a degradação normal do serviço em caso de sobrecarga ou falha catastrófica.

A principal coisa a lembrar ao criar sistemas confiáveis ​​é lidar com falhas potenciais de uma forma bem definida que permita que os sistemas dependentes reajam. Isto significa que se houver entradas que possam fazer com que o sistema fique indisponível para todos, então não é um sistema confiável. Da mesma forma, se um sistema depende de outro sistema que pode não ser confiável, então ele deverá ter políticas para lidar com a falta de confiabilidade para garantir a confiabilidade.

ManutençãoManutenção

Um sistema pode ser mantido quando as alterações são feitas com esforço proporcional e implementadas com o mínimo de interrupção do usuário. Isto exige que, ao implementar o sistema, se presuma que os requisitos mudarão e que o sistema seja flexível o suficiente para lidar com mudanças de direção previsíveis. Significa também garantir que o código seja legível para que o próximo conjunto de mantenedores (talvez a mesma equipe, mas olhando para ele com novos olhos no futuro) possa manter o software e permitir que ele evolua para atender às necessidades futuras.

Ninguém quer ficar preso à manutenção de software rígido, difícil de alterar, mal organizado, mal documentado, mal projetado, não testado e remendado.

Garantir a alta qualidade do código faz parte da excelência da engenharia e reflete profissionalismo e excelente habilidade artesanal. Isso não é apenas bom, mas também permite que equipes de engenharia altamente funcionais e de alto desempenho forneçam software que pode ser alterado e expandido para continuar a agregar valor.

DisponibilidadeDisponibilidade

Se o seu serviço não estiver disponível, provavelmente não existe.

O projeto do sistema deve abordar como um sistema deve permanecer disponível para permanecer relevante para clientes e usuários do sistema. isso significa:

  • A redundância é introduzida para lidar com falhas subjacentes do sistema.
  • Tenha um plano de backup e recuperação e um guia prático para recuperar o sistema de uma falha grave.
  • Remova tantos pontos únicos de falha do sistema quanto possível.
  • Além da escalabilidade horizontal, há replicação regional e construção de uma rede de entrega de conteúdo (quando apropriado) para disponibilizar seus dados.
  • Monitore a disponibilidade do seu sistema da perspectiva do cliente para entender melhor como seu sistema está atendendo seus clientes.

No início da minha carreira, aprendi que um sistema instável e inutilizável às vezes pode ser o maior motivo para perder a confiança do cliente. Depois que você perde a confiança de seus clientes, é difícil recuperá-la.

SegurançaSegurança

A concepção do sistema deve abordar a segurança como um aspecto fundamental, especialmente na era dos sistemas ligados à Internet, onde as ameaças e vulnerabilidades à segurança podem causar danos reais aos nossos clientes e utilizadores do sistema. O objetivo na construção de software seguro não é atingir a perfeição, mas compreender os riscos envolvidos em vulnerabilidades e ataques. Ter um modelo apropriado de ameaças à segurança e uma abordagem sistemática para compreender onde estão os riscos e quais tipos de ameaças merecem ser priorizados e projetar mitigações é o início do projeto de segurança e das práticas de engenharia.

Hoje, à medida que os nossos sistemas de software se tornam parte de serviços de missão crítica para mais partes da sociedade moderna, a segurança já não é opcional. Levar a segurança a sério desde o início nos sistemas que projetamos nos aproxima de poder confiar melhor no software que construímos e implantamos para atender às necessidades de nossos usuários. Ganhar a confiança de seus clientes já é bastante difícil e basta uma vulnerabilidade para perder uma boa parte dessa confiança.

padrões de design modernos

Tendo em vista os aspectos acima, alguns padrões de sistemas distribuídos modernos surgiram para resolver alguns problemas nestes aspectos de diferentes maneiras. Vamos explorar alguns dos padrões de design mais populares que vimos hoje em relação aos cinco aspectos do design de sistemas.

MicrosserviçosMicrosserviços

Com o surgimento de sistemas distribuídos, que se concentram na construção de confiabilidade e escala por meio de redundância, eficiência e desempenho por meio de escalonamento horizontal e resiliência por meio da dissociação de partes do sistema em serviços executados de forma independente, o termo "microsserviços" As palavras ganham popularidade ao alcançar o seguinte:

  • Vincule o desenvolvimento, a implantação, a operação e a manutenção de serviços independentes com as equipes que possuem esses serviços dentro da operação comercial maior. Podemos fazer isso atendendo clientes externos direta ou indiretamente atendendo clientes internos por meio de APIs.
  • Permita que os microsserviços sejam dimensionados de forma independente com base na demanda.
  • Os serviços são fornecidos através de um contrato bem definido, permitindo aos implementadores desenvolverem-se como um serviço autónomo ou como um sistema de serviços.

Da nossa perspectiva, os microsserviços têm propriedades atraentes que os tornam um bom padrão, se aplicável ao caso de uso:

  • Escalabilidade: os microsserviços sem estado são normalmente projetados para escalar horizontalmente e também podem se beneficiar do escalonamento vertical. Quando os microsserviços são implantados em um ambiente de orquestração em contêiner, como um cluster Kubernetes, os microsserviços podem até ser executados nos mesmos nós, fazendo melhor uso do hardware existente e dimensionando a capacidade disponível com base na demanda. Uma desvantagem é a complexidade da implantação à medida que o tamanho e a criticidade de um microsserviço aumentam dentro de um gráfico de microsserviços.
  • Confiabilidade: os microsserviços sem estado são normalmente hospedados atrás de balanceadores de carga e distribuídos geograficamente para evitar falhas regionais que consomem toda a capacidade do sistema. Uma desvantagem de estabelecer confiabilidade com microsserviços sem estado é que o sistema de armazenamento muitas vezes precisa ser tão confiável quanto, ou até mais confiável, do que a implementação/implantação do microsserviço. Os microsserviços com estado sofrem com o pior de ambas as abordagens, com o custo da confiabilidade muitas vezes vindo na forma de provisionamento excessivo para lidar com possíveis interrupções.
  • Capacidade de manutenção: Microsserviços que implementam contratos estáveis ​​e bem definidos fornecidos por meio de uma API, permitindo que os clientes programem com base nessa API, e implementações que podem evoluir de forma independente. No entanto, a coordenação de mudanças de API envolve migrações de clientes potencialmente caras e coordenação entre equipes, introduzindo um período em que um microsserviço tem múltiplas versões com suporte ativo até que o cliente final seja migrado da implementação antiga. Esta situação só vai piorar à medida que mais clientes começarem a interagir com microsserviços.
  • Disponibilidade: Os microsserviços normalmente dependem do ambiente de implantação e da infraestrutura externa para atender aos requisitos de disponibilidade do cliente. A desvantagem disto é que depende da infraestrutura específica na qual os microsserviços são implantados para fornecer uma solução de alta disponibilidade. Sistemas como service meshes e balanceadores de carga de software tornam-se partes críticas da infraestrutura, não mais sob o controle do implementador. Isso pode ser bom, mas também pode ser uma fonte contínua de manutenção, uma vez que esses sistemas também possuem ciclos de atualização e custos operacionais.
  • Segurança: Autenticação, autorização, gerenciamento de identidade e gerenciamento de credenciais podem ser delegados ao middleware ou por meio de mecanismos externos (como identidade de carga de trabalho no Kubernetes), e as implementações de microsserviços podem se concentrar na integração de lógica de negócios relevante. Tal como acontece com a disponibilidade, a desvantagem é que estas partes externas da solução tornam-se partes críticas da infraestrutura, acrescentando os seus próprios custos operacionais à implementação de microsserviços.

Os microsserviços são uma ótima maneira de dividir aplicativos grandes, onde é possível identificar partições lógicas que exigem seus próprios domínios de escala e confiabilidade. No entanto, ao começar do zero, projetar microsserviços desde o início não é o ideal, pois existe o risco de quebrar o serviço em pedaços muito pequenos. O custo da comunicação entre microsserviços – geralmente solicitações HTTP ou gRPC – é significativo e deve ser incorrido somente quando necessário. Uma boa maneira de determinar se a funcionalidade é apropriada para um serviço é seguir práticas como design orientado a domínio ou decomposição funcional.

Sem servidor

Como em uma solução baseada em microsserviços, o uso de uma implementação sem servidor delega ainda mais partes funcionais importantes das solicitações de serviço à infraestrutura subjacente. Se em um microsserviço o serviço for fornecido por um processo persistente, então as soluções sem servidor normalmente implementam apenas um ponto de entrada para lidar com solicitações ao endpoint (geralmente um URI via HTTP ou gRPC). Em uma implantação sem servidor, nenhum servidor real é configurado; em vez disso, o ambiente de implantação gera recursos conforme necessário para lidar com as solicitações recebidas. Às vezes, esses recursos permanecem por um período de tempo para amortizar o custo de lançamento, mas isso é apenas um detalhe de implementação.

Vejamos vários aspectos do design do sistema para ver como as soluções sem servidor se comparam:

  • Escalabilidade: As soluções sem servidor são tão escaláveis ​​horizontalmente quanto os microsserviços, ou até mais, porque são projetadas para escalar sob demanda. A desvantagem dessa abordagem é que ela requer mais controle e delega totalmente a funcionalidade de escalonamento à infraestrutura sem servidor subjacente.
  • Confiabilidade: a confiabilidade sem servidor depende da capacidade de escalar horizontalmente e rotear o tráfego de rede. Isso tem as mesmas desvantagens da solução de microsserviços.
  • Capacidade de manutenção: uma implementação sem servidor é mais fácil de manter do que microsserviços porque se concentra na lógica de negócios de processamento de solicitações e minimiza clichês. Este é o mesmo problema de evolução da API que os microsserviços apresentam.
  • Disponibilidade: as implantações sem servidor estão disponíveis apenas conforme o ambiente em que são implantadas. Isto tem o mesmo problema, onde a infra-estrutura subjacente se torna mais crítica do que a própria solução.
  • Segurança: a implementação sem servidor depende inteiramente da configuração de segurança da infraestrutura subjacente. Isto tem o mesmo problema, onde a infra-estrutura subjacente se torna mais crítica do que a própria solução em si.

As soluções sem servidor, ou funções como serviço, são uma forma muito atraente de criar protótipos e até mesmo de implantá-las em produção, concentrando-se na lógica e no valor do negócio e deixando a infraestrutura subjacente lidar com a escalabilidade, a confiabilidade e a disponibilidade da solução em nível de serviço. Este é um ponto de partida típico para colocar uma solução em funcionamento com carga operacional mínima e, como acontece com a maioria dos protótipos, é uma ótima maneira de provar nossa hipótese. Também é uma experiência típica que, quando estas soluções atingem os seus limites de escalabilidade, os custos associados à sua execução se tornam suficientemente elevados. Tudo isso se transforma em implementações de microsserviços mais otimizadas e ajustadas à escala necessária.

Orientado por EventosEvent-Driven

No entanto, algumas áreas problemáticas não exigem processamento de transações on-line, e microsserviços e implementações sem servidor não são adequados. Considere situações em que as transações podem ser processadas em segundo plano ou quando há recursos disponíveis. Outro caso é a atividade de processamento em segundo plano, cujo resultado não é necessariamente interativo.

Os sistemas orientados a eventos seguem o padrão de ter uma fonte de eventos e um coletor de eventos, de onde os eventos (mensagens) vêm e são enviados. O processamento é realizado nessas fontes e coletores por assinantes e editores, respectivamente. Um exemplo de sistema orientado a eventos é um chatbot que pode participar de muitas conversas (fontes e coletores de eventos) e processar mensagens à medida que elas chegam.

Os sistemas distribuídos controlados por eventos podem ter vários manipuladores de mensagens simultâneos aguardando na mesma fonte, potencialmente publicando muitos coletores como fontes para outros manipuladores de mensagens. Esse padrão de encadeamento de processadores por meio de coletores e fontes é chamado de pipeline de eventos. Normalmente, os coletores e as fontes têm uma implementação única que fornece uma interface de fila de mensagens e é estendida com base na demanda de mensagens que passam pelo sistema. Muitos sistemas distribuídos de gerenciamento de filas também podem se beneficiar efetivamente do dimensionamento diagonal, como Apache Kafka, RabbitMQ, etc.

Vejamos os sistemas distribuídos orientados a eventos através de nossas cinco dimensões:

  • Escalabilidade: Tanto as implementações do agente de mensagens/eventos quanto os manipuladores de mensagens são escaláveis ​​de forma independente. Algumas desvantagens surgem quando muitas mensagens/eventos são processados ​​e a demanda no corretor de eventos cresce muito além da capacidade disponível do sistema.
  • Confiabilidade: Uma boa implementação do agente de mensagens fornece um alto nível de confiabilidade. É uma boa ideia não criar sua própria implementação do agente de mensagens. A desvantagem é a dependência de uma solução que atenda às necessidades de confiabilidade (por exemplo, o processamento de transações financeiras é muito diferente do processamento de roteamento de mensagens instantâneas para uma sala de chat).
  • Capacidade de manutenção: se você usar um formato flexível de troca de mensagens, como buffers de protocolo, faz sentido desenvolver escritores e leitores de mensagens usando a mesma linguagem de descrição de dados. Isto ainda requer coordenação, mas não é tão complicado quanto a evolução dos contratos de API em sistemas de processamento de transações em tempo real (como em microsserviços e implementações sem servidor).
  • Disponibilidade: Como as mensagens geralmente são armazenadas em mídia persistente, os sistemas orientados a eventos geralmente são mais fáceis de obter disponibilidade, especialmente porque normalmente são aplicativos não interativos. Os custos de disponibilidade podem vir de mensagens obsoletas e atrasos ilimitados no processamento de filas.
  • Segurança: Os sistemas orientados a eventos devem gerenciar a disponibilidade de dados independentemente de identidades e credenciais. Garantir que apenas determinados serviços ou processadores de mensagens tenham acesso a filas ou logs de mensagens específicos torna-se uma tarefa de tempo integral, à medida que dados cada vez mais diversos são explorados através do sistema.

Conclusão

A engenharia de software moderna requer o projeto de sistemas que sejam escaláveis, confiáveis, de fácil manutenção, disponíveis e seguros. Projetar sistemas distribuídos exige requisitos muito rigorosos porque a complexidade real dos sistemas modernos cresce com a demanda da sociedade por melhores serviços de software. Revisamos três padrões de projeto modernos para sistemas distribuídos e examinamos cinco aspectos de um sistema bem projetado.

Como engenheiros de software, somos responsáveis ​​por projetar sistemas que resolvam os principais problemas dos sistemas distribuídos modernos.
No próximo artigo desta série, escreverei sobre testes e seu papel na engenharia de software moderna.

Acho que você gosta

Origin blog.csdn.net/jeansboy/article/details/131703187
Recomendado
Clasificación