“O Kubernetes aumentou a latência em 10 vezes”: quem é o culpado por isso?

Observação. trad.: Este artigo, escrito por Galo Navarro, que ocupa o cargo de Engenheiro Principal de Software na empresa europeia Adevinta, é uma “investigação” fascinante e instrutiva no campo das operações de infraestrutura. Seu título original foi ligeiramente ampliado na tradução por um motivo que o autor explica logo no início.

“O Kubernetes aumentou a latência em 10 vezes”: quem é o culpado por isso?

Nota do autor: Parece esta postagem atraído muito mais atenção do que o esperado. Ainda recebo comentários irados de que o título do artigo é enganoso e que alguns leitores ficam tristes. Entendo os motivos do que está acontecendo, portanto, apesar do risco de estragar toda a intriga, quero contar imediatamente do que trata este artigo. Uma coisa curiosa que tenho visto enquanto as equipes migram para o Kubernetes é que sempre que surge um problema (como o aumento da latência após uma migração), a primeira coisa a ser responsabilizada é o Kubernetes, mas depois acontece que o orquestrador não é realmente o responsável. culpa. Este artigo fala sobre um desses casos. Seu nome repete a exclamação de um de nossos desenvolvedores (mais tarde você verá que o Kubernetes não tem nada a ver com isso). Você não encontrará nenhuma revelação surpreendente sobre o Kubernetes aqui, mas pode esperar algumas boas lições sobre sistemas complexos.

Algumas semanas atrás, minha equipe estava migrando um único microsserviço para uma plataforma central que incluía CI/CD, um tempo de execução baseado em Kubernetes, métricas e outras vantagens. A mudança foi experimental: planejávamos tomá-la como base e transferir cerca de mais 150 serviços nos próximos meses. Todos eles são responsáveis ​​pelo funcionamento de algumas das maiores plataformas online de Espanha (Infojobs, Fotocasa, etc.).

Depois de implantarmos o aplicativo no Kubernetes e redirecionarmos parte do tráfego para ele, uma surpresa alarmante nos aguardava. Atraso (latência) as solicitações no Kubernetes foram 10 vezes maiores do que no EC2. Em geral, era necessário encontrar uma solução para esse problema ou abandonar a migração do microsserviço (e, possivelmente, de todo o projeto).

Por que a latência é muito maior no Kubernetes do que no EC2?

Para encontrar o gargalo, coletamos métricas ao longo de todo o caminho da solicitação. Nossa arquitetura é simples: um gateway de API (Zuul) faz proxy de solicitações para instâncias de microsserviços no EC2 ou Kubernetes. No Kubernetes usamos o NGINX Ingress Controller, e os backends são objetos comuns como desenvolvimento com um aplicativo JVM na plataforma Spring.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

O problema parecia estar relacionado à latência inicial no backend (marquei a área do problema no gráfico como "xx"). No EC2, a resposta do aplicativo demorou cerca de 20 ms. No Kubernetes, a latência aumentou para 100-200 ms.

Rapidamente descartamos os prováveis ​​suspeitos relacionados à mudança no tempo de execução. A versão da JVM permanece a mesma. Os problemas de conteinerização também não tiveram nada a ver com isso: o aplicativo já estava rodando com sucesso em contêineres no EC2. Carregando? Mas observamos altas latências mesmo com 1 solicitação por segundo. As pausas para coleta de lixo também poderiam ser negligenciadas.

Um de nossos administradores do Kubernetes se perguntou se o aplicativo tinha dependências externas porque as consultas DNS causaram problemas semelhantes no passado.

Hipótese 1: resolução de nomes DNS

Para cada solicitação, nosso aplicativo acessa uma instância do AWS Elasticsearch de uma a três vezes em um domínio como elastic.spain.adevinta.com. Dentro de nossos contêineres há uma concha, para que possamos verificar se a busca por um domínio realmente leva muito tempo.

Consultas DNS do contêiner:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

Solicitações semelhantes de uma das instâncias do EC2 em que o aplicativo está em execução:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

Considerando que a consulta demorou cerca de 30ms, ficou claro que a resolução do DNS ao acessar o Elasticsearch estava de fato contribuindo para o aumento da latência.

No entanto, isso foi estranho por dois motivos:

  1. Já temos vários aplicativos Kubernetes que interagem com os recursos da AWS sem sofrer alta latência. Seja qual for o motivo, ele se refere especificamente a este caso.
  2. Sabemos que a JVM faz cache de DNS na memória. Nas nossas imagens, o valor TTL está escrito em $JAVA_HOME/jre/lib/security/java.security e definido para 10 segundos: networkaddress.cache.ttl = 10. Em outras palavras, a JVM deve armazenar em cache todas as consultas DNS por 10 segundos.

Para confirmar a primeira hipótese, decidimos parar de chamar o DNS por um tempo e ver se o problema desaparecia. Primeiro, decidimos reconfigurar a aplicação para que ela se comunicasse diretamente com o Elasticsearch por endereço IP, e não por meio de um nome de domínio. Isso exigiria alterações de código e uma nova implantação, então simplesmente mapeamos o domínio para seu endereço IP em /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Agora o contêiner recebeu um IP quase que instantaneamente. Isso resultou em algumas melhorias, mas ficamos apenas um pouco mais próximos dos níveis de latência esperados. Embora a resolução do DNS tenha demorado muito, o verdadeiro motivo ainda nos escapava.

Diagnóstico via rede

Decidimos analisar o tráfego do contêiner usando tcpdumppara ver o que exatamente está acontecendo na rede:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

Em seguida, enviamos várias solicitações e baixamos sua captura (kubectl cp my-service:/capture.pcap capture.pcap) para análise posterior em Wireshark.

Não havia nada de suspeito nas consultas de DNS (exceto por uma coisinha sobre a qual falarei mais tarde). Mas havia algumas peculiaridades na forma como nosso serviço tratava cada solicitação. Abaixo está uma captura de tela da captura mostrando a solicitação sendo aceita antes do início da resposta:

“O Kubernetes aumentou a latência em 10 vezes”: quem é o culpado por isso?

Os números dos pacotes são mostrados na primeira coluna. Para maior clareza, codifiquei por cores os diferentes fluxos TCP.

O fluxo verde começando com o pacote 328 mostra como o cliente (172.17.22.150) estabeleceu uma conexão TCP com o contêiner (172.17.36.147). Após o aperto de mão inicial (328-330), o pacote 331 trouxe HTTP GET /v1/.. — uma solicitação recebida ao nosso serviço. Todo o processo levou 1 ms.

O fluxo cinza (do pacote 339) mostra que nosso serviço enviou uma solicitação HTTP para a instância do Elasticsearch (não há handshake TCP porque está usando uma conexão existente). Isso levou 18ms.

Até agora está tudo bem e os tempos correspondem aproximadamente aos atrasos esperados (20-30 ms quando medidos no cliente).

No entanto, a seção azul leva 86ms. O que está acontecendo nele? Com o pacote 333, nosso serviço enviou uma solicitação HTTP GET para /latest/meta-data/iam/security-credentials, e imediatamente depois, pela mesma conexão TCP, outra solicitação GET para /latest/meta-data/iam/security-credentials/arn:...

Descobrimos que isso se repetia a cada solicitação durante o rastreamento. A resolução de DNS é de fato um pouco mais lenta em nossos contêineres (a explicação para esse fenômeno é bastante interessante, mas vou guardá-la para um artigo separado). Descobriu-se que a causa dos longos atrasos eram chamadas para o serviço AWS Instance Metadata em cada solicitação.

Hipótese 2: chamadas desnecessárias para AWS

Ambos os pontos finais pertencem a API de metadados de instância da AWS. Nosso microsserviço usa esse serviço durante a execução do Elasticsearch. Ambas as chamadas fazem parte do processo básico de autorização. O endpoint acessado na primeira solicitação emite a função do IAM associada à instância.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

A segunda solicitação solicita ao segundo endpoint permissões temporárias para esta instância:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

O cliente pode utilizá-los por um curto período de tempo e deve obter periodicamente novos certificados (antes de serem Expiration). O modelo é simples: a AWS alterna chaves temporárias com frequência por motivos de segurança, mas os clientes podem armazená-las em cache por alguns minutos para compensar a penalidade de desempenho associada à obtenção de novos certificados.

O AWS Java SDK deveria assumir a responsabilidade de organizar esse processo, mas por algum motivo isso não acontece.

Depois de pesquisar problemas no GitHub, nos deparamos com um problema #1921. Ela nos ajudou a determinar a direção na qual “cavar” ainda mais.

O AWS SDK atualiza certificados quando ocorre uma das seguintes condições:

  • Data de validade (Expiration) Cair em EXPIRATION_THRESHOLD, codificado para 15 minutos.
  • Mais tempo se passou desde a última tentativa de renovação de certificados REFRESH_THRESHOLD, codificado por 60 minutos.

Para ver a data de expiração real dos certificados que recebemos, executamos os comandos cURL acima no contêiner e na instância do EC2. O prazo de validade do certificado recebido do contêiner acabou sendo bem menor: exatamente 15 minutos.

Agora tudo ficou claro: para a primeira solicitação, nosso serviço recebeu certificados temporários. Como eles não eram válidos por mais de 15 minutos, o AWS SDK decidiria atualizá-los em uma solicitação subsequente. E isso aconteceu com todos os pedidos.

Por que o período de validade dos certificados ficou mais curto?

Os metadados de instância da AWS foram projetados para funcionar com instâncias EC2, não com Kubernetes. Por outro lado, não queríamos alterar a interface do aplicativo. Para isso usamos KIAM - uma ferramenta que, usando agentes em cada nó do Kubernetes, permite que os usuários (engenheiros que implantam aplicativos em um cluster) atribuam funções IAM a contêineres em pods como se fossem instâncias do EC2. O KIAM intercepta chamadas para o serviço AWS Instance Metadata e as processa a partir de seu cache, tendo-as recebido previamente da AWS. Do ponto de vista da aplicação, nada muda.

KIAM fornece certificados de curto prazo para pods. Isso faz sentido considerando que a vida útil média de um pod é menor do que a de uma instância EC2. Período de validade padrão para certificados igual aos mesmos 15 minutos.

Como resultado, se você sobrepor os dois valores padrão, surge um problema. Cada certificado fornecido a um aplicativo expira após 15 minutos. No entanto, o AWS Java SDK força a renovação de qualquer certificado que tenha menos de 15 minutos restantes antes da data de expiração.

Como resultado, o certificado temporário é forçado a ser renovado a cada solicitação, o que implica algumas chamadas para a API da AWS e resulta em um aumento significativo na latência. No AWS Java SDK encontramos pedido de recurso, que menciona um problema semelhante.

A solução acabou sendo simples. Simplesmente reconfiguramos o KIAM para solicitar certificados com prazo de validade mais longo. Quando isso aconteceu, as solicitações começaram a fluir sem a participação do serviço AWS Metadata e a latência caiu para níveis ainda mais baixos do que no EC2.

Descobertas

Com base em nossa experiência com migrações, uma das fontes mais comuns de problemas não são bugs no Kubernetes ou em outros elementos da plataforma. Também não aborda nenhuma falha fundamental nos microsserviços que estamos portando. Muitas vezes os problemas surgem simplesmente porque juntamos diferentes elementos.

Misturamos sistemas complexos que nunca interagiram antes, esperando que juntos formem um sistema único e maior. Infelizmente, quanto mais elementos, mais espaço para erros e maior será a entropia.

Em nosso caso, a alta latência não foi resultado de bugs ou decisões erradas no Kubernetes, KIAM, AWS Java SDK ou em nosso microsserviço. Foi o resultado da combinação de duas configurações padrão independentes: uma no KIAM e outra no AWS Java SDK. Tomados separadamente, ambos os parâmetros fazem sentido: a política de renovação de certificado ativa no AWS Java SDK e o curto período de validade dos certificados no KAIM. Mas quando você os junta, os resultados tornam-se imprevisíveis. Duas soluções independentes e lógicas não precisam fazer sentido quando combinadas.

PS do tradutor

Você pode aprender mais sobre a arquitetura do utilitário KIAM para integração do AWS IAM com o Kubernetes em Este artigo de seus criadores.

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário