Serviços de construção no Airbnb, parte 3

No terceiro post de nossa série sobre desenvolvimento de serviços de dimensionamento, mergulhamos nas práticas de engenharia de resiliência incorporadas na plataforma de serviços padrão que impulsiona a nova Arquitetura Orientada a Serviços no Airbnb.

Liang Guo Blocked Unblock Seguir Seguindo 11 de dezembro de 2018

Por: Weibo Ele , Liang Guo

Um brilhante e espaçoso Airbnb Plus listagem em Londres

Um irbnb está mudando sua infraestrutura para uma Arquitetura Orientada a Serviços. Uma plataforma de serviços poliglota confiável, eficiente e favorável ao desenvolvedor é um componente subjacente na evolução arquitetônica do Airbnb. Na Parte 1 e na Parte 2 de nossa série Building Services , compartilhamos como usamos a estrutura de serviço centrada no serviço Thrift IDL para dimensionar o desenvolvimento de serviços; como uma plataforma de serviços padronizada incentiva e reforça os padrões de infraestrutura; e como aplicar as melhores práticas para todos os novos serviços sem incorrer em sobrecarga de desenvolvimento adicional.

A arquitetura orientada a serviços cultiva a propriedade e aumenta a velocidade de desenvolvimento. No entanto, impõe um novo conjunto de desafios. A complexidade do sistema de serviços distribuídos é muito maior que a dos aplicativos monolíticos, e muitas técnicas que costumavam funcionar na arquitetura monolítica não são mais suficientes. Neste post, compartilhamos como construímos a engenharia de resiliência nos padrões da plataforma de serviços e ajudamos os proprietários de serviços a melhorar a disponibilidade de seus serviços.

Resiliência é um requisito, não um recurso

Na arquitetura de serviços distribuídos, a resiliência de serviços é um requisito difícil. A capacidade de cada serviço de responder e evitar o tempo de inatividade diminui à medida que aumenta a complexidade da comunicação entre serviços. Como exemplo, o PDP (Página de Detalhes do Produto) do Airbnb Homes precisa buscar dados de 20 serviços downstream. Supondo que não tenhamos tomado medidas para melhorar nossa resiliência, se cada um desses 20 serviços dependentes tivesse 99,9% de disponibilidade, nossa página de PDP teria apenas 98,0% de tempo de atividade, ou cerca de 14,5 horas de inatividade por mês.

Nada torna mais claro o impacto da resiliência do que a análise da causa-raiz incidente da produção no mundo real. Nestas amostras de post-mortem de incidentes de produção anteriores, podemos ver alguns problemas comuns de confiabilidade.

Vimos várias ondas de aumento de tráfego (tanto externas quanto compostas por novas tentativas em falhas de solicitação). Isso resultou em um período sustentado de disponibilidade reduzida de nosso serviço OAuth da perspectiva do Monorail. Isso afetou a maioria das APIs envolvidas nos fluxos de login e resultou em problemas de autenticação em todo o site para os usuários, afetando assim as principais métricas de negócios. As taxas de erro do monotrilho aumentaram para 25% inicialmente durante um período de 5 minutos e novamente para 5% durante um período de 36 minutos. Erros de API aumentaram significativamente na forma de 500 erros aumentados durante as interrupções. Isso foi visto na forma de uma queda nas taxas de impressão para P2 / P3, mensagens criadas e outras métricas de negócios. "

À medida que cada caixa de serviço atingiu seu limite, ele passou a exceder o tempo limite e fez com que o serviço P2 upstream tentasse novamente. Tentativas agressivas do serviço P2 rapidamente dobraram seu tráfego para o serviço. Como resultado, o serviço tentou criar muitas conexões de proxy, todas com tempo esgotado. A latência de todos os backends de serviço aumentou, causando falhas na verificação de integridade. Eventualmente, todas as caixas do serviço P2 estão marcadas . ”

Durante o tempo, vimos um pico de QPS de 10 a 20x e a latência da solicitação de serviço aumentou mais de 10 vezes na P99. Consequentemente, os clientes a montante sofreram falhas de solicitação. O serviço usa um algoritmo de hashing que é computacionalmente intensivo, e achamos que a causa raiz foi o aumento de solicitações causado por um aumento acentuado da utilização da CPU, que privou os recursos de outras solicitações e fez com que os relatórios de integridade marcassem nós de serviço individuais como não íntegros. Essa alteração localizada pode ter efeitos em cascata no restante da frota, já que outro nó teria que assumir mais tráfego. "

Os leitores que lidaram com sistemas e incidentes semelhantes devem ter notado alguns problemas comuns de confiabilidade: picos de solicitações , sobrecarga do sistema , esgotamento de recursos do servidor , repetição agressiva , falhas em cascata . No passado, medidas comuns tolerantes a falhas, como tempos limite de solicitação, tentativas com backoff exponencial e disjuntores, foram implementadas de forma inconsistente. Nossa plataforma de serviços padronizada tem práticas de engenharia de resiliência consistentemente construídas em todos os serviços para reduzir a possibilidade de que qualquer serviço único seja um link fraco para a disponibilidade do site.

O que nós construímos

Um serviços ll sobre o fluxo de reserva núcleo do Airbnb deve suportar alto rendimento e ser tolerante a falhas de falhas transitórias. Os engenheiros de produto que trabalham nos principais serviços de negócios devem se concentrar apenas na criação de novos recursos, não na implementação desses requisitos. As medidas de resiliência que implementamos são padrões bem conhecidos e já impediram o tempo de inatividade no fluxo de reserva principal.

Processamento de solicitação de servidor assíncrono

A maioria dos nossos principais serviços de fluxo de reserva são serviços Java. Na Parte 1 , explicamos como ampliamos a estrutura de serviços da Web Dropwizard para o desenvolvimento de serviços Java. Na maioria dos serviços do Airbnb, o processamento de solicitações envolve várias chamadas RPC para serviços ou bancos de dados downstream. A versão do Dropwizard que estávamos usando tinha um processamento de solicitação síncrona, no qual os threads do servidor aguardavam inativos enquanto o trabalho intensivo de E / S era processado pelos pools de encadeamentos do trabalhador. Como resultado, nossos serviços eram suscetíveis a subutilização de recursos ao aguardar a E / S da rede, incapazes de absorver tráfego em rajadas e vulneráveis a sobrecargas de solicitações ou tentativas de tempestades.

Aproveitando o recurso de resposta assíncrona na última estrutura do Dropwizard, criamos um fluxo de processamento de solicitação assíncrono de ponta a ponta que permite a gravação de aplicativos altamente não simultâneos e não bloqueantes. Ele conecta resposta assíncrona Dropwizard / Jersey 2, nossa estrutura interna de carregador de dados assíncrono e clientes IDL de serviço assíncrono para comunicação entre serviços. O processamento de solicitação assíncrona tem alguns benefícios principais:

  • Maior taxa de transferência: os principais threads de E / S enviam solicitações ao executor async e retornam para aceitar novas solicitações de entrada. Menos encadeamentos de E / S podem manipular mais solicitações simultâneas e absorver melhor o tráfego espinhoso.
  • Prevenção de inanição: um serviço pode ter alguns endpoints com uso intensivo de I / O ou CPU. Se um nó receber muitas solicitações para esses endpoints caros, ele deixará passar solicitações simples e rápidas ao tentar lidar com as mais caras. Com o processo de solicitação assíncrona, os principais encadeamentos de E / S estão disponíveis para aceitar todas as solicitações e despachá-las para solicitar filas de diferentes pesos. Solicitações caras e baratas podem ser executadas simultaneamente.

Com o processamento de solicitação assíncrona, um servidor geralmente usa menos encadeamentos e tem melhor latência de cauda quando está sob carga.

Solicitar enfileiramento

Cada servidor de back-end possui um limite para o número de solicitações que pode manipular dentro de seus objetivos de nível de serviço definidos para latência. Esse limite é uma função de recursos limitados: CPU, memória, largura de banda de rede, descritores de arquivos, fila de disco, etc. Para a maioria dos serviços na Airbnb, o padrão típico de carga de trabalho envolve dados buscando chamadas para serviços e bancos de dados downstream; aplicação de lógica leve para calcular dados derivados; e compondo uma resposta dos dados obtidos e derivados. Em outras palavras, a maioria dos serviços são ligados a E / S de rede, não vinculados a computação e, portanto, são mais propensos a serem limitados pela memória. Para entender como o esgotamento de recursos desempenha um papel em causar falha generalizada, vamos analisar novamente uma das postmortems de incidente de produção de amostra:

À medida que cada caixa de serviço atingiu seu limite, ele passou a exceder o tempo limite e fez com que o serviço P2 upstream tentasse novamente. Tentativas agressivas do serviço P2 rapidamente dobraram seu tráfego para o serviço. Como resultado, o serviço tentou criar muitas conexões de proxy, todas com tempo esgotado. A latência de todos os backends de serviço aumentou, causando falhas na verificação de integridade. Eventualmente, todas as caixas para o serviço P2 são marcadas . ”

O enfileiramento de solicitações é uma técnica eficaz para permitir que os serviços absorvam a carga de solicitações em rajadas e evitem que os serviços falhem devido ao esgotamento de recursos. Mais especificamente, implementamos uma fila de atraso controlado ( CoDel ) com LIFO adaptável ( last-in first-out ), conforme introduzido no artigo Fail at Scale .

Fila de Atraso Controlado

Em condições normais de operação, o servidor pode processar solicitações à medida que elas chegam e esvaziar a fila dentro de N milissegundos, e a fila de solicitações do CoDel usa N como o valor de tempo limite de solicitação normal. Se a fila de pedidos não estiver sendo esvaziada em N milissegundos, ela usará um valor de tempo limite de solicitação mais agressivo. O timeout agressivo ajuda a evitar que uma fila de espera se acumule e, portanto, protege o servidor de tropeçar sob carga que não consegue acompanhar.

LIFO adaptativo

Em condições normais de operação, o servidor processa solicitações na ordem FIFO. Quando uma fila está aumentando, no entanto, ela muda para o modo LIFO. Durante um período de enfileiramento, as solicitações de última geração têm mais chances de atender aos prazos de solicitações do lado do cliente do que as solicitações iniciais, que ficam na fila por mais tempo.

O LIFO adaptável funciona muito bem com a fila CoDel para serviços de alta latência e baixa latência no fluxo de reserva central da Airbnb, porque são medidas eficazes de prevenção de latência. Além disso, vemos que a implementação da fila de solicitações personalizadas melhora, na verdade, o rendimento do host de serviço único e a taxa de erros.

Nós comparamos a fila de atraso controlada (amarelo) com o manipulador anterior, um pool de threads limitado (vermelho). Em um serviço Java de alto rendimento, a mudança para uma fila de atraso controlada melhorou o throughput de host único e reduziu a taxa de erros.

A fila de solicitações introduz um ligeiro aumento nas latências dos percentis 95 e 99 durante a condição de sobrecarga, conforme mostrado nos gráficos abaixo.

Pool de threads limitado é mostrado em azul, enquanto a fila de atraso controlado é mostrada em roxo.

Derramamento de carga

Os post mortem de incidentes de produção também citam tentativas agressivas de clientes mais de uma vez. Na verdade, é a causa mais comum de falhas em cascata porque a tempestade de tentativas não fornece a nenhum serviço sobrecarregado qualquer espaço para recuperação. Nosso cliente padrão entre serviços implementa padrões de timeout, repetição e disjuntor. Ele libera os desenvolvedores de escreverem seus próprios ciclos de repetição, que geralmente são a fonte de novas tempestades. No entanto, um recurso de resiliência mais eficaz é para o cliente saber quando parar completamente de enviar solicitações.

Pressão de retorno do serviço

O status da fila de solicitações do lado do servidor é o melhor sinal de se um serviço está sobrecarregado. Quando a fila de solicitações em um host de serviço é construída, ela alterna para o modo LIFO e inicia solicitações de entrada com falha rápida com um erro de pressão de retorno até que a marca d'água da fila de solicitações caia para um limite seguro. Quando propagado para o lado do cliente, o smart client abster-se-á de repetir e falhar rapidamente o pedido de upstream. A pressão de retorno do serviço exige coordenação entre o servidor e o cliente e resulta em uma recuperação mais rápida ao falhar conscientemente em uma fração de solicitações. Mais importante, evita falhas em cascata que vimos em incidentes de produção devido a uma combinação de sobrecarga de serviço e nova tentativa de tempestade.

Neste gráfico de painel de servidores padrão, podemos ver que um host sofreu uma queda drástica em QPS bem-sucedido, mas depois se recuperou em um período de 1 minuto. A fila de solicitações para essa instância foi criada e o servidor começou a enviar erros de pressão de retorno de serviço. Os clientes inteligentes honravam o sinal de sobrecarga de serviço não repetindo nenhuma solicitação e acionando os disjuntores do cliente imediatamente. Essa coordenação permitiu que o servidor se recuperasse por conta própria e impedisse um possível incidente de produção.

Prazo de solicitação da API

Outra técnica de redução de carga que usamos é o prazo de solicitação da API, uma implementação da propagação de prazo da RPC, que permite que os serviços manipulem graciosamente as sobrecargas de solicitações. Todos os pontos de extremidade da API pública do Airbnb definiram tempos limite de solicitação cuidadosamente vinculados a seus SLOs de latência. Cada solicitação de API de entrada tem um prazo definido pelo gateway da API usando o tempo limite do nível do terminal. O prazo é então propagado à medida que a solicitação é enviada para os serviços de back-end. A estrutura do servidor verifica o prazo e as solicitações de falha rápida que atingiram seus prazos. À medida que continuamos a construir mais práticas de engenharia de resiliência em toda a pilha de comunicação entre serviços, seremos mais agressivos ao definir o tempo limite de solicitação da API e impor prazos.

Limite de Taxa Baseado em Cota do Cliente

A contrapressão de serviço protege um serviço de falhas sob carga excessiva, no entanto, ele não distingue o tráfego entre os diferentes chamadores upstream. Por exemplo, nosso serviço de listagens tem mais de 30 chamadores upstream. Qualquer chamador mal-intencionado pode enviar uma quantidade excessiva de solicitações e afetar outros clientes.

A Airbnb sofreu vários incidentes de serviços principais devido à carga excessiva de clientes invasores. Implementamos a limitação de taxa baseada em quota de cliente na estrutura de serviço padrão, composta por um serviço de contador e um sistema de configuração dinâmica. Um proprietário do serviço pode definir cotas de clientes no sistema de configuração dinâmica, e o serviço usará o serviço de balcão distribuído para rastrear cotas e limitar o limite de qualquer cliente abusivo.

A limitação da taxa baseada em cotas funciona bem com a pressão de retorno do serviço na produção. Durante condições normais, a capacidade do serviço é distribuída de forma justa entre seus chamadores upstream. Quando um chamador se comporta mal, a limitação de taxa protege o serviço contra cargas inesperadas, evitando erros de pressão de retorno de serviço para outros clientes.

Isolamento de Dependência e Degradação Graciosa

Em uma arquitetura de serviços distribuídos, um serviço geralmente terá várias dependências downstream. Quando o número de dependências aumenta, a probabilidade de uma única dependência problemática derrubar um serviço torna-se maior. Vejamos um exemplo disso acontecendo no Airbnb:

“O serviço P3 tem uma dependência suave do serviço de moderação de conteúdo, porque apenas uma fração minúscula de solicitações o chamaria. Durante o incidente, o serviço de moderação de conteúdo downstream não respondeu e os threads de trabalho assíncrono do serviço P3 não foram liberados em tempo hábil. Eventualmente, o serviço P3 esgotou seus pools de threads, causando erros elevados para todas as solicitações. ”

