Escalar Kubernetes a 7500 nodos

Bootcamp AI
16 min readApr 12, 2021

Traducción: OpenAI

Hemos escalado los clústeres de Kubernetes a 7500 nodos, produciendo una infraestructura escalable para modelos grandes como GPT-3 , CLIP y DALL · E , pero también para investigaciones iterativas rápidas a pequeña escala como Leyes de escala para modelos de lenguaje neuronal . El escalado de un solo clúster de Kubernetes a este tamaño rara vez se realiza y requiere un cuidado especial, pero la ventaja es una infraestructura simple que permite a nuestros equipos de investigación de aprendizaje automático moverse más rápido y escalar sin cambiar su código.

Desde nuestra última publicación sobre Escalado a 2500 nodos , hemos seguido ampliando nuestra infraestructura para satisfacer las necesidades de los investigadores, y en el proceso hemos aprendido muchas lecciones adicionales. Esta publicación resume esas lecciones para que otros miembros de la comunidad de Kubernetes puedan beneficiarse de ellas y termina con los problemas que aún enfrentamos y que abordaremos a continuación.

Nuestra carga de trabajo

Antes de llegar demasiado lejos, es importante describir nuestra carga de trabajo. Las aplicaciones y el hardware que ejecutamos con Kubernetes son bastante diferentes de los que puede encontrar en una empresa típica. ¡Nuestros problemas y las soluciones correspondientes pueden, o no, coincidir bien con su propia configuración!

Un gran trabajo de aprendizaje automático abarca muchos nodos y se ejecuta de manera más eficiente cuando tiene acceso a todos los recursos de hardware en cada nodo. Esto permite que las GPU se comuniquen directamente mediante NVLink o que las GPU se comuniquen directamente con la NIC mediante GPUDirect . Entonces, para muchas de nuestras cargas de trabajo, un solo pod ocupa todo el nodo. Cualquier conflicto de recursos de NUMA, CPU o PCIE no son factores para la programación. El empaquetado o la fragmentación no es un problema común. Nuestros clústeres actuales tienen un ancho de banda de bisección completo, por lo que tampoco hacemos consideraciones de topología de red o rack. Todo esto significa que, si bien tenemos muchos nodos, existe una tensión relativamente baja en el programador.

Dicho esto, la tensión en el programador de kube es puntiaguda. Un nuevo job puede consistir en muchos cientos de grupos que se crean a la vez y luego vuelven a una tasa de rotación relativamente baja.

Nuestros trabajos más importantes ejecutan MPI y todos los módulos dentro del trabajo participan en un solo comunicador MPI. Si alguno de los grupos participantes muere, todo el trabajo se detiene y debe reiniciarse. El trabajo se revisa periódicamente y, cuando se reinicia, se reanuda desde el último punto de control. Por lo tanto, consideramos que las vainas son semiestatales: las vainas muertas se pueden reemplazar y el trabajo puede continuar, pero hacerlo es perjudicial y debe mantenerse al mínimo.

No confiamos tanto en el equilibrio de carga de Kubernetes. Tenemos muy poco tráfico HTTPS, sin necesidad de pruebas A / B, blue/ green o canary. Los pods se comunican directamente entre sí en sus direcciones IP de pod con MPI a través de SSH, no a los puntos finales de servicio. El “descubrimiento” del servicio es limitado; solo hacemos una búsqueda única de qué pods están participando en MPI en el momento del inicio del trabajo.

La mayoría de los trabajos interactúan con alguna forma de almacenamiento de blobs. Por lo general, transmiten algunos fragmentos de un conjunto de datos o un punto de control directamente desde el almacenamiento de blobs, o lo almacenan en caché en un disco efímero local rápido. Tenemos algunos PersistentVolumes para los casos en los que la semántica POSIX es útil, pero el almacenamiento de blobs es mucho más escalable y no requiere operaciones lentas de desconexión / conexión.

Por último, la naturaleza de nuestro trabajo es fundamentalmente de investigación, lo que significa que las cargas de trabajo en sí mismas cambian constantemente. Si bien el equipo de Supercomputación se esfuerza por proporcionar lo que consideraríamos un nivel de calidad de “producción” de infraestructura informática, las aplicaciones que se ejecutan en ese clúster son de corta duración y sus desarrolladores iteran rápidamente. En cualquier momento pueden surgir nuevos patrones de uso que desafíen nuestras suposiciones sobre las tendencias y las compensaciones apropiadas. Necesitamos un sistema sostenible que también nos permita responder rápidamente cuando las cosas cambian.

Redes

A medida que aumentaba la cantidad de nodos y vainas dentro de nuestros clústeres, descubrimos que Flannel tenía dificultades para escalar el rendimiento requerido. Cambiamos al uso de las tecnologías de redes nativas de pod para nuestras configuraciones de IP para Azure VMSSes y los complementos CNI relevantes. Esto nos permitió obtener un rendimiento de red a nivel de host en nuestros pods.

Otra razón por la que hemos cambiado al uso de direcciones IP basadas en alias es que en nuestros clústeres más grandes, posiblemente podríamos tener aproximadamente 200.000 direcciones IP en uso en cualquier momento. Cuando probamos las redes de pods basadas en rutas, descubrimos que había limitaciones significativas en la cantidad de rutas que podíamos usar de manera efectiva.

Evitar la encapsulación aumenta las demandas en el motor de enrutamiento o SDN subyacente, pero mantiene la configuración de nuestra red simple. Se puede agregar VPN o tunelización sin ningún adaptador adicional. No debemos preocuparnos por la fragmentación de paquetes debido a que una parte de la red tiene un MTU más bajo. Las políticas de red y la supervisión del tráfico son sencillas; no hay ambigüedad sobre el origen y el destino de los paquetes.

Usamos el etiquetado de iptables en el host para rastrear el uso de recursos de red por espacio de nombres y pod. Esto permite a los investigadores visualizar sus patrones de uso de la red. En particular, dado que muchos de nuestros experimentos tienen patrones de comunicación distintos de Internet e intra-pod, a menudo es útil poder investigar dónde podrían estar ocurriendo los cuellos de botella.

Las manglereglas de iptables se pueden utilizar para marcar arbitrariamente paquetes que coincidan con criterios particulares. Estas son nuestras reglas para detectar si el tráfico es interno o está vinculado a Internet. Las FORWARDreglas cubren el tráfico de los pods INPUTy el OUTPUTtráfico del host:

iptables -t mangle -A INPUT ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"
iptables -t mangle -A FORWARD ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"
iptables -t mangle -A OUTPUT ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"
iptables -t mangle -A FORWARD ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"

Una vez marcados, iptables iniciará contadores para rastrear el número de bytes y paquetes que coinciden con esta regla. Puede observar estos contadores utilizándose a iptables sí mismo:

% iptables -t mangle -L -v
Chain FORWARD (policy ACCEPT 50M packets, 334G bytes)
pkts bytes target prot opt in out source destination
....
1253K 555M all -- any any anywhere !10.0.0.0/8 /* iptables-exporter openai traffic=internet-out */
1161K 7937M all -- any any !10.0.0.0/8 anywhere /* iptables-exporter openai traffic=internet-in */

Usamos un exportador Prometheus de código abierto llamado iptables-exporter para luego rastrearlos en nuestro sistema de monitoreo. Esta es una forma sencilla de rastrear paquetes que coinciden con una variedad de diferentes tipos de condiciones.

Un aspecto algo único de nuestro modelo de red es que exponemos completamente los rangos de CIDR de red de servicio, pod y nodo a nuestros investigadores. Tenemos un modelo de red de concentrador y radio, y utilizamos los rangos CIDR de nodo y pod nativos para enrutar ese tráfico. Los investigadores se conectan al concentrador y desde allí tienen acceso a cualquiera de los grupos individuales (los radios). Pero los grupos en sí mismos no pueden comunicarse entre sí. Esto asegura que los clústeres permanezcan aislados sin dependencias entre clústeres que puedan romper el aislamiento de fallas.

Usamos un host “NAT” para traducir el rango CIDR de la red de servicio para el tráfico que proviene de fuera del clúster. Esta configuración permite a nuestros investigadores una flexibilidad significativa para elegir cómo y qué tipo de configuraciones de red pueden elegir para sus experimentos.

Servidores API

Los servidores API de Kubernetes y etcd son componentes críticos para un clúster de trabajo saludable, por lo que prestamos especial atención al estrés en estos sistemas. Usamos los paneles de Grafana proporcionados por kube-prometheus , así como paneles internos adicionales. Hemos encontrado útil alertar sobre la tasa de estado HTTP 429 (Demasiadas solicitudes) y 5xx (Error del servidor) en los servidores API como una señal de problemas de alto nivel.

Si bien algunas personas ejecutan servidores API dentro de kube, siempre los hemos ejecutado fuera del clúster. Los servidores etcd y API se ejecutan en sus propios nodos dedicados. Nuestros clústeres más grandes ejecutan 5 servidores API y 5 nodos etcd para distribuir la carga y minimizar el impacto si uno llegara a fallar. No hemos tenido problemas notables con etcd desde que dividimos los eventos de Kubernetes en su propio clúster etcd en nuestra última publicación de blog . Los servidores API no tienen estado y, por lo general, son fáciles de ejecutar en un grupo de instancias de recuperación automática o un conjunto de escalas. Todavía no hemos intentado construir ninguna automatización de recuperación automática de clústeres etcd porque los incidentes han sido extremadamente raros.

Los servidores API pueden ocupar bastante memoria y eso tiende a escalar linealmente con la cantidad de nodos en el clúster. Para nuestro clúster con 7500 nodos, observamos que se utilizan hasta 70 GB de almacenamiento dinámico por servidor API, por lo que, afortunadamente, esto debería seguir estando dentro de las capacidades del hardware en el futuro.

Una gran tensión en los servidores API fue WATCHes on Endpoints. Hay algunos servicios, como ‘kubelet’ y ‘node-exporter’, de los cuales todos los nodos del clúster son miembros. Cuando se agregaba o quitaba un nodo del clúster, este WATCH se activaba. Y debido a que normalmente cada nodo estaba viendo el kubeletservicio a través de kube-proxy, el # y el ancho de banda requeridos en estas respuestas seríanN ^ 2norte2y enorme, ocasionalmente 1GB / so más. EndpointSlices , lanzado en Kubernetes 1.17, fue un gran beneficio que redujo esta carga 1000 veces.

En general, somos muy conscientes de las solicitudes de API Server que se adaptan al tamaño del clúster. Intentamos evitar que los DaemonSets interactúen con el servidor API. En los casos en los que necesite que cada nodo esté atento a los cambios, la introducción de un servicio de almacenamiento en caché intermedio, como Datadog Cluster Agent , parece ser un buen patrón para evitar cuellos de botella en todo el clúster.

A medida que nuestros clústeres crecen, realizamos menos autoescalado real de nuestros clústeres. Pero ocasionalmente nos hemos encontrado con problemas cuando realizamos un ajuste de escala automático demasiado a la vez. Se generan muchas solicitudes cuando un nuevo nodo se une a un clúster, y agregar cientos de nodos a la vez puede sobrecargar la capacidad del servidor API. Suavizar esto, aunque solo sea por unos segundos, ha ayudado a evitar interrupciones.

Métricas de series de tiempo con Prometheus y Grafana

Usamos Prometheus para recopilar métricas de series de tiempo y Grafana para gráficos, paneles y alertas. Comenzamos con una implementación de kube-prometheus que recopila una amplia variedad de métricas y buenos cuadros de mando para la visualización. Con el tiempo, hemos agregado muchos de nuestros propios paneles, métricas y alertas.

A medida que agregamos más y más nodos, luchamos con la gran cantidad de métricas recopiladas por Prometheus. Si bien kube-prometheus expone una gran cantidad de datos útiles, algunos de ellos nunca los miramos y otros eran demasiado granulares para recopilarlos, almacenarlos y consultarlos de manera efectiva. Usamos las reglas de Prometheus para “eliminar” algunas de estas métricas de la ingestión.

Durante un tiempo, luchamos con un problema en el que Prometheus consumía cada vez más memoria hasta que finalmente colapsaba el contenedor en un error de memoria insuficiente (OOM). Esto pareció ocurrir incluso después de arrojar enormes cantidades de capacidad de memoria a la aplicación. Lo que es peor fue que, cuando se bloqueó, se necesitarían muchas horas al iniciar la reproducción de los archivos de registro de escritura anticipada antes de que se pudiera usar nuevamente.

Finalmente, rastreamos la fuente de estos OOM para que fuera una interacción entre Grafana y Prometheus, donde Grafana usaría la /api/v1/seriesAPI en Prometheus con una consulta de {le!=""} (Básicamente, "dame todas las métricas del histograma"). La implementación de /api/v1/seriesfue ilimitada tanto en tiempo como en espacio; para una consulta con muchos resultados, esto continuaría consumiendo cada vez más memoria y tiempo. También continúa creciendo incluso después de que el solicitante se ha dado por vencido y ha cerrado la conexión. Para nosotros, nunca hubo suficiente memoria y Prometheus eventualmente colapsaría. Nosotros implementamos Prometeo para contener esta API dentro de un contexto para hacer cumplir un tiempo de espera, que fija por completo.

Si bien Prometheus se bloqueaba con mucha menos frecuencia, en los momentos en que necesitábamos reiniciarlo, la reproducción de WAL seguía siendo un problema. A menudo, llevaría muchas horas reproducir todos los registros de WAL antes de que Prometheus recopilara nuevas métricas y realizara consultas. Con la ayuda de Robust Perception , descubrimos que la aplicación de un GOMAXPROCS=24tenía una gran mejora. Prometheus intenta usar todos los núcleos durante la reproducción de WAL, y para servidores con una gran cantidad de núcleos, la contención mata todo el rendimiento.

Estamos explorando nuevas opciones para aumentar nuestra capacidad de monitoreo, descritas en la sección “ Problemas sin resolver “ a continuación.

Chequeos de salud

Con un clúster de este tamaño, por supuesto, confiamos en la automatización para detectar y eliminar los nodos que se comportan mal del clúster. Con el tiempo, hemos creado varios sistemas de chequeo de salud.

Comprobaciones de salud pasivas

Algunas comprobaciones de estado son pasivas y siempre se ejecutan en todos los nodos. Estos monitorean los recursos básicos del sistema, como la accesibilidad de la red, discos defectuosos o completos o errores de GPU. Las GPU presentan problemas de diferentes formas, pero una de las más comunes es un “error ECC incorregible”. Las herramientas de Data Center GPU Manager (DCGM) de Nvidia facilitan la consulta de este y otros errores “Xid”. Una forma de rastrear estos errores es a través de dcgm-exporter para incorporar las métricas en Prometheus, nuestro sistema de monitoreo. Esto aparecerá como la DCGM_FI_DEV_XID_ERRORSmétrica y se establecerá con el código de error que se haya producido más recientemente. Además, la API de consulta de dispositivos NVML expone información más detallada sobre el estado y el funcionamiento de una GPU.

Una vez que detectamos un error, a menudo se pueden solucionar reiniciando la GPU o el sistema, aunque en algunos casos hace que la GPU subyacente deba ser reemplazada físicamente.

Otra forma de chequeo de salud rastrea los eventos de mantenimiento del proveedor de nube ascendente. Cada uno de los principales proveedores de la nube expone una forma de saber si la VM actual debe recibir un próximo evento de mantenimiento que eventualmente causará una interrupción. Es posible que sea necesario reiniciar la máquina virtual para poder aplicar un parche de hipervisor subyacente o intercambiar el nodo físico por otro hardware.

Estas comprobaciones de estado pasivas se ejecutan constantemente en segundo plano en todos los nodos. Si una verificación de estado comienza a fallar, el nodo se acordona automáticamente para que no se programen nuevos pods en el nodo. En el caso de fallas de control de estado más graves, también intentaremos desalojar el módulo para solicitar que todos los módulos que se estén ejecutando actualmente salgan de inmediato. Aún depende del módulo en sí, configurable a través de un presupuesto de interrupción del módulo, decidir si quiere permitir que se produzca este desalojo. Eventualmente, ya sea después de que todos los pods hayan terminado, o después de que hayan transcurrido 7 días (parte de nuestro SLA), cancelaremos la VM por la fuerza.

Pruebas de GPU activas

Desafortunadamente, no todos los problemas de la GPU se manifiestan como códigos de error visibles a través de DCGM. Hemos creado nuestra propia biblioteca de pruebas que ejercitan las GPU para detectar problemas adicionales y garantizar que el hardware y el controlador se comporten como se esperaba. Estas pruebas no se pueden ejecutar en segundo plano; requieren el uso exclusivo de una GPU durante varios segundos o minutos para ejecutarse.

Primero ejecutamos estas pruebas en los nodos al arrancar, en un sistema que llamamos “verificación previa”. Todos los nodos se unen al clúster con una mancha de “verificación previa” y una etiqueta aplicada. Esta mancha evita que se programen pods normales en el nodo. Un DaemonSet está configurado para ejecutar pods de prueba previa al vuelo en todos los nodos con esta etiqueta. Una vez finalizada con éxito la prueba, la prueba en sí misma elimina la mancha y la etiqueta y el nodo está disponible para uso general.

Luego, también ejecutamos estas pruebas periódicamente durante la vida útil de un nodo. Ejecutamos esto como un CronJob, lo que le permite aterrizar en cualquier nodo disponible en el clúster. Es cierto que esto es un poco aleatorio y descontrolado sobre qué nodos se prueban, pero hemos descubierto que con el tiempo proporciona una cobertura suficiente con una mínima coordinación o interrupción.

Cuotas y uso de recursos

A medida que ampliamos nuestras agrupaciones, los investigadores empezaron a tener dificultades para obtener toda la capacidad que se les asignó. Los sistemas tradicionales de programación de trabajos tienen muchas características diferentes disponibles para ejecutar el trabajo de manera justa entre equipos rivales, lo que Kubernetes no tiene. Con el tiempo, nos inspiramos en esos sistemas de programación de trabajos y desarrollamos varias capacidades de forma nativa de Kubernetes.

Manchas de equipo

Tenemos un servicio en cada cluster, “team-resource-manager” que tiene múltiples funciones. Su fuente de datos es un ConfigMap que especifica tuplas de (selector de nodo, etiqueta de equipo a aplicar, cantidad de asignación) para todos los equipos de investigación que tienen capacidad en un clúster determinado. Concilia esto con los nodos actuales en el clúster, contaminando el número apropiado de nodos con openai.com/team=teamname:NoSchedule.

“Team-resource-manager” también tiene un servicio de webhook de admisión, de modo que a medida que se envía cada trabajo, se aplica la tolerancia correspondiente en función de la membresía del equipo del remitente. El uso de taints nos permite restringir el programador de pod de Kubernetes de manera flexible, como permitir una tolerancia “cualquiera” para los pods de menor prioridad, lo que permite a los equipos tomar prestada la capacidad de los demás sin requerir una coordinación de peso.

Globos de CPU y GPU

Además de usar el escalador automático de clústeres para escalar dinámicamente nuestros clústeres respaldados por VM, lo usamos para corregir (eliminar y volver a agregar) miembros en mal estado dentro del clúster. Hacemos esto estableciendo el “tamaño mínimo” del clúster en cero y el “tamaño máximo” del clúster en la capacidad disponible. Sin embargo, el escalador automático de clúster, si ve nodos inactivos, intentará reducir la escala a la capacidad necesaria. Por varias razones (latencia de giro de VM, costos preasignados, los impactos del servidor de API mencionados anteriormente), este escalado inactivo no es ideal.

Por lo tanto, presentamos una implementación de globo para nuestros hosts de solo CPU y GPU. Esta implementación contiene un ReplicaSet con un número de “tamaño máximo” de pods de baja prioridad. Estos pods ocupan recursos dentro de un nodo, por lo que el escalador automático no los considera inactivos. Sin embargo, dado que son de baja prioridad, el programador puede desalojarlos inmediatamente para dejar espacio para el trabajo real. (Elegimos usar una implementación en lugar de un DaemonSet, para evitar que DaemonSet se considere una carga de trabajo inactiva en un nodo).

Una cosa a tener en cuenta es que utilizamos la antiafinidad de los pods para garantizar que los pods se distribuyan uniformemente entre los nodos. Las versiones anteriores del programador de Kubernetes tenían unaO (N ^ 2)O ( N2)Problema de rendimiento con antiafinidad de pod. Esto se ha corregido desde Kubernetes 1.18.

Programación de pandillas

Nuestros experimentos a menudo involucran uno o más StatefulSets, cada uno operando una parte diferente del esfuerzo de capacitación. Para los optimizadores, los investigadores necesitan que todos los miembros de StatefulSet estén programados, antes de que se pueda realizar cualquier capacitación (ya que a menudo usamos MPI para coordinar entre los miembros del optimizador, y MPI es sensible a los cambios de membresía de grupo).

Sin embargo, Kubernetes de forma predeterminada no priorizará necesariamente el cumplimiento de todas las solicitudes de un StatefulSet sobre otro. Por ejemplo, si dos experimentos solicitaban cada uno el 100% de la capacidad del clúster, en lugar de programar todo un experimento o el otro, Kubernetes podría programar solo la mitad de los grupos de cada experimento, lo que lleva a un punto muerto en el que ninguno de los experimentos puede avanzar.

Probamos algunas cosas que necesitaban un programador personalizado, pero encontramos casos extremos que causaban conflictos con la forma en que se programaban los pods normales. Kubernetes 1.18 introdujo una arquitectura de complementos para el programador central de Kubernetes, lo que facilita mucho la adición de características como esta de forma nativa. Recientemente aterrizamos en el complemento Coscheduling como una buena manera de resolver este problema.

Problemas no resueltos

Aún quedan muchos problemas por resolver a medida que ampliamos nuestros clústeres de Kubernetes. Algunos de ellos incluyen:

Métrica

A nuestra escala, hemos tenido muchas dificultades con el motor de almacenamiento TSDB integrado de Prometheus que es lento para compactar y necesita mucho tiempo para reproducir el WAL (Write-Ahead-Log) cada vez que se reinicia. Las consultas también tienden a dar como resultado errores de “procesamiento de consultas cargaría demasiadas muestras”. Estamos en el proceso de migrar a un motor de consultas y almacenamiento compatible con Prometheus diferente. ¡Esperamos una futura publicación de blog sobre cómo va!

Conformación del tráfico de la red de vainas

A medida que ampliamos nuestros clústeres, se calcula que cada pod tiene una cierta cantidad de ancho de banda de Internet disponible. Los requisitos agregados de ancho de banda de Internet por persona se han vuelto sustanciales, y nuestros investigadores ahora tienen la capacidad de poner involuntariamente una carga significativa de recursos en otras ubicaciones de Internet, como conjuntos de datos para descargar y paquetes de software para instalar.

Conclusiones

Descubrimos que Kubernetes es una plataforma excepcionalmente flexible para nuestras necesidades de investigación. Tiene la capacidad de escalar para cumplir con las cargas de trabajo más exigentes que le hemos puesto. Sin embargo, todavía hay muchas áreas en las que necesita mejoras, y el equipo de supercomputación de OpenAI continuará explorando cómo Kubernetes puede escalar. Si este tipo de trabajo parece interesante, ¡debería considerar postularse en OpenAI!

--

--