Cloud Computing

20 out, 2016

Zuul 2: a jornada da Netflix para sistemas assíncronos não bloqueantes

Publicidade

Provavelmente você não tenha notado, mas recentemente a Netflix fez uma grande mudança de arquitetura no Zuul,  seu gateway de nuvem. O Zuul 2 faz a mesma coisa que seu antecessor, atuando como porta de entrada para a infraestrutura de servidores da Netflix, bem como gerenciando o tráfego de todos os usuários da Netflix por todo o mundo. Além disso, é responsável por direcionar os pedidos, dar suporte para testes e depuração dos desenvolvedores, fornecer uma visão profunda sobre o status geral do serviço, proteger a Netflix de ataques e direcionar os canais de tráfego para outras regiões quando ocorre algum problema com a sua infra AWS.

A principal diferença entre a arquitetura do Zuul 2 e o original é que o Zuul 2 está sendo executado assincronamente e sem bloqueio de framework, usando o Netty.  Depois de estar em produção durante os últimos meses, a principal vantagem é que ele fornece a capacidade para dispositivos e navegadores web terem conexões persistentes com os servidores Netflix. Imagine o desafio que é escalar mais de 83 milhões de usuários, cada um com vários dispositivos conectados. Através de uma conexão persistente com a infraestrutura de nuvem da Netflix, pode-se ativar muitos recursos e inovações de produtos interessantes, reduzir solicitações gerais do dispositivo, melhorar o seu desempenho, e entender e depurar melhor a experiência do cliente.  Também era esperado que o Zuul 2 oferecesse benefícios de resiliência e melhorias de desempenho, em termos de latência, taxa de transferência e custos.

Diferenças entre sistemas com bloqueio e sem bloqueio

Para entender por que a Netflix construiu o Zuul 2, você deve primeiro entender as diferenças entre arquitetura de sistemas assíncronos e não-bloqueantes, e sistemas bloqueantes e multithreaded, tanto na teoria como na prática.

O Zuul 1 foi construído sobre o framework Servlet, que implementa sistemas bloqueantes e multithreaded, o que significa que eles processam solicitações usando uma thread por conexão. Operações I/O são feitas pela escolha de um worker a partir de um pool de threads para executar o I/O, e a thread solicitada é bloqueada até que o trabalho do worker esteja concluído. O worker notifica a thread de solicitação quando o seu trabalho está completo. Isso funciona bem com instâncias modernas multi-core da AWS manipulando centenas de conexões simultâneas cada. Mas quando as coisas dão errado, por exemplo quando há um aumento de latência do back-end ou tentativas recorrentes de conexão de dispositivos devido a falhas, a contagem de conexões ativas e threads aumenta. Quando isso acontece, os nós entram em apuros e podem entrar em um ciclo mortal, no qual threads em backup dão pico de cargas no servidor e sobrecarregam o cluster. Para compensar esses riscos, a empresa construiu mecanismos de aceleração e bibliotecas (por exemplo, Hystrix) para ajudar a manter seus sistemas de bloqueio estáveis durante esses eventos.

netflix-1Arquitetura de sistema multithreaded

Sistemas assíncronos funcionam de forma diferente, geralmente com uma thread por núcleo da CPU manipulando todos os pedidos e respostas. O ciclo de vida do pedido e da resposta é tratado através de eventos e retornos de chamada. Como não há uma thread para cada pedido, o custo das conexões é baixo. Esse é o custo de um descritor de arquivo, e da adição de um ouvinte, considerando que o custo de uma ligação no modelo de bloqueio é uma thread com uma memória pesada e sobrecarga do sistema. Há alguns ganhos de eficiência, pois os dados permanecem na mesma CPU, fazendo melhor uso de caches de nível de CPU e exigindo menos trocas de contexto. A queda da latência do backend e a “tempestade de repetições” (clientes e dispositivos repetindo requisições quando ocorrem problemas) também são menos estressantes no sistema porque conexões e aumento de eventos na fila são muito menos custosos do que acumular threads.

netflix-2Arquitetura de sistema assíncrono e não-bloqueante

Sistemas bloqueantes são fáceis de grokar e depurar. Uma thread está sempre fazendo uma única operação para a pilha de thread; e uma thread descartada pode ser lida para seguir um pedido abrangendo vários segmentos seguindo os bloqueios. Uma exceção lançada só aparece na pilha. Um manipulador de exceção “pega-tudo” pode limpar tudo que não foi capturado de forma explícita.

Sistemas assíncronos, pelo contrário, retornam uma chamada com base em um ciclo de eventos. Nesse caso, o rastreamento de pilha do ciclo de eventos não faz sentido quando se tenta procurar uma requisição. É difícil rastrear uma requisição enquanto eventos e retornos de chamada são processados, e as ferramentas para ajudar com a depuração disso são extremamente fracas nessa área. Casos extremos, exceções não tratadas e mudanças de estado mal manuseadas criam recursos soltos, resultando em vazamentos ByteBuf, vazamentos de descritor de arquivo, respostas perdidas etc. Esses tipos de problemas têm provado ser bastante difíceis de depurar, porque é difícil saber qual evento não foi tratado ou limpo adequadamente.

A construção do Zuul não-bloqueante

Muitos serviços dentro do ecossistema Netflix foram construídos com um pressuposto bloqueante. As principais bibliotecas de redes da Netflix também foram construídas com a arquitetura bloqueante; muitas bibliotecas dependem de threads locais para serem construídas e armazenam o contexto sobre o pedido. Variáveis de thread locais não funcionam no mundo assíncrono de não bloqueio, onde múltiplas requisições são processadas na mesma thread. Consequentemente, grande parte da complexidade na construção do Zuul 2 foi identificar cantos escuros onde as variáveis de thread locais estavam sendo usadas. Outros desafios envolvidos foram converter a lógica de rede bloqueada em código de rede não bloqueada, encontrar o código de bloqueio dentro de bibliotecas e consertar vazamentos de recursos e conversão de infraestrutura básica para executar de forma assíncrona. Não há uma estratégia única para a conversão da lógica de bloqueio de rede para assíncrona; elas devem ser analisadas e refatoradas individualmente. O mesmo se aplica às principais bibliotecas da Netflix, nas quais algum código foi modificado e alguns tiveram que ser forkados e reformulados para trabalhar de forma assíncrona. O projeto de código aberto Reactive-Audit foi útil para a instrumentação dos servidores da Netflix para descobrir casos em que blocos de código e bibliotecas estavam bloqueados.

Foi adotada uma abordagem interessante para a construção do Zuul 2. Como os sistemas de bloqueio podem executar código de forma assíncrona, primeiro foram mudados os Filtros Zuul e o código de encadeamento de filtro para executar de maneira assíncrona.  Filtros Zuul contêm a lógica específica que foi criada para fazer as funções de gateway (roteamento, log, proxy reverso, prevenção de DDos etc). Foram reformulados o núcleo Zuul, as classes de filtro Zuul da base, e os Filtros Zuul usando RxJava para permitir executar de forma assíncrona. Agora o Zuul possui dois tipos de filtros que são usados em conjunto: assíncrono, utilizado para operações de I/O, e um filtro de sincronização que executa operações lógicas que não exigem I/O. Os filtros assíncronos do Zuul permitem executar a mesma lógica exata do filtro em ambos os sistemas de bloqueio e os sistemas sem bloqueio.  Isso dá a capacidade de trabalhar com um conjunto de filtros somente para que se pudesse desenvolver recursos de gateway para os parceiros da Netflix e, ao mesmo tempo, desenvolver a arquitetura baseada em Netty em uma única base de código. Com os filtros Zuul assíncronos no lugar, a construção do Zuul 2 era “apenas” uma questão de fazer o resto da infraestrutura Zuul executar de forma assíncrona e não-bloqueante. Os mesmos Filtros Zuul poderiam ser usados em ambas as arquiteturas.

Os resultados do Zuul 2 em produção

As hipóteses variaram muito sobre os benefícios da arquitetura assíncrona no gateway da Netflix. Alguns pensavam que iria-se ver uma ordem de magnitude no aumento da eficiência devido à redução da troca de contexto e ao uso mais eficiente de caches de CPU, e outros esperavam que não se veria nenhum ganho de eficiência no final. Opiniões também variavam quanto à complexidade do esforço da mudança e desenvolvimento.