Adotar o padrão do Bulkhead para nossas solicitações poderia ter atenuado esse problema. Implementamos o isolamento de dependência na estrutura do executor de solicitação assíncrona e nos clientes IDL entre serviços na plataforma de serviço, semelhante à abordagem da Hystrix da Netflix . Dependências separadas têm conjuntos de encadeamentos de trabalhadores assíncronos separados, para que uma dependência downstream problemática sature apenas o conjunto de encadeamentos de trabalhadores correspondente. Se uma dependência problemática for uma dependência leve, como informações de tendências na página de detalhes do produto, a disponibilidade do serviço não será afetada, exceto a execução de respostas parciais.

Gráfico de tamanhos de conjuntos de encadeamentos por dependência de um serviço.

Detecção de Host de Servidor Outlier

As falhas de host do servidor são uma ameaça frequente ao tempo de atividade, como vimos em nossos postmortems. Quando você tem uma grande frota de servidores em produção, é apenas uma questão de tempo até que alguns hosts do servidor comecem a agir erroneamente.

“Uma instância de serviço experimentou um status anormal a partir das 7:45 pm PDT. QPS para a instância caiu e a taxa de erro da instância aumentou. O alerta da equipe de serviço não foi acionado até as 23h49. Às 12h40, o engenheiro de plantão descobriu a instância problemática e a reinicializou para resolver o problema. ”

Este incidente destacou três ferramentas que precisávamos melhorar: monitoramento, detecção e alerta de servidores outliers. A combinação de queda QPS e elevada taxa de erro do servidor defeituoso foi um sinal claro. No entanto, ele não foi capturado e utilizado pela nossa pilha de descoberta de serviços, exigindo intervenção humana. Os engenheiros de plantão precisavam pesquisar em painéis fragmentados para correlacionar os sintomas à causa raiz antes de girar o host do servidor defeituoso. Como resultado, o tempo do MTTR (tempo médio de reparo) para esse tipo de cenário era muito pior do que poderia ter sido.

O balanceamento de carga inteligente do lado do cliente é uma medida de resiliência automatizada que pode impedir que esses cenários aconteçam. No balanceamento de carga inteligente do lado do cliente, o cliente mantém uma lista de hosts do servidor. Ele rastreia a taxa de sucesso e a latência de resposta de cada host e evita solicitações de roteamento para hosts com taxa de sucesso baixa e latência elevada. Adicionamos o Envoy à nossa pilha de descoberta de serviços, que oferece suporte à detecção de exceções .

Na produção, quando um host de servidor tem respostas de erros mais altas que o normal, a detecção de outliers detecta e ejeta rapidamente, e o host recebe imediatamente menos tráfego. A medida de resiliência automatizada entra em ação em menos de um minuto depois que um servidor se torna defeituoso, e os engenheiros de serviço por chamada não precisam ser paginados.

Conclusão

Em uma arquitetura orientada a serviços, a complexidade da comunicação entre serviços aumenta exponencialmente à medida que o número de serviços e a profundidade da pilha aumentam. As técnicas para manter a confiabilidade e a disponibilidade do site são diferentes daquelas de uma arquitetura monolítica. Compartilhamos algumas das melhores práticas de engenharia de resiliência que criamos em nossa plataforma de serviços padrão. Eles são implementados na estrutura do servidor e nas bibliotecas do cliente de maneira coesa. Eles são fáceis de configurar e simples de usar. Acreditamos que a resiliência é um requisito em vez de um recurso opcional em uma arquitetura de serviços distribuídos. Este trabalho efetivamente impediu um número crescente de possíveis incidentes.

Os serviços individuais devem manter seus SLOs (objetivos de nível de serviço) em todas as circunstâncias, sejam implantações, surtos de tráfego, falhas de rede transitórias ou falhas de host persistentes. A engenharia de resiliência de serviço padrão pode ajudar os proprietários de serviços com esse objetivo. A engenharia de resiliência é fundamental para todo o conjunto de obras para ajudar os proprietários de serviços a configurar seu SLO, testes de verificação de disponibilidade de serviços, planejamento de capacidade, etc. Esse post abordou os resultados mais baixos. Em nosso próximo post, compartilharemos o que construímos e planejamos construir para impor o SLO; teste de injeção de falha; testes de verificação de confiabilidade e disponibilidade; e engenharia do caos. Fique ligado!