BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Linkerd v2: como a adoção em produção serviu de lição para reescrever o Service Mesh

Linkerd v2: como a adoção em produção serviu de lição para reescrever o Service Mesh

Pontos Principais

  • O Linkerd 2.0 introduziu uma grande reescrita de código no service mesh, escrito anteriormente em Scala. Esse trabalho foi inspirado pelo Twitter que utilizou o sistema RPC Finagle.
  • Essa nova versão saiu da JVM para uma implementação dividida entre Go (para o plano de controle) e Rust (plano de dados).
  • A equipe da Buoyant fez altos investimentos tecnológicos no stack de rede do Rust, e concentrou a experiência de usuário na simplicidade, facilidade de uso e baixa sobrecarga cognitiva. O resultado foi dramaticamente mais rápido, mais leve e mais simples de operar.
  • Já faz seis meses desde o lançamento do Linkerd 2.0, e a equipe acredita que a reescrita pagou dividendos com os usuários que anteriormente não conseguiam adotar a versão 1.x e agora adotaram alegremente a 2.x.

O service mesh está se tornando uma parte crítica do stack cloud-native moderno. Movendo o mecanismo de comunicação entre serviços do código da aplicação para a camada de aplicação e fornecendo ferramentas para medir e manipular essa comunicação aos operadores e proprietários da plataforma com uma camada de visibilidade e controle muito necessária que é independente do código da aplicação.

Embora o termo "service mesh" tenha se tornado comum recentemente, os conceitos por trás dele não são novos, já que estão em produção há mais uma década em empresas como Twitter, Netflix e Google, geralmente em forma de bibliotecas do tipo "fat client" como Finagle, Hystrix e Stubby. Do ponto de vista técnico, a abordagem de proxy co-implantada moderna do service mesh é mais como um reempacotamento dessas idéias da biblioteca em forma de proxy, possibilitada pela rápida adoção de containers e orquestradores como Docker e Kubernetes.

A ascensão do service mesh começou com o Linkerd, sendo o projeto mais antigo a usar o termo. Lançado em 2016, o Linkerd atualmente apresenta duas linhas paralelas de desenvolvimento: o 1.x original, construído em "stack Twitter" de Scala, Finagle, Netty e JVM; e a versão 2.x, reconstruído a partir do zero em Rust and Go.

O lançamento do Linkerd 2.0 representou não apenas uma mudança na implementação, mas também na abordagem e a riqueza dos aprendizados com anos de experiência de produção. Este artigo discute esses aprendizados e como se tornaram a base da filosofia, design e implementação do Linkerd 2.0.

O que é Linkerd e por que devemos nos interessar?

O Linkerd é um service mesh open-source e projeto membro da Cloud Native Computing Foundation. Lançado em 2016, atualmente impulsiona a arquitetura de empresas em todo o mundo, desde startups como Strava e Planet Labs a grandes empresas como Comcast, Expedia, Ask e Chase Bank.

O Linkerd fornece observabilidade, confiabilidade e segurança para aplicações microservice. Crucialmente, essa funcionalidade é fornecida na camada de aplicação, isso significa que os recursos estarão disponíveis uniformemente a todos os serviços independentemente da implementação ou implantação, e são fornecidos aos proprietários da plataforma de uma maneira que é amplamente independente do roadmap ou escolhas técnicas dos desenvolvedores. Por exemplo, o Linkerd pode adicionar o TLS às conexões entre os serviços e permitir que o proprietário da plataforma configure a maneira como os certificados serão gerados, compartilhados e validados sem a necessidade de inserir o trabalho relacionado ao TLS nos roadmaps das equipes de desenvolvedores de cada serviço.

O Linkerd trabalha inserindo proxies TCPs transparentes nas camada 5/ camada 7 em seus serviços para que o operador escolha o mesh. Esses proxies formam o plano de dados do Linkerd e gerenciam todo o tráfego de entrada e saída dos serviços. O plano de dados é gerenciado pelo plano de controle, um conjunto de processos que fornece ao operador um único ponto para monitorar e manipular o tráfego que flui através dos serviços.

O Linkerd é baseado em uma realização fundamental, que o tráfego da requisição que flui através de uma aplicação microservice é tão parte da área operacional como o código da própria aplicação. Embora o Linkerd não possa examinar internamente um determinado microservice, pode relatar métricas de integridade de primeira linha observando a taxa de entrega, transferência e a latência das respostas. Da mesma forma, o Linkerd não pode modificar a lógica de tratamento de erros das aplicações, podendo melhorar a integridade do serviço, repetindo automaticamente as solicitações para instâncias com falha ou com atraso. O Linkerd também pode criptografar conexões, fornecer identidade de serviço criptograficamente segura, executar canaries e implantações blue/green, alterando as porcentagens de tráfego e assim por diante.

Linkerd 1.x

O Linkerd nasceu da nossa experiência em um dos maiores aplicativos de microservice do mundo, o Twitter. À medida que o Twitter migrou de um aplicativo Ruby on Rails de três camadas para uma arquitetura proto-cloud-native construída no Mesos e na JVM, criou-se uma biblioteca, a Finagle, que fornecia instrumentação, descoberta de serviços e muito mais. A introdução do Finagle foi uma parte crítica para permitir que o Twitter adotasse microservices em escala.

O Linkerd 1.x foi lançado em 2016 e foi construído no stack do Finagle, Scala, Netty e JVM, e testado diretamente no Twitter em produção. Nosso objetivo inicial era simplesmente fornecer a semântica poderosa da Finagle da forma mais ampla possível. Reconhecendo que o público de uma biblioteca Scala para chamadas RPC assíncronas era algo limitado, na melhor das hipóteses, agrupamos o Finagle em forma de proxy, permitindo que o código da aplicação fosse escrito em qualquer linguagem. Felizmente, a ascensão contemporânea de containers e orquestradores reduziu bastante o custo operacional da implantação de proxies ao lado de cada instância de serviço. Assim, o Linkerd ganhou força, especialmente na comunidade cloud-native, que estava adotando rapidamente tecnologias como Docker e Kubernetes.

Humildemente o Linkerd cresceu em conjunto com o service-mesh. Hoje, a versão 1.x do Linkerd está em funcionamento em empresas de todo o mundo e continua em constante desenvolvimento.

Lições aprendidas com o Linkerd

Apesar do sucesso do Linkerd, muitas organizações não estavam dispostas a implanta-lo em produção e as que estavam, tiveram que fazer grandes investimentos.

Esse atrito foi causado por diversos fatores. Algumas organizações estavam simplesmente relutantes em introduzir a JVM no ambiente operacional. A JVM possui uma área de superfície operacional particularmente complexa e algumas equipes de operações, correta ou incorretamente, evitam introduzir qualquer software baseado em JVM no próprio stack - especialmente uma que desempenha um papel de missão crítica como o Linkerd.

Outras organizações relutaram em alocar os recursos de sistema que Linkerd exigia. De um modo geral, o Linkerd 1.x era muito bom em aumentar o escalonamento, uma única instância podia processar dezenas de milhares de solicitações por segundo, com memória e CPU suficientes, mas não era boa em diminuir, era difícil obter a memória de uma única instância abaixo de um RSS de 150 MB. Scala, Netty e Finagle pioraram ainda mais o problema, pois todos foram projetados para maximizar a taxa de transferência em ambientes ricos em recursos, à custa da memória.

Como uma organização pode implantar centenas ou milhares de proxies do Linkerd, isso é um fato importante. Como alternativa, recomendamos que os usuários implantem o plano de dados por host e não por processo, permitindo que os usuários amortizem melhor o consumo dos recursos. Entretanto, isso adicionou complexidade operacional, e limitou a capacidade do Linkerd fornecer certas funcionalidades como por exemplo fornecimento de certificados TLS.

(As JVMs mais recentes melhoraram significativamente esses números. O uso de recursos e a latência do Linkerd 1.x são bastante reduzidas no OpenJ9 da IBM, e o GraalVM da Oracle promete reduzi-lo ainda mais.)

Finalmente, houve a questão da complexidade. O Finagle era uma biblioteca rica com um grande conjunto de recursos e mostramos muitos desses recursos mais ou menos diretamente ao usuário por meio de um arquivo de configuração. Como resultado, o Linkerd 1.x era personalizável e flexível, mas tinha uma curva de aprendizado acentuada. Um erro de design em particular foi o uso de tabelas delegadas (dtabs), uma linguagem de roteamento de retorno, hierárquica e de preservação de sufixo usada pelo Finagle, como uma configuração fundamental. Qualquer usuário que tentasse personalizar o comportamento do Linkerd encontraria rapidamente os dtabs e teria que fazer um investimento mental significativo antes de poder prosseguir.

O Novo Começo

Apesar do crescente nível de adoção, ao final de 2017 estávamos convencidos que precisávamos rever nossa abordagem, e ficou claro que as propostas de valores estavam corretas, mas os requisitos que impunham às equipes operacionais eram desnecessários. À medida que refletimos sobre nossa experiência em ajudar as organizações a adotá-lo, decidimos por alguns princípios-chave sobre como deveria ser o futuro do Linkerd:

  1. Requisitos mínimos de recursos. O Linkerd deve impor o mínimo custo possível de desempenho e de recursos, principalmente na camada de proxy.
  2. Apenas funcione. O Linkerd não deve interromper as aplicações xistentes, nem exigir uma configuração complexa só para começar.
  3. Simples. O Linkerd deve ser operacionalmente simples com baixa sobrecarga cognitiva. Os usuários devem encontrar os componentes de maneira clara e o comportamento deve ser compreensível.

Cada um desses requisitos apresentou o próprio conjunto de desafios. Para diminuir os requisitos do sistema, ficou claro que precisaríamos sair da JVM. Para "apenas funcionar", precisaríamos investir em técnicas complexas, como a detecção de protocolo de rede. Finalmente, para ser simples, o requisito mais difícil, precisaríamos priorizar explicitamente o minimalismo, a incrementabilidade e a introspecção a todo momento.

Durante a reescrita, percebemos que deveríamos ter focado em um caso de uso inicial concreto. Como ponto de partida, decidimos nos concentrar puramente nos ambientes Kubernetes e nos protocolos comuns de HTTP, HTTP/2 e gRPC, enquanto entendemos que mais tarde precisaríamos expandir todas essas restrições.

Objetivo 1: Requisitos mínimos

No Linkerd 1.x, o plano de controle e de dados foram gravados na mesma plataforma (a JVM). No entanto, os requisitos do produto para essas duas peças são bem diferentes. O plano de dados, implantado ao lado de todas as instâncias de cada serviço e manipulando todo o tráfego para esse serviço deve ser o mais rápido e o menor possível. Mais do que isso, deve ser seguro, os usuários do Linkerd confiam nele com informações incrivelmente sensíveis, incluindo dados sujeitos aos regulamentos de conformidade com PCI e HIPAA.

Por outro lado, o plano de controle, implantado ao lado e não no caminho crítico para solicitações, possui requisitos de velocidade e recursos mais tranquilos. Aqui, o mais importante era se concentrar na extensibilidade e facilidade de interação.

Desde o início, ficou claro que Go era a linguagem correta para plano de controle. Embora o Go tivesse uma runtime gerenciada e um garbage collector como a JVM, foram ajustados para serviços de rede modernos e não impuseram nem uma fração de custo que vimos na JVM. O Go também teve ordens de magnitude menos complexas em termos operacionais e em seus binários estáticos do que na JVM, e o espaço ocupado na memória e tempo de inicialização foram uma melhoria bem-vinda. Embora nossos benchmarks mostrassem que o Go ainda era mais lento que os idiomas compilados nativamente, era rápido o suficiente para o plano de controle. Por fim, o excelente ecossistema de bibliotecas do Go nos deu acesso a diversas funcionalidades existentes em torno do Kubernetes e sentimos que a barreira da linguagem está bem baixa o que incentiva a entrada e popularização do código aberto.

