Isolamento preditivo de CPU de contêineres no Netflix

De Benoit Rostykus, Gabriel Hartmann

Blog da Tecnologia Netflix em Netflix TechBlog Segue 4 de junho · 8 min ler

Vizinhos barulhentos

Todos nós já tivemos vizinhos barulhentos em algum momento da nossa vida. Seja em um café ou na parede de um apartamento, é sempre perturbador. A necessidade de boas maneiras em espaços compartilhados acaba sendo importante não apenas para as pessoas, mas também para seus contêineres Docker.

Quando você está executando na nuvem, seus contêineres estão em um espaço compartilhado; em particular, eles compartilham a hierarquia de memória da CPU da instância do host.

Como os microprocessadores são tão rápidos, o projeto de arquitetura de computadores evoluiu para adicionar vários níveis de armazenamento em cache entre as unidades de computação e a memória principal, a fim de ocultar a latência de trazer os bits para os cérebros. No entanto, o principal insight aqui é que esses caches são parcialmente compartilhados entre as CPUs, o que significa que o isolamento perfeito de desempenho de contêineres co-hospedados não é possível. Se o contêiner em execução no núcleo próximo ao seu contêiner de repente decidir buscar muitos dados da RAM, isso inevitavelmente resultará em mais erros de cache para você (e, portanto, uma possível degradação do desempenho).

Linux para o resgate?

Tradicionalmente, é responsabilidade do agendador de tarefas do sistema operacional atenuar esse problema de isolamento de desempenho. No Linux, a solução atual é o CFS (Completely Fair Scheduler). Seu objetivo é atribuir processos em execução a fatias de tempo da CPU de maneira “justa”.

O CFS é amplamente utilizado e, portanto, bem testado e as máquinas Linux em todo o mundo funcionam com desempenho razoável. Então, por que mexer com isso? Acontece que, para a grande maioria dos casos de uso do Netflix, seu desempenho está longe de ser ideal. Titus é a plataforma de contêineres da Netflix. Todos os meses, corremos milhões de contêineres em milhares de máquinas no Titus, atendendo a centenas de aplicativos e clientes internos. Esses aplicativos variam de serviços críticos de baixa latência que impulsionam o serviço de streaming de vídeo voltado para o cliente, a tarefas em lote para codificação ou aprendizado de máquina. A manutenção do isolamento de desempenho entre esses diferentes aplicativos é fundamental para garantir uma boa experiência para clientes internos e externos.

Conseguimos melhorar significativamente a previsibilidade e o desempenho desses contêineres, retirando parte da responsabilidade do isolamento da CPU do sistema operacional e avançando para uma solução orientada por dados que envolve otimização combinatória e aprendizado de máquina.

A ideia

O CFS opera com muita freqüência (a cada poucos microssegundos) aplicando um conjunto de heurísticas que encapsulam um conceito geral de melhores práticas em relação ao uso de hardware da CPU.

Em vez disso, e se reduzíssemos a frequência de intervenções (a cada poucos segundos), mas tomamos melhores decisões baseadas em dados em relação à alocação de processos para calcular recursos, a fim de minimizar o ruído de colocação?

Uma forma tradicional de atenuar os problemas de desempenho do CFS é que os proprietários de aplicativos cooperem manualmente por meio do uso de pinagem do núcleo ou valores agradáveis. No entanto, podemos automaticamente tomar melhores decisões globais, detectando oportunidades de colocação com base nas informações de uso reais. Por exemplo, se prevermos que o contêiner A vai se tornar muito intensivo em CPU logo, então talvez devamos executá-lo em um soquete NUMA diferente do contêiner B, que é muito sensível à latência. Isso evita a sobrecarga de caches para B e equilibra a pressão nos caches L3 da máquina.

Otimizando veiculações por meio da otimização combinatória

O que o Agendador de Tarefas do SO está fazendo é essencialmente resolver um problema de alocação de recursos: Eu tenho threads X para executar, mas apenas CPUs Y disponíveis, como alocar os threads para as CPUs para dar a ilusão de simultaneidade?

Como exemplo ilustrativo, vamos considerar uma instância de brinquedo de 16 hyperthreads . Possui 8 núcleos hipercrescidos físicos, divididos em 2 soquetes NUMA. Cada hyperthread compartilha seus caches L1 e L2 com seu vizinho e compartilha seu cache L3 com os outros 7 hyperthreads no soquete:

Se quisermos executar o contêiner A em quatro encadeamentos e o contêiner B em duas encadeamentos nessa instância, poderemos analisar as decisões de posicionamento "ruins" e "boas":

A primeira colocação é intuitivamente ruim porque nós potencialmente criamos ruído de colocação entre A e B nos primeiros 2 núcleos através de seus caches L1 / L2, e no soquete através do cache L3 enquanto deixamos um soquete inteiro vazio. O segundo posicionamento parece melhor, já que cada CPU recebe seus próprios caches L1 / L2, e usamos melhor os dois caches L3 disponíveis.

Os problemas de alocação de recursos podem ser eficientemente resolvidos por meio de um ramo da matemática chamado otimização combinatória, usado, por exemplo, para agendamento de linhas aéreas ou problemas de logística.

Nós formulamos o problema como um Programa Inteiro Misto (MIP). Dado um conjunto de contêineres K cada um solicitando um número específico de CPUs em uma instância que possui encadeamentos d, o objetivo é encontrar uma matriz de atribuição b M de tamanho (d, K) tal que cada contêiner obtenha o número de CPUs solicitadas. A função de perda e as restrições contêm vários termos que expressam decisões a priori de boa colocação, como:

  • evitar a propagação de um contêiner através de vários sockets NUMA (para evitar acessos de memória cross-sockets potencialmente lentos ou migrações de páginas)
  • não use hyper-threads a menos que você precise (para reduzir a surra L1 / L2)
  • Tentar minimizar a pressão nos caches L3 (com base em possíveis medições do uso de hardware do contêiner)
  • não embaralhe demais as coisas entre as decisões de colocação

Dados os requisitos de baixa latência e baixa computação do sistema (certamente não queremos gastar muitos ciclos de CPU para descobrir como os contêineres devem usar os ciclos de CPU!), Podemos realmente fazer isso funcionar na prática?

Implementação

Decidimos implementar a estratégia através de cgroups do Linux, uma vez que eles são totalmente suportados pelo CFS, modificando o cgroup cpuset de cada contêiner com base no mapeamento desejado de contêineres para hyper-threads. Dessa maneira, um processo de espaço do usuário define uma “cerca” dentro da qual o CFS opera para cada contêiner. Com efeito, removemos o impacto da heurística do CFS no isolamento de desempenho e, ao mesmo tempo, retemos seus principais recursos de agendamento.

Esse processo de espaço do usuário é um subsistema Titus chamado titus-isolate, que funciona da seguinte maneira. Em cada instância, definimos três eventos que acionam uma otimização de posicionamento:

  • add : Um novo contêiner foi alocado pelo agendador Titus para esta instância e precisa ser executado
  • remove : um contêiner em execução acabou de terminar
  • rebalanceamento : o uso da CPU pode ter mudado nos contêineres, por isso devemos reavaliar nossas decisões de veiculação

Nós periodicamente enfileiramos eventos de rebalanceamento quando nenhum outro evento acionou recentemente uma decisão de colocação.

Toda vez que um evento de posicionamento é acionado, o titus-isolate consulta um serviço de otimização remota (executado como um serviço Titus, portanto, também isolando-se … tartarugas até o fim ) que resolve o problema de posicionamento de contêiner em encadeamento.

Este serviço então consulta um modelo GBRT local (treinado a cada duas horas em semanas de dados coletados de toda a plataforma Titus) prevendo o uso da CPU P95 de cada contêiner nos próximos 10 minutos (regressão quantil condicional). O modelo contém recursos contextuais (metadados associados ao contêiner: quem o iniciou, configuração de imagem, memória e rede, nome do aplicativo…), bem como recursos de série temporal extraídos da última hora de uso histórico da CPU do contêiner coletado regularmente por o host do controlador de contabilidade da CPU do kernel.

As previsões são então alimentadas em um MIP que é resolvido em tempo real. Estamos usando o cvxpy como um bom front-end simbólico genérico para representar o problema, que pode ser alimentado em vários back-ends de solucionador de MIP de código aberto ou proprietários. Como os MIPs são NP-hard, alguns cuidados precisam ser tomados. Nós impomos um orçamento difícil ao solucionador para direcionar a estratégia branch-and-cut para um regime de baixa latência, com guardrails em torno do gap do MIP para controlar a qualidade geral da solução encontrada.

Em seguida, o serviço retorna a decisão de colocação para o host, que o executa modificando os cpusets dos contêineres.

Por exemplo, a qualquer momento, um r4.16xlarge com 64 CPUs lógicas pode se parecer com isso (a escala de cores representa o uso da CPU):

Resultados

A primeira versão do sistema levou a resultados surpreendentemente bons. Reduzimos o tempo de execução geral de tarefas em lote em vários percentuais, em média, enquanto, o mais importante, reduzimos a variação de tempo de execução de tarefas (um proxy razoável para isolamento), conforme ilustrado abaixo. Aqui vemos uma distribuição de tempo de execução de trabalho em lote do mundo real com e sem isolamento aprimorado:

Observe como, na maioria das vezes, o problema dos outliers de longa duração desaparece. A cauda direita dos azarados vizinhos ruidosos desapareceu.

Para os serviços, os ganhos foram ainda mais impressionantes. Um serviço de middleware Titus específico que atende o serviço de streaming da Netflix teve uma redução de capacidade de 13% (uma redução de mais de 1000 contêineres) necessária no pico de tráfego para atender à mesma carga com o SLA de latência P99 necessário! Também notamos uma redução acentuada do uso da CPU nas máquinas, já que muito menos tempo foi gasto pelo kernel na lógica de invalidação de cache. Nossos contêineres agora são mais previsíveis, mais rápidos e a máquina é menos usada! Não é sempre que você pode ter seu bolo e comê-lo também.

Próximos passos

Estamos animados com os avanços feitos até agora nesta área. Estamos trabalhando em várias frentes para ampliar a solução apresentada aqui.

Queremos estender o sistema para suportar excesso de assinatura da CPU. A maioria dos nossos usuários tem desafios para saber como dimensionar corretamente o número de CPUs que seu aplicativo precisa. E, de fato, esse número varia durante o tempo de vida de seus contêineres. Como já prevemos o uso futuro da CPU dos contêineres, queremos detectar e recuperar automaticamente os recursos não utilizados. Por exemplo, pode-se decidir atribuir automaticamente um contêiner específico a um cgroup compartilhado de CPUs subutilizadas, para melhorar melhor o isolamento geral e a utilização da máquina, se pudermos detectar o limite de sensibilidade de nossos usuários nos vários eixos do gráfico a seguir.

Também queremos aproveitar os eventos do PMC do kernel para otimizar mais diretamente o ruído mínimo do cache. Um caminho possível é usar as instâncias de bare metal baseadas na Intel introduzidas recentemente pela Amazon, que permitem acesso profundo às ferramentas de análise de desempenho. Poderíamos então alimentar essa informação diretamente no mecanismo de otimização para avançar para uma abordagem de aprendizado mais supervisionada. Isso exigiria uma aleatorização contínua adequada dos posicionamentos para coletar contrafactuais imparciais, para que pudéssemos construir algum tipo de modelo de interferência (“qual seria o desempenho do contêiner A no próximo minuto, se eu fosse colocar um de seus encadeamentos no mesmo núcleo do contêiner B, sabendo que também há C rodando no mesmo soquete agora? ”).

Conclusão

Se algo disso despertar seu interesse, entre em contato conosco! Estamos procurando engenheiros da ML para nos ajudar a ampliar o limite de desempenho de contêineres e “aprendizado de máquina para sistemas” e engenheiros de sistemas para nossa infraestrutura básica e plataforma de computação.