Então, qual o real ganho de se fazer essa mudança arquitetural? E valeu a pena? Isso é muito debatido. A equipe de gateway na nuvem foi a pioneira no esforço para criar e testar serviços assíncronos na Netflix. Houve um grande interesse em compreender como microsserviços usando assíncrono iriam funcionar na Netflix, e o Zuul parecia um serviço ideal para ver os benefícios.

Enquanto não era visto um benefício significativo na eficiência na migração para assíncrono e não-bloqueante, foram alcançadas as metas de escala na conexão. O Zuul traz benefício diminuindo consideravelmente o custo de conexões de rede que permitirão empurrar e comunicação bidirecional entre dispositivos. Essas características permitirão mais inovações para o usuário na experiência em tempo real e reduzirão os custos globais de nuvem, substituindo protocolos “chatty” de dispositivos hoje (que representam uma parte significativa do tráfego da API) para notificações push. Há também alguma vantagem resiliente em lidar com repetidas requisições e latência de sistemas de origem melhor do que o modelo de bloqueio. Eles continuam a aprimorar essa área, mas deve ser destacado que as vantagens de resiliência não foram alcançadas de forma fácil ou sem esforço e ajustes.

Com a capacidade de deixar a lógica de negócio core do Zuul em qualquer arquitetura bloqueante ou qualquer arquitetura assíncrona, há uma interessante comparação. Então, como dois sistemas fazendo exatamente o mesmo trabalho real, embora de maneiras muito diferentes, se comparam em termos de características, desempenho e resiliência?  Depois de executar o Zuul 2 em produção nos últimos meses, a avaliação da Netflix é que quanto mais vinculado à CPU um sistema é, menor é o ganho de eficiência.

A Netflix hoje possui vários clusters Zuul diferentes que fazem frente para serviços de origem como API, playback, website e log. Cada serviço de origem exige que as diferentes operações sejam manuseadas pelo cluster Zuul correspondente. O cluster Zuul que faz frente com o serviço de API, por exemplo, realiza a maioria dos trabalhos on-box de todos os grupos, incluindo cálculos de métricas, log e descriptação de cargas recebidas e compressão de respostas. Não se viu qualquer ganho de eficiência ao trocar um Zuul 2 assíncrono para um de bloqueio para esse cluster. Do ponto de vista de capacidade e CPU, eles são essencialmente equivalentes, o que faz sentido, dado o consumo intensivo de CPU que o serviço Zuul de frente de API tem. Eles também tendem a degradar na mesma taxa de transferência por nó.

O cluster Zuul que faz frente aos serviços de log da Netflix tem um perfil de desempenho diferente. O Zuul geralmente recebe mensagens de registro e análise de dispositivos e faz muitas escritas, então as requisições são grandes, mas as respostas são pequenas e não criptografadas pelo Zuul. Como resultado, o Zuul faz muito menos trabalho para esse cluster. Ainda sobre CPU-bound, viu-se um aumento de aproximadamente 25% na taxa de transferência correspondente a uma redução de 25% na utilização da CPU, executando o Zuul baseado em Netty.  Assim, pode-se observar que quanto menos trabalho um sistema realmente faz, mais eficiência se ganha com o assíncrono.

No geral, o valor que se ganhou a partir dessa mudança de arquitetura foi alto, com conexão de escalabilidade sendo o principal benefício, mas ele vem com um custo. O novo um sistema é muito mais complexo para debug, codificar e testar, e trabalha dentro de um ecossistema na Netflix que opera em uma suposição de sistemas bloqueantes. É pouco provável que o ecossistema mude tão cedo, por isso, à medida que forem adicionados e integrados mais recursos para o sistema, é provável que seja mais eficaz utilizar threads locais e outros pressupostos bloqueantes nas bibliotecas do cliente e outros códigos de apoio. Será necessário também reescrever chamadas bloqueantes de forma assíncrona.  Esse é um desafio de engenharia único, o de trabalhar com uma plataforma bem estabelecida e com código que faz suposições bloqueantes.

A Netflix está no processo de liberação de Zuul 2 como open source. Possivelmente serão adicionadas novas funcionalidades, tais como http/2 e suporte para websocket no Zuul 2 para que a comunidade também possa se beneficiar dessas inovações.

***

Fonte: http://techblog.netflix.com/2016/09/zuul-2-netflix-journey-to-asynchronous.html