Embora considerássemos o Go e o C++ para o plano de dados, ficou claro desde o início que o Rust era a única linguagem que atendia nossos requisitos. O foco do Rust é segurança, especialmente seu poderoso borrow checker, que praticamente força a economia de memória em tempo de compilação, permitindo evitar toda uma classe de vulnerabilidades de segurança relacionadas à memória, tornando-a muito mais atraente do que o C++. Sua capacidade de compilar para código nativo e o controle refinado do gerenciamento de memória proporcionaram uma vantagem significativa de desempenho em relação ao Go e o melhor controle de utilização de memória. Já o Rust, possui uma linguagem rica e expressiva que atraiu os programadores do Scala, e o modelo de abstrações de custo zero sugeria que (ao contrário da Scala) poderíamos fazer uso dessa expressividade sem sacrificar a segurança ou o desempenho.

O Rust sofreu uma grande desvantagem, por volta de 2017. Seu ecossistema de bibliotecas ficou significativamente atrás das outras linguagens. Sabíamos que a escolha do Rust também exigiria um grande investimento em bibliotecas de redes.

Objetivo 2: Apenas Funcione

Com as opções de tecnologia subjacentes definidas, nos mexemos para satisfazer o próximo objetivo do projeto: que apenas funcionasse. Para aplicações Kubernetes, isso significava que adicioná-lo a uma aplicação em funcionamento pré-existente não poderia causar a parada e nem exigir o mínimo de configuração.

Fizemos várias mudanças de design para chegar ao objetivo. Projetamos os proxies para que pudessem detectar o protocolo, podendo fazer o proxy do tráfego TCP, mas poderiam detectar automaticamente o protocolo da camada 7. Combinado com a reconexão do iptables no momento da criação do pod, isso significava que o código do aplicativo que fazia qualquer conexão TCP teria a conexão de proxy transparente por meio da instância local, e se essa conexão usasse HTTP, HTTP/2 ou gRPC, o Linkerd alteraria automaticamente seu comportamento para a semântica da camada 7, por exemplo, relatando taxas de sucesso, repetindo solicitações idempotentes, balanceando a carga no nível da solicitação, etc. Tudo isso foi feito sem a necessidade de configuração do usuário.

Investimos em fornecer o máximo de funcionalidades diferentes. Embora o Linkerd 1.x forneça métricas avançadas por proxy, deixou de agregar os relatórios dessas métricas para o usuário. No Linkerd 2.0, empacotamos uma pequena instância do Prometheus e com limite de tempo, como parte do plano de controle, para que pudéssemos fornecer métricas agregadas no dashboard do Grafana. Usamos essas métricas para alimentar um conjunto de comandos no estilo UNIX que permitem aos operadores observar o comportamento do serviço ao vivo na linha de comando. Combinado com a detecção de protocolo, isso significa que os operadores podem obter métricas ricas em nível de serviço do Linkerd imediatamente, sem nenhuma configuração complexa.

Fig. 1: As conexões TCP de entrada e saída de/para são aplicações de instâncias roteadas automaticamente pelo plano de dados do Linkerd ("Linkerd-proxy"), que por sua vez é monitorado e gerenciado pelo painel de controle.

Objetivo 3: Simplicidade

Esse era o objetivo mais importante, embora estivesse em tensão com o objetivo da facilidade de uso. Somos gratos à palestra de Rich Hickey sobre simplicidade versus facilidade por clarear nosso pensamento sobre esse assunto. Sabíamos que o Linkerd era um produto voltado para o operador, ou seja, em oposição a uma service mesh que um provedor cloud opera, portanto esperamos que nós operemos. Isso significava que, minimizar a área de superfície operacional de Linkerd era uma preocupação primordial. Felizmente, nossos esforços de ajudar as empresas a adotar o Linkerd 1.x nos deram idéias concretas sobre o que isso implicaria:

  • O Linkerd não podia esconder o que estava fazendo ou se mostrar extremamente mágico;
  • O estado do Linkerd deve ser inspecionável internamente;
  • Os componentes do Linkerd devem ser bem definidos, discretos e claramente demarcados.

Fizemos várias mudanças no serviço para alcançar esse objetivo. Em vez de unificar o plano de controle em um único processo monolítico, o dividimos em seus limites naturais: um serviço web alimenta a interface do usuário web, um serviço de API de proxy lida com a comunicação com o plano de dados e assim por diante. Expusemos esses componentes diretamente ao usuário no dashboard do Linkerd e com isso tanto o dashboard e a CLI UX se encaixaram nos idiomas e as expectativas de comando do ecossistema Kubernetes: o linkerdinstall emite um manifesto do Kubernetes no formulário YAML para ser aplicado ao cluster via kubectl quando se aplica, o dashboard do Linkerd se parece com o painel do Kubernetes e assim por diante.

