Na internet (service mesh), e aqui está mais um. Uau! Mas por quê? Porque quero expor minha opinião de que os service meshes deveriam ter surgido há 10 anos, antes do surgimento de plataformas de contêineres, como Docker e Kubernetes. Não estou afirmando que meu ponto de vista é melhor ou pior que os outros, mas como os service meshes são criaturas bastante complexas, a multiplicidade de perspectivas ajudará a compreendê-los melhor.
Vou falar sobre a plataforma dotCloud, que foi construída sobre mais de uma centena de microsserviços e suportou milhares de aplicativos em contêineres. Vou explicar os problemas que enfrentamos durante seu desenvolvimento e lançamento, e como os service meshes poderiam ajudar (ou não).
História da dotCloud
Já escrevi sobre a história da dotCloud e a escolha da arquitetura para essa plataforma, mas pouco falei sobre o nível de rede. Se você não quiser se aprofundar na leitura sobre a dotCloud, aqui está um resumo: é uma plataforma como serviço PaaS, permitindo que clientes executem uma ampla gama de aplicativos (Java, PHP, Python...), com suporte a uma ampla gama de serviços de dados (MongoDB, MySQL, Redis...) e com um fluxo de trabalho semelhante ao Heroku: você faz o upload do seu código na plataforma, ela cria as imagens dos contêineres e as implanta.
Vou explicar como o tráfego era direcionado para a plataforma dotCloud. Não porque isso fosse especialmente incrível (embora, para a sua época, o sistema funcionasse bem!), mas principalmente porque, com as ferramentas modernas, esse design pode ser facilmente implementado em pouco tempo por uma equipe modesta, caso precisem de um modo de roteamento de tráfego entre um monte de microsserviços ou uma série de aplicativos. Assim, podemos comparar as opções: o que acontece se você desenvolver tudo sozinho ou usar um service mesh existente. A escolha padrão: fazer você mesmo ou comprar.
Roteamento de tráfego para aplicativos hospedados
Os aplicativos na dotCloud podem fornecer endpoints HTTP e TCP.
Endpoints HTTP são adicionados dinamicamente à configuração do cluster de balanceadores de carga . É semelhante ao que hoje em dia os recursos fazem no Kubernetes e com um balanceador de carga como .
Os clientes se conectam aos endpoints HTTP através dos domínios correspondentes, desde que o nome de domínio aponte para os balanceadores de carga dotCloud. Nada de especial.
Endpoints TCP estão relacionados ao número da porta, que é então passado para todos os contêineres deste stack através de variáveis de ambiente.
Os clientes podem se conectar aos pontos finais TCP usando o nome de host correspondente (algo como gateway-X.dotcloud.com) e o número da porta.
Esse nome de host é resolvido para um cluster de servidores “nats“ (não tem relação com ), que roteará as conexões TCP de entrada para o contêiner certo (ou, no caso de serviços com balanceamento de carga, para os contêineres certos).
Se você está familiarizado com Kubernetes, provavelmente isso lhe lembrará dos serviços .
Na plataforma dotCloud não havia equivalente aos serviços : para simplificar, o acesso aos serviços ocorria da mesma forma tanto de dentro quanto de fora da plataforma.
Tudo foi organizado de maneira bastante simples: as implementações iniciais das redes de roteamento HTTP e TCP provavelmente consistiam em algumas centenas de linhas de Python. Algoritmos simples (eu diria, ingênuos) que foram aprimorados à medida que a plataforma cresceu e surgiram necessidades adicionais.
Um extenso refatoramento do código existente não foi necessário. Em particular, podem usar diretamente o endereço obtido através de variáveis de ambiente.
Como isso se diferencia de uma malha de serviços moderna?
Limitada visibilidade. Nós não tínhamos métricas para a malha de roteamento TCP. No que diz respeito à roteação HTTP, em versões posteriores surgiram métricas HTTP detalhadas com códigos de erro e tempos de resposta, mas as malhas de serviços modernas vão ainda mais longe, fornecendo integração com sistemas de coleta de métricas, como Prometheus, por exemplo.
A visibilidade é importante não apenas do ponto de vista operacional (para ajudar na resolução de problemas), mas também ao lançar novas funcionalidades. Falo sobre a implantação segura e .
A eficácia do roteamento também era limitado. Na rede de roteamento dotCloud, todo o tráfego precisava passar por um cluster de nós de roteamento dedicados. Isso significava potencial cruzamento de várias zonas de disponibilidade (AZ) e um aumento significativo na latência. Lembro-me de resolver problemas com um código que fazia mais de cem consultas SQL por página e, para cada consulta, abria uma nova conexão com o servidor SQL. Quando executado localmente, a página carrega instantaneamente, mas no dotCloud a carga leva vários segundos, porque cada conexão TCP (e consulta SQL subsequente) requer dezenas de milissegundos. Neste caso específico, a solução foi conexões persistentes.
Os serviços de mesh modernos lidam melhor com esses problemas. Primeiro, eles verificam se as conexões estão sendo roteadas na fonte. O fluxo lógico é o mesmo: cliente → mesh → serviço, mas agora o mesh opera localmente, e não em nós remotos, portanto, a conexão cliente → mesh é local e muito rápida (microsegundos em vez de milissegundos).
Além disso, as malhas de serviços modernas implementam algoritmos de balanceamento de carga mais inteligentes. Monitorando a saúde dos backends, podem direcionar mais tráfego para os backends mais rápidos, resultando em um aumento do desempenho geral.
Segurança também é melhor. A rede de roteamento dotCloud funcionava totalmente no EC2 Classic e não criptografava o tráfego (presumindo que, se alguém conseguisse instalar um sniffer no tráfego de rede do EC2, você já teria grandes problemas). As malhas de serviços modernas protegem transparentemente todo o nosso tráfego, por exemplo, com autenticação mútua TLS e criptografia subsequente.
Roteamento de tráfego para serviços de plataforma
Bem, discutimos o tráfego entre aplicativos, mas e a própria plataforma dotCloud?
A própria plataforma consistia em cerca de cem microsserviços responsáveis por várias funções. Alguns aceitavam requisições de outros, enquanto outros eram workers em segundo plano que se conectavam a outros serviços, mas não aceitavam conexões. De qualquer forma, cada serviço precisa conhecer os pontos finais dos endereços aos quais deve se conectar.
Muitos serviços de alto nível podem usar a rede de roteamento descrita acima. Na verdade, muitos dos mais de cem microserviços da dotCloud foram implantados como aplicativos regulares na própria plataforma dotCloud. No entanto, um pequeno número de serviços de baixo nível (particularmente aqueles que implementam essa rede de roteamento) precisava de algo mais simples, com menos dependências (já que para funcionar eles não podiam depender de si mesmos — o antigo problema do ovo e da galinha).
Esses serviços de baixo nível, mas cruciais, foram implantados ao executar contêineres diretamente em vários nós-chave. Nesse processo, não foram utilizados os serviços padrão da plataforma: compositor, programador e runner. Se você quiser comparar com as modernas plataformas de contêineres, isso se assemelha a executar o plano de controle com docker run diretamente nos nós, em vez de delegar a tarefa ao Kubernetes. Isso é bastante semelhante ao conceito de , que são utilizados pelo ou ao inicializar um cluster autônomo.
Esses serviços foram expostos de forma simples e rudimentar: em um arquivo YAML seus nomes e endereços foram listados; e cada cliente precisava pegar uma cópia desse arquivo YAML para implantar.
Por um lado, isso é extremamente confiável, pois não requer suporte a um armazenamento externo de chave/valor, como Zookeeper (não se esqueça, naquela época ainda não existiam etcd ou Consul). Por outro lado, isso dificultava a movimentação de serviços. Cada vez que um serviço era movido, todos os clientes precisavam obter um arquivo YAML atualizado (e potencialmente reiniciar). Não era muito conveniente!
Subsequentemente, começamos a implementar um novo esquema, onde cada cliente se conectava a um proxy local. Em vez de saber o endereço e a porta, bastava conhecer apenas o número da porta do serviço e se conectar através de localhost. O proxy local lida com a conexão e a direciona ao servidor real. Agora, ao mover o backend para outra máquina ou ao escalar, em vez de atualizar todos os clientes, apenas esses proxies locais precisam ser atualizados; e a reinicialização não é mais necessária.
(Além disso, tinha-se a intenção de encapsular o tráfego em conexões TLS e colocar um outro servidor proxy no lado receptor, além de verificar certificados TLS sem a participação do serviço receptor, que está configurado para aceitar conexões apenas em localhost).
Isso é muito parecido com da Airbnb, mas a diferença significativa é que o SmartStack foi implementado e implantado em produção, enquanto o sistema interno de roteamento da dotCloud foi colocado de lado quando a dotCloud se transformou no Docker.
Pessoalmente, considero o SmartStack um dos precursores de sistemas como Istio, Linkerd e Consul Connect, porque todos eles seguem um padrão:
- Executar um proxy em cada nó.
- Os clientes se conectam ao proxy.
- O plano de controle atualiza a configuração do servidor proxy ao alterar os backends.
- … Lucro!
A implementação moderna de service mesh
Se precisarmos implementar uma rede semelhante hoje, podemos usar princípios análogos. Por exemplo, configurar uma zona DNS interna, mapeando nomes de serviços para endereços no espaço 127.0.0.0/8. Em seguida, executar o HAProxy em cada nó do cluster, aceitando conexões em cada endereço de serviço (nesta sub-rede 127.0.0.0/8) e redirecionando/balanceando a carga nos respectivos backends. A configuração do HAProxy pode ser gerenciada , permitindo armazenar informações sobre o backend no etcd ou Consul e atualizar automaticamente a configuração no HAProxy quando necessário.
É mais ou menos assim que o Istio funciona! Mas com algumas diferenças:
- Utiliza em vez do HAProxy.
- Armazena a configuração do backend através da API do Kubernetes em vez de etcd ou Consul.
- Os serviços recebem endereços na sub-rede interna (endereços do Kubernetes ClusterIP) em vez de 127.0.0.0/8.
- Possui um componente adicional (Citadel) para adicionar autenticação mútua TLS entre cliente e servidores.
- Suporta novas funcionalidades, como circuit breaking, rastreamento distribuído, implantação de canary, entre outras.
Vamos brevemente examinar algumas diferenças.
Envoy Proxy
O Envoy Proxy foi desenvolvido pela empresa Lyft [concorrente da Uber no mercado de táxis – nota do tradutor]. Ele é em muitos aspectos semelhante a outros proxies (como HAProxy, Nginx, Traefik…), mas a Lyft criou o seu porque precisava de funcionalidades que estavam ausentes em outros proxies, e parecia mais sensato desenvolver um novo do que expandir o existente.
O Envoy pode ser usado por si só. Se eu tiver um serviço específico que precisa se conectar a outros serviços, posso configurá-lo para se conectar ao Envoy e, em seguida, configurar e reconfigurar dinamicamente o Envoy com a localização de outros serviços, obtendo assim muitas funcionalidades adicionais excelentes, como visibilidade. Em vez de uma biblioteca de cliente personalizada ou implementação de rastreamento em código, direcionamos o tráfego para o Envoy, que coleta métricas para nós.
Mas o Envoy também é capaz de funcionar como plano de dados (data plane) para um service mesh. Isso significa que, agora, para esse service mesh, o Envoy é configurado plano de controle (control plane).
O plano de controle
No plano de controle, o Istio depende da API do Kubernetes. Isso não é muito diferente de usar confd, que se baseia no etcd ou Consul para revisar um conjunto de chaves em um armazenamento de dados. O Istio revisa um conjunto de recursos do Kubernetes através da API do Kubernetes.
Enquanto isso: pessoalmente, achei útil esta , que diz:
O servidor da API do Kubernetes é um "servidor burro" que oferece armazenamento, gerenciamento de versões, validação, atualização e semântica dos recursos da API.
O Istio foi projetado para trabalhar com Kubernetes; e se você quiser usá-lo fora do Kubernetes, você precisará iniciar uma instância do servidor da API do Kubernetes (e do serviço auxiliar etcd).
Endereços de serviços
O Istio depende dos endereços ClusterIP que o Kubernetes atribui, portanto, os serviços do Istio recebem um endereço interno (não na faixa de 127.0.0.0/8).
O tráfego para o endereço ClusterIP de um serviço específico no cluster Kubernetes sem o Istio é interceptado pelo kube-proxy e enviado para a parte de trás desse proxy. Se você estiver interessado em detalhes técnicos, o kube-proxy estabelece regras de iptables (ou balanceadores de carga IPVS, dependendo de como foi configurado) para reescrever os endereços IP dos destinos das conexões que vão para o endereço ClusterIP.
Depois que o Istio é instalado no cluster Kubernetes, nada muda até que ele seja explicitamente ativado para um determinado consumidor ou até mesmo para todo o espaço de nomes, introduzindo um contêiner sidecar nos pods personalizados. Este contêiner executará uma instância do Envoy e estabelecerá uma série de regras de iptables para interceptar o tráfego que vai para outros serviços e redirecionar esse tráfego para o Envoy.
Ao integrar com o DNS do Kubernetes, isso significa que nosso código pode se conectar pelo nome do serviço e tudo "simplesmente funciona". Em outras palavras, nosso código faz requisições do tipo http://api/v1/users/4242, então api resolve a requisição para 10.97.105.48, as regras do iptables interceptam as conexões de 10.97.105.48 e as redirecionam para o proxy local Envoy, e esse proxy local direcionará a requisição para o backend API real. Ufa!
Decorações adicionais
O Istio também oferece criptografia e autenticação de ponta a ponta através do mTLS (mutual TLS). Isso é responsabilidade de um componente chamado Citadel.
Há também um componente Mixer, que o Envoy pode solicitar para cada requisição, a fim de tomar uma decisão especial sobre essa requisição com base em vários fatores, como cabeçalhos, carga do backend, etc... (não se preocupe: há muitos meios de garantir que o Mixer funcione, e mesmo que ele falhe, o Envoy continuará operando normalmente como proxy).
E, claro, mencionamos a observabilidade: o Envoy coleta uma enorme quantidade de métricas, garantindo ao mesmo tempo rastreamento distribuído. Na arquitetura de microserviços, se uma requisição API precisa passar pelos microserviços A, B, C e D, ao entrar no sistema, o rastreamento distribuído adicionará um identificador único à requisição e manterá esse identificador através de sub-requisições para todos esses microserviços, permitindo registrar todas as chamadas associadas, suas latências, etc.
Desenvolver ou comprar
O Istio tem a reputação de ser um sistema complexo. Em contrapartida, construir a malha de roteamento que descrevi no início deste post é relativamente simples usando as ferramentas existentes. Então, faz sentido criar nossa própria service mesh?
Se temos necessidades modestas (sem necessidade de observabilidade, interrupção da cadeia e outras sutilezas), surgem pensamentos sobre desenvolver nossa própria ferramenta. Mas se estivermos usando Kubernetes, talvez nem seja necessário, porque o Kubernetes já fornece ferramentas básicas para descoberta de serviços e balanceamento de carga.
Mas se tivermos requisitos avançados, então "comprar" uma service mesh parece ser uma opção muito melhor. (Isso nem sempre é realmente uma "compra", uma vez que o Istio é de código aberto, mas ainda precisamos investir tempo de engenharia para entender seu funcionamento, implantá-lo e gerenciá-lo).
O que escolher: Istio, Linkerd ou Consul Connect?
Até agora falamos apenas sobre Istio, mas este não é o único service mesh. Uma alternativa popular é , e ainda há o .
O que escolher?
Para ser honesto, não sei. Neste momento, não me considero suficientemente competente para responder a essa pergunta. Existem algumas comparando essas ferramentas e até mesmo .
Uma das abordagens promissoras é usar uma ferramenta como . Ela implementa uma camada de abstração para simplificar e unificar as APIs fornecidas pelos service meshes. Em vez de estudar as APIs específicas (e, na minha opinião, relativamente complexas) de vários service meshes, podemos usar construções mais simples do SuperGloo — e mudar facilmente de um para outro, como se tivéssemos um formato intermediário de configuração que descreve interfaces HTTP e backends, capaz de gerar a configuração real para Nginx, HAProxy, Traefik, Apache...
Brinquei um pouco com Istio e SuperGloo, e no próximo artigo quero mostrar como adicionar Istio ou Linkerd em um cluster existente usando SuperGloo, e como este último se sai em sua função, ou seja, permitindo trocar de um service mesh para outro sem reescrever configurações.
Fonte: habr.com
