Transición de Tinder a Kubernetes

Nota. transl.: Os empregados do mundialmente famoso servizo Tinder compartiron recentemente algúns detalles técnicos sobre a migración da súa infraestrutura a Kubernetes. O proceso levou case dous anos e deu lugar ao lanzamento dunha plataforma a gran escala en K8, que consta de 200 servizos aloxados en 48 mil contedores. Que dificultades interesantes atoparon os enxeñeiros de Tinder e que resultados chegaron? Le esta tradución.

Transición de Tinder a Kubernetes

Por que?

Hai case dous anos, Tinder decidiu trasladar a súa plataforma a Kubernetes. Kubernetes permitiría que o equipo de Tinder conteinerizara e pasara á produción cun esforzo mínimo mediante a implantación inmutable (implementación inmutable). Neste caso, o conxunto de aplicacións, o seu despregamento e a propia infraestrutura estarían definidos de forma exclusiva por código.

Tamén buscabamos unha solución ao problema de escalabilidade e estabilidade. Cando a escala se volveu crítica, moitas veces tivemos que esperar varios minutos para que aparezan novas instancias EC2. A idea de lanzar contedores e comezar a servir o tráfico en segundos en lugar de minutos fíxosenos moi atractiva.

O proceso resultou difícil. Durante a nosa migración a principios de 2019, o clúster de Kubernetes alcanzou unha masa crítica e comezamos a atopar varios problemas debido ao volume de tráfico, o tamaño do clúster e o DNS. Ao longo do camiño, resolvemos moitos problemas interesantes relacionados coa migración de 200 servizos e o mantemento dun clúster de Kubernetes formado por 1000 nodos, 15000 pods e 48000 contedores en execución.

Como?

Dende xaneiro de 2018 pasamos por varias etapas migratorias. Comezamos por contener todos os nosos servizos e despregámolos en ambientes de proba de Kubernetes. A partir de outubro, comezamos a migrar metodicamente todos os servizos existentes a Kubernetes. En marzo do ano seguinte, completamos a migración e agora a plataforma Tinder funciona exclusivamente en Kubernetes.

Creando imaxes para Kubernetes

Temos máis de 30 repositorios de código fonte para microservizos que se executan nun clúster de Kubernetes. O código destes repositorios está escrito en diferentes linguaxes (por exemplo, Node.js, Java, Scala, Go) con varios ambientes de execución para a mesma linguaxe.

O sistema de compilación está deseñado para ofrecer un "contexto de compilación" totalmente personalizable para cada microservizo. Normalmente consiste nun Dockerfile e unha lista de comandos de shell. O seu contido é totalmente personalizable e, ao mesmo tempo, todos estes contextos de compilación están escritos segundo un formato estandarizado. A estandarización dos contextos de compilación permite que un único sistema de compilación xestione todos os microservizos.

Transición de Tinder a Kubernetes
Figura 1-1. Proceso de construción estandarizado a través do contenedor Builder

Para conseguir a máxima coherencia entre os tempos de execución (entornos de execución) úsase o mesmo proceso de compilación durante o desenvolvemento e as probas. Enfrontámonos a un reto moi interesante: tivemos que desenvolver un xeito de garantir a coherencia do ambiente de construción en toda a plataforma. Para conseguilo, todos os procesos de montaxe realízanse dentro dun recipiente especial. Constructor.

A súa implementación de contedores requiriu técnicas avanzadas de Docker. Builder herda o ID de usuario local e os segredos (como a chave SSH, as credenciais de AWS, etc.) necesarios para acceder aos repositorios privados de Tinder. Monta directorios locais que conteñen fontes para almacenar de forma natural artefactos de construción. Este enfoque mellora o rendemento porque elimina a necesidade de copiar artefactos de compilación entre o contenedor de Builder e o host. Os artefactos de construción almacenados pódense reutilizar sen configuración adicional.

Para algúns servizos, tivemos que crear outro contedor para asignar o ambiente de compilación ao ambiente de execución (por exemplo, a biblioteca bcrypt de Node.js xera artefactos binarios específicos da plataforma durante a instalación). Durante o proceso de compilación, os requisitos poden variar entre os servizos e o ficheiro Dockerfile final compílase sobre a marcha.

Arquitectura e migración de clúster de Kubernetes

Xestión do tamaño do clúster

Decidimos usar kube-aws para a implementación automatizada de clúster en instancias de Amazon EC2. Ao principio, todo funcionaba nun conxunto común de nós. Axiña decatámonos da necesidade de separar as cargas de traballo por tamaño e tipo de instancia para facer un uso máis eficiente dos recursos. O lóxico era que executar varios pods multiproceso cargados resultou ser máis previsible en termos de rendemento que a súa coexistencia cunha gran cantidade de pods dun só fío.

Ao final decidimos:

  • m5.4 x grande — para vixilancia (Prometeo);
  • c5.4xgrande - para a carga de traballo de Node.js (carga de traballo dun só fío);
  • c5.2xgrande - para Java e Go (carga de traballo multiproceso);
  • c5.4xgrande — para o panel de control (3 nodos).

A migración

Un dos pasos preparatorios para migrar da antiga infraestrutura a Kubernetes foi redirixir a comunicación directa existente entre os servizos aos novos equilibradores de carga (Elastic Load Balancers (ELB). Creáronse nunha subrede específica dunha nube privada virtual (VPC). Esta subrede conectouse a unha VPC de Kubernetes. Isto permitiunos migrar módulos gradualmente, sen ter en conta a orde específica das dependencias do servizo.

Estes puntos finais creáronse utilizando conxuntos ponderados de rexistros DNS que tiñan CNAME que apuntaban a cada novo ELB. Para cambiar, engadimos unha nova entrada que apunta ao novo ELB do servizo Kubernetes cun peso de 0. A continuación, establecemos o Time To Live (TTL) da entrada definido en 0. Despois diso, os pesos antigo e novo foron definidos. axustouse lentamente e, finalmente, o 100 % da carga foi enviada a un novo servidor. Despois de completar o cambio, o valor TTL volveu a un nivel máis adecuado.

Os módulos Java que tiñamos podían facer fronte a un DNS baixo TTL, pero as aplicacións Node non. Un dos enxeñeiros reescribiu parte do código do grupo de conexións e envolveuno nun xestor que actualizaba os grupos cada 60 segundos. O enfoque escollido funcionou moi ben e sen ningunha degradación notable do rendemento.

Leccións

Os límites do tecido de rede

Na madrugada do 8 de xaneiro de 2019, a plataforma Tinder estrelouse inesperadamente. En resposta a un aumento non relacionado na latencia da plataforma esa mañá, aumentou o número de pods e nodos do clúster. Isto fixo que a caché ARP se esgotase en todos os nosos nodos.

Hai tres opcións de Linux relacionadas coa caché ARP:

Transición de Tinder a Kubernetes
(fonte)

gc_thresh3 - Este é un límite duro. A aparición de entradas de "desbordamento da táboa de veciños" no rexistro fixo que mesmo despois da recollida de lixo sincrónica (GC), non había espazo suficiente na caché ARP para almacenar a entrada veciña. Neste caso, o núcleo simplemente descartou o paquete por completo.

Usamos Flanela como un tecido de rede en Kubernetes. Os paquetes transmítense a través de VXLAN. VXLAN é un túnel L2 elevado sobre unha rede L3. A tecnoloxía usa a encapsulación MAC-in-UDP (MAC Address-in-User Datagram Protocol) e permite a expansión dos segmentos de rede de capa 2. O protocolo de transporte na rede física do centro de datos é IP máis UDP.

Transición de Tinder a Kubernetes
Figura 2-1. Diagrama de franela (fonte)

Transición de Tinder a Kubernetes
Figura 2-2. paquete VXLAN (fonte)

Cada nodo de traballo de Kubernetes asigna un espazo de enderezo virtual cunha máscara /24 dun bloque /9 máis grande. Para cada nodo isto é medios unha entrada na táboa de enrutamento, unha entrada na táboa ARP (na interface flannel.1) e unha entrada na táboa de conmutación (FDB). Engádense a primeira vez que se inicia un nodo traballador ou cada vez que se descobre un novo nodo.

Ademais, a comunicación nodo-pod (ou pod-pod) pasa finalmente pola interface eth0 (como se mostra no diagrama Flannel anterior). Isto dá como resultado unha entrada adicional na táboa ARP para cada host fonte e destino correspondente.

Na nosa contorna, este tipo de comunicación é moi habitual. Para os obxectos de servizo en Kubernetes, créase un ELB e Kubernetes rexistra cada nodo co ELB. O ELB non sabe nada sobre os pods e é posible que o nodo seleccionado non sexa o destino final do paquete. A cuestión é que cando un nodo recibe un paquete do ELB, considérao tendo en conta as regras iptables para un servizo específico e selecciona aleatoriamente un pod noutro nodo.

No momento do fallo, había 605 nodos no clúster. Polos motivos anteriormente expostos, isto foi suficiente para superar a importancia gc_thresh3, que é o predeterminado. Cando isto ocorre, non só comezan a soltarse paquetes, senón que todo o espazo de enderezos virtuais Flannel cunha máscara /24 desaparece da táboa ARP. Interrómpese a comunicación entre nodos e as consultas DNS (o DNS está aloxado nun clúster; lea máis adiante neste artigo para obter máis información).

Para resolver este problema, cómpre aumentar os valores gc_thresh1, gc_thresh2 и gc_thresh3 e reinicie Flannel para volver rexistrar as redes que faltan.

Escalado de DNS inesperado

Durante o proceso de migración, utilizamos activamente DNS para xestionar o tráfico e transferir gradualmente servizos da antiga infraestrutura a Kubernetes. Establecemos valores TTL relativamente baixos para RecordSets asociados en Route53. Cando a antiga infraestrutura estaba a executarse en instancias EC2, a nosa configuración de resolución apuntaba a Amazon DNS. Damos isto por feito e o impacto do baixo TTL nos nosos servizos e servizos de Amazon (como DynamoDB) pasou desapercibido.

Mentres migramos os servizos a Kubernetes, descubrimos que DNS procesaba 250 mil solicitudes por segundo. Como resultado, as aplicacións comezaron a experimentar tempo de espera constante e grave para as consultas de DNS. Isto ocorreu a pesar dos esforzos incribles para optimizar e cambiar o provedor de DNS a CoreDNS (que na carga máxima alcanzou os 1000 pods que funcionan con 120 núcleos).

Mentres investigamos outras posibles causas e solucións, descubrimos un artigo, describindo as condicións de carreira que afectan ao marco de filtrado de paquetes filtro neto en Linux. Os tempos mortos que observamos, unidos a un contador crecente insert_failed na interface Flannel foron consistentes coas conclusións do artigo.

O problema prodúcese na fase de tradución de enderezos de rede de orixe e destino (SNAT e DNAT) e a posterior entrada na táboa contratrack. Unha das solucións alternativas discutidas internamente e suxeridas pola comunidade foi mover o DNS ao propio nodo traballador. Neste caso:

  • SNAT non é necesario porque o tráfico permanece dentro do nodo. Non é necesario enrutar a través da interface eth0.
  • Non se precisa DNAT xa que a IP de destino é local do nodo e non é un pod seleccionado ao azar segundo as regras iptables.

Decidimos seguir con este enfoque. CoreDNS implantouse como DaemonSet en Kubernetes e implementamos un servidor DNS de nodo local en resolver.conf cada vaina colocando unha bandeira --cluster-dns ordes cubeta . Esta solución resultou ser eficaz para os tempos de espera de DNS.

Non obstante, aínda vimos a perda de paquetes e un aumento do contador insert_failed na interface Flannel. Isto continuou despois de que se implementara a solución porque puidemos eliminar SNAT e/ou DNAT só para o tráfico DNS. Preserváronse as condicións da carreira para outros tipos de tráfico. Afortunadamente, a maioría dos nosos paquetes son TCP, e se ocorre algún problema, simplemente retransmítense. Seguimos tratando de atopar unha solución adecuada para todo tipo de tráfico.

Usando Envoy para un mellor equilibrio de carga

A medida que migramos os servizos de backend a Kubernetes, comezamos a sufrir unha carga desequilibrada entre os pods. Descubrimos que HTTP Keepalive fixo que as conexións ELB se colgasen nos primeiros pods listos de cada implementación. Así, a maior parte do tráfico pasou por unha pequena porcentaxe de pods dispoñibles. A primeira solución que probamos foi establecer MaxSurge ao 100 % en novos despregamentos para os peores escenarios. O efecto resultou ser insignificante e pouco prometedor en canto a implantacións máis grandes.

Outra solución que utilizamos foi aumentar artificialmente as solicitudes de recursos para servizos críticos. Neste caso, as vainas colocadas preto terían máis espazo de manobra en comparación con outras vainas pesadas. Tampouco funcionaría a longo prazo porque sería un malgasto de recursos. Ademais, as nosas aplicacións Node eran dun só fío e, en consecuencia, só podían usar un núcleo. A única solución real era utilizar un mellor equilibrio de carga.

Hai tempo que queriamos apreciar plenamente enviado. A situación actual permitiunos implantalo dun xeito moi limitado e obter resultados inmediatos. Envoy é un proxy de capa XNUMX de código aberto de alto rendemento deseñado para grandes aplicacións SOA. Pode implementar técnicas avanzadas de equilibrio de carga, incluíndo reintentos automáticos, interruptores automáticos e limitación de taxa global. (Nota. transl.: Podes ler máis sobre isto en Este artigo sobre Istio, que se basea en Envoy.)

Creamos a seguinte configuración: ter un sidecar Envoy para cada pod e unha única ruta, e conectar o clúster ao contedor localmente a través do porto. Para minimizar o potencial en cascada e manter un pequeno radio de impacto, utilizamos unha flota de pods proxy frontales de Envoy, un por zona de dispoñibilidade (AZ) para cada servizo. Contaban cun motor de descubrimento de servizos sinxelo escrito por un dos nosos enxeñeiros que simplemente devolveu unha lista de pods en cada AZ para un determinado servizo.

Os front-Envoys de servizo usaron entón este mecanismo de descubrimento de servizos cun clúster e ruta ascendentes. Establecemos tempos de espera adecuados, aumentamos todas as configuracións dos interruptores de circuito e engadimos unha configuración mínima de reintentos para axudar con fallos únicos e garantir unha implantación fluida. Colocamos un TCP ELB diante de cada un destes enviados de servizo. Aínda que o keepalive da nosa capa de proxy principal estaba atascado nalgúns pods de Envoy, aínda puideron xestionar a carga moito mellor e estaban configurados para equilibrar a través de least_request no backend.

Para a implantación, usamos o gancho preStop tanto en módulos de aplicación como en módulos sidecar. O gancho provocou un erro ao comprobar o estado do punto final de administración situado no contedor do sidecar e quedou inactiva durante un tempo para permitir que as conexións activas finalizasen.

Unha das razóns polas que puidemos movernos tan rápido débese ás métricas detalladas que puidemos integrar facilmente nunha instalación típica de Prometheus. Isto permitiunos ver exactamente o que estaba a suceder mentres axustabamos os parámetros de configuración e redistribuíamos o tráfico.

Os resultados foron inmediatos e evidentes. Comezamos cos servizos máis desequilibrados, e nestes momentos opera fronte aos 12 servizos máis importantes do clúster. Este ano estamos a planear unha transición a unha malla de servizo completo con descubrimento de servizos máis avanzados, interrupcións de circuítos, detección de valores atípicos, limitación de taxas e rastrexo.

Transición de Tinder a Kubernetes
Figura 3-1. Converxencia da CPU dun servizo durante a transición a Envoy

Transición de Tinder a Kubernetes

Transición de Tinder a Kubernetes

Resultado final

A través desta experiencia e investigación adicional, creamos un equipo de infraestrutura forte con fortes habilidades para deseñar, implantar e operar grandes clústeres de Kubernetes. Todos os enxeñeiros de Tinder teñen agora o coñecemento e a experiencia para empaquetar contedores e implementar aplicacións en Kubernetes.

Cando xurdiu a necesidade de capacidade adicional na antiga infraestrutura, tivemos que esperar varios minutos para que se lanzaran novas instancias EC2. Agora os contedores comezan a funcionar e comezan a procesar o tráfico en segundos en lugar de minutos. A programación de varios contedores nunha única instancia EC2 tamén proporciona unha concentración horizontal mellorada. Como resultado, prevemos unha redución significativa dos custos de EC2019 en 2 en comparación co ano pasado.

A migración levou case dous anos, pero rematamos en marzo de 2019. Actualmente, a plataforma Tinder funciona exclusivamente nun clúster de Kubernetes composto por 200 servizos, 1000 nodos, 15 pods e 000 contedores en execución. A infraestrutura xa non é o dominio exclusivo dos equipos operativos. Todos os nosos enxeñeiros comparten esta responsabilidade e controlan o proceso de creación e implantación das súas aplicacións utilizando só código.

PS do tradutor

Le tamén unha serie de artigos no noso blog:

Fonte: www.habr.com

Engadir un comentario