Também evitamos a complexidade para exercer restrições. Operamos com o essencial do Kubernetes, como deployments e pods, em vez de introduzir nossas próprias definições do que é um "serviço". Construímos usando recursos existentes no Kubernetes como secrets e admission controllers sempre que possível. Minimizamos o uso de definições de recursos personalizadas porque sabemos que adicionam complexidade significativa ao cluster. E assim por diante.

Por fim, adicionamos diagnósticos abrangentes, permitindo que os operadores inspecionem o estado interno do Linkerd e validem as expectativas. Combinamos o plano de controle (ou seja, cada pod do plano de controle possui um side-car do plano de dados que copia todo o tráfego de e para ele), permitindo que os operadores usem a valiosa telemetria do Linkerd para entender e monitorar o estado, da mesma forma que as aplicações. Adicionamos comandos como linkerdendpoints, que despejam as informações internas de descoberta de serviço do Linkerd e linkerdcheck, que verifica se todos os aspectos de um cluster Kubernetes e de uma instalação do Linkerd estão operando conforme o esperado.

Em resumo, fizemos o possível para tornar o Linkerd explicititamente observável, em vez de algo fácil e mágico.

Fig. 2: O dashboard do Linkerd 2.0 imita a aparência do painel do Kubernetes, desde o tratamento visual até a navegação.

O Linkerd 2.0 hoje

Lançamos o Linkerd 2.0 em setembro de 2018, aproximadamente um ano após o início interno dos esforços. Embora as proposições de valor sejam basicamente as mesmas, nosso foco é na facilidade do uso, simplicidade operacional e requisitos mínimos de recursos que resultou em um produto substancialmente diferente da versão 1.x. Seis meses depois, essa abordagem pagou as nossas dívidas, com os usuários que anteriormente eram incapazes de adotar a versão 1.x e agora adotaram a versão 2.x.

A escolha do Rust despertou um interesse significativo, embora tenha sido originalmente uma espécie de aposta (na verdade, lançamos uma versão anterior com o nome "Conduit", com medo de manchar a marca Linkerd), e a aposta valeu a pena. Desde 2017, fizemos investimentos significativos nas principais bibliotecas da rede Rust, como Tokio, Tower e Hyper. Ajustamos o proxy do Linkerd 2.0 (chamado simplesmente de, "linkerd2-proxy") para liberar eficientemente a memória alocada a uma solicitação quando a solicitação é finalizada, permitindo distribuições de latência incrivelmente nítidas à medida que a alocação e a desalocação de memória são amortizadas através do fluxo de solicitação. Os proxies do Linkerd agora apresentam uma latência de p99, inferior a um milissegundo e uso de memória menor de 10 MB, uma ordem de magnitude muito menor que o Linkerd 1.x.

Hoje, o Linkerd tem uma comunidade próspera, e o futuro do projeto é brilhante. Com mais de 50 colaboradores na branch 2.x, e com lançamentos semanais além do canal no Slack bem ativo e amigável da comunidade, estamos orgulhosos de nossos esforços e esperamos continuar resolvendo os desafios da vida real para nossos usuários, mantendo-nos fiéis ao nosso design e filosofia de simplicidade, facilidade de uso e requisitos mínimos de recursos.

Sobre o Autor

William Morgan é co-fundador e CEO da Buoyant, uma startup focada na construção de tecnologia service-mesh open source para ambientes cloud-native. Antes da Buoyant, era engenheiro de infraestrutura no Twitter, onde ajudou a migrar o Twitter de uma aplicação Ruby on Rails monolítica com falhas, para uma arquitetura de microservice tolerante a falhas e altamente distribuída. Era engenheiro de software na Powerset, Microsoft e Adap.tv, cientista de pesquisa do MITRE, e possui um Mestrado em ciência da computação pela Stanford University.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT