One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

¡Aloha, gente! Mi nombre es Oleg Anastasyev, trabajo en Odnoklassniki en el equipo de Plataforma. Y además de mí, hay mucho hardware funcionando en Odnoklassniki. Contamos con cuatro centros de datos con alrededor de 500 racks con más de 8 mil servidores. En cierto momento, nos dimos cuenta de que la introducción de un nuevo sistema de gestión nos permitiría cargar equipos de manera más eficiente, facilitar la gestión de accesos, automatizar la (re)distribución de recursos informáticos, acelerar el lanzamiento de nuevos servicios y acelerar las respuestas. a accidentes de gran escala.

¿Qué resultó de esto?

Además de mí y de un montón de hardware, también hay personas que trabajan con este hardware: ingenieros que están ubicados directamente en los centros de datos; usuarios de redes que configuran software de red; administradores, o SRE, que brindan resiliencia a la infraestructura; y equipos de desarrollo, cada uno de ellos es responsable de parte de las funciones del portal. El software que crean funciona más o menos así:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Las solicitudes de los usuarios se reciben tanto en los frentes del portal principal www.ok.ruy en otros, por ejemplo en los frentes de API de música. Para procesar la lógica empresarial, llaman al servidor de aplicaciones, que, al procesar la solicitud, llama a los microservicios especializados necesarios: one-graph (gráfico de conexiones sociales), user-cache (caché de perfiles de usuario), etc.

Cada uno de estos servicios se despliega en muchas máquinas, y cada una de ellas cuenta con desarrolladores responsables del funcionamiento de los módulos, su operación y desarrollo tecnológico. Todos estos servicios se ejecutan en servidores de hardware y hasta hace poco lanzábamos exactamente una tarea por servidor, es decir, estaba especializado para una tarea específica.

¿Porqué es eso? Este enfoque tenía varias ventajas:

  • Aliviado gestión masiva. Digamos que una tarea requiere algunas bibliotecas, algunas configuraciones. Y luego, el servidor se asigna exactamente a un grupo específico, se describe la política de cfengine para este grupo (o ya se ha descrito) y esta configuración se implementa de forma centralizada y automática en todos los servidores de este grupo.
  • Simplificado diagnóstico. Digamos que observa el aumento de carga en el procesador central y se da cuenta de que esta carga solo podría ser generada por la tarea que se ejecuta en este procesador de hardware. La búsqueda de un culpable termina muy rápidamente.
  • Simplificado monitoreo. Si algo anda mal con el servidor, el monitor lo informa y usted sabe exactamente quién tiene la culpa.

A un servicio que consta de varias réplicas se le asignan varios servidores, uno para cada uno. Luego, el recurso informático para el servicio se asigna de manera muy simple: la cantidad de servidores que tiene el servicio, la cantidad máxima de recursos que puede consumir. “Fácil” aquí no significa que sea fácil de usar, sino en el sentido de que la asignación de recursos se realiza manualmente.

Este enfoque también nos permitió hacer configuraciones de hierro especializadas para una tarea que se ejecuta en este servidor. Si la tarea almacena grandes cantidades de datos, entonces utilizamos un servidor 4U con un chasis con 38 discos. Si la tarea es puramente computacional, entonces podemos comprar un servidor 1U más económico. Esto es computacionalmente eficiente. Entre otras cosas, este enfoque nos permite utilizar cuatro veces menos máquinas con una carga comparable a la de una red social amigable.

Esta eficiencia en el uso de los recursos informáticos también debería garantizar la eficiencia económica, si partimos de la premisa de que lo más caro son los servidores. Durante mucho tiempo, el hardware fue el más caro y nos esforzamos mucho en reducir el precio del hardware, ideando algoritmos de tolerancia a fallas para reducir los requisitos de confiabilidad del hardware. Y hoy hemos llegado al punto en el que el precio del servidor ha dejado de ser decisivo. Si no consideramos los últimos exóticos, entonces la configuración específica de los servidores en el rack no importa. Ahora tenemos otro problema: el precio del espacio que ocupa el servidor en el centro de datos, es decir, el espacio en el rack.

Al darnos cuenta de que este era el caso, decidimos calcular la eficacia con la que estábamos utilizando los bastidores.
Tomamos el precio del servidor más potente de los económicamente justificables, calculamos cuántos servidores de este tipo podríamos colocar en bastidores, cuántas tareas ejecutaríamos en ellos según el modelo antiguo “un servidor = una tarea” y cuánto tareas podrían utilizar el equipo. Contaron y derramaron lágrimas. Resultó que nuestra eficiencia en el uso de bastidores es de aproximadamente el 11%. La conclusión es obvia: necesitamos aumentar la eficiencia del uso de los centros de datos. Parecería que la solución es obvia: es necesario ejecutar varias tareas a la vez en un servidor. Pero aquí es donde empiezan las dificultades.

La configuración masiva se vuelve dramáticamente más complicada: ahora es imposible asignar un grupo a un servidor. Después de todo, ahora se pueden ejecutar varias tareas con diferentes comandos en un servidor. Además, la configuración puede ser conflictiva para diferentes aplicaciones. El diagnóstico también se vuelve más complicado: si observa un aumento en el consumo de CPU o disco en un servidor, no sabe qué tarea está causando el problema.

Pero lo principal es que no existe aislamiento entre las tareas que se ejecutan en la misma máquina. Aquí, por ejemplo, se muestra un gráfico del tiempo medio de respuesta de una tarea del servidor antes y después de que se iniciara otra aplicación informática en el mismo servidor, que no tiene ninguna relación con la primera: el tiempo de respuesta de la tarea principal ha aumentado significativamente.

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Obviamente, necesita ejecutar tareas en contenedores o en máquinas virtuales. Dado que casi todas nuestras tareas se ejecutan en un sistema operativo (Linux) o están adaptadas para él, no necesitamos admitir muchos sistemas operativos diferentes. En consecuencia, la virtualización no es necesaria; debido a la sobrecarga adicional, será menos eficiente que la contenedorización.

Como implementación de contenedores para ejecutar tareas directamente en servidores, Docker es un buen candidato: las imágenes del sistema de archivos resuelven bien los problemas con configuraciones conflictivas. El hecho de que las imágenes puedan estar compuestas por varias capas nos permite reducir significativamente la cantidad de datos necesarios para implementarlas en la infraestructura, separando las partes comunes en capas base separadas. Luego, las capas básicas (y más voluminosas) se almacenarán en caché con bastante rapidez en toda la infraestructura, y para entregar muchos tipos diferentes de aplicaciones y versiones, solo será necesario transferir pequeñas capas.

Además, un registro listo para usar y un etiquetado de imágenes en Docker nos brindan primitivas listas para usar para versionar y entregar código a producción.

Docker, como cualquier otra tecnología similar, nos proporciona cierto nivel de aislamiento de contenedores desde el primer momento. Por ejemplo, aislamiento de memoria: a cada contenedor se le asigna un límite en el uso de memoria de la máquina, más allá del cual no consumirá. También puede aislar contenedores según el uso de la CPU. Para nosotros, sin embargo, el aislamiento estándar no era suficiente. Pero más sobre eso a continuación.

La ejecución directa de contenedores en servidores es sólo una parte del problema. La otra parte está relacionada con el alojamiento de contenedores en servidores. Debe comprender qué contenedor se puede colocar en qué servidor. Esta no es una tarea tan fácil, porque los contenedores deben colocarse en los servidores lo más densamente posible sin reducir su velocidad. Esta colocación también puede resultar difícil desde el punto de vista de la tolerancia a fallos. Muchas veces queremos colocar réplicas del mismo servicio en diferentes racks o incluso en diferentes salas del centro de datos, de modo que si falla un rack o sala, no perdamos inmediatamente todas las réplicas del servicio.

Distribuir contenedores manualmente no es una opción cuando tienes 8 mil servidores y entre 8 y 16 mil contenedores.

Además, queríamos darles a los desarrolladores más independencia en la asignación de recursos para que ellos mismos pudieran alojar sus servicios en producción, sin la ayuda de un administrador. Al mismo tiempo, queríamos mantener el control para que algún servicio menor no consumiera todos los recursos de nuestros centros de datos.

Obviamente, necesitamos una capa de control que haga esto automáticamente.

Así llegamos a una imagen sencilla y comprensible que todos los arquitectos adoran: tres cuadrados.

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

one-cloud masters es un clúster de conmutación por error responsable de la orquestación de la nube. El desarrollador envía un manifiesto al maestro, que contiene toda la información necesaria para alojar el servicio. En base a esto, el maestro da órdenes a los minions seleccionados (máquinas diseñadas para ejecutar contenedores). Los minions tienen nuestro agente, que recibe el comando, envía sus comandos a Docker y Docker configura el kernel de Linux para lanzar el contenedor correspondiente. Además de ejecutar comandos, el agente informa continuamente al maestro sobre los cambios en el estado tanto de la máquina minion como de los contenedores que se ejecutan en ella.

Asignación de recursos

Ahora veamos el problema de la asignación de recursos más compleja para muchos minions.

Un recurso informático en una nube es:

  • La cantidad de potencia del procesador consumida por una tarea específica.
  • La cantidad de memoria disponible para la tarea.
  • Tráfico de red. Cada uno de los minions tiene una interfaz de red específica con ancho de banda limitado, por lo que es imposible distribuir tareas sin tener en cuenta la cantidad de datos que transmiten a través de la red.
  • Discos. Además, obviamente, del espacio para estas tareas, también asignamos el tipo de disco: HDD o SSD. Los discos pueden atender una cantidad finita de solicitudes por segundo: IOPS. Por lo tanto, para las tareas que generan más IOPS de las que un solo disco puede manejar, también asignamos "ejes", es decir, dispositivos de disco que deben reservarse exclusivamente para la tarea.

Luego, para algún servicio, por ejemplo para el caché de usuario, podemos registrar los recursos consumidos de esta manera: 400 núcleos de procesador, 2,5 TB de memoria, tráfico de 50 Gbit/s en ambas direcciones, 6 TB de espacio en disco duro ubicado en 100 ejes. O en una forma más familiar como esta:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Los recursos del servicio de caché de usuario consumen sólo una parte de todos los recursos disponibles en la infraestructura de producción. Por lo tanto, quiero asegurarme de que de repente, debido a un error del operador o no, la caché de usuario no consuma más recursos de los que se le asignan. Es decir, debemos limitar los recursos. ¿Pero a qué podríamos vincular la cuota?

Volvamos a nuestro diagrama muy simplificado de la interacción de los componentes y volvamos a dibujarlo con más detalles, como este:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Lo que llama la atención:

  • La interfaz web y la música utilizan clústeres aislados del mismo servidor de aplicaciones.
  • Podemos distinguir las capas lógicas a las que pertenecen estos clusters: frentes, cachés, almacenamiento de datos y capa de gestión.
  • La interfaz es heterogénea; consta de diferentes subsistemas funcionales.
  • Los cachés también pueden estar dispersos por el subsistema cuyos datos almacenan en caché.

Volvamos a dibujar la imagen:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

¡Bah! ¡Sí, vemos una jerarquía! Esto significa que puede distribuir recursos en porciones más grandes: asigne un desarrollador responsable a un nodo de esta jerarquía correspondiente al subsistema funcional (como "música" en la imagen) y adjunte una cuota al mismo nivel de la jerarquía. Esta jerarquía también nos permite organizar los servicios de manera más flexible para facilitar la administración. Por ejemplo, dividimos toda la web, ya que se trata de un grupo muy grande de servidores, en varios grupos más pequeños, que se muestran en la imagen como grupo1, grupo2.

Al eliminar las líneas adicionales, podemos escribir cada nodo de nuestra imagen en una forma más plana: grupo1.web.front, api.music.front, usuario-cache.cache.

Así llegamos al concepto de “cola jerárquica”. Tiene un nombre como "group1.web.front". Se le asigna una cuota de recursos y derechos de usuario. Le daremos a la persona de DevOps los derechos para enviar un servicio a la cola, y dicho empleado puede iniciar algo en la cola, y la persona de OpsDev tendrá derechos de administrador, y ahora podrá administrar la cola, asignar personas allí, otorgar derechos a estas personas, etc. Los servicios que se ejecutan en esta cola se ejecutarán dentro de la cuota de la cola. Si la cuota informática de la cola no es suficiente para ejecutar todos los servicios a la vez, se ejecutarán secuencialmente, formando así la cola misma.

Echemos un vistazo más de cerca a los servicios. Un servicio tiene un nombre completo, que siempre incluye el nombre de la cola. Entonces el servicio web frontal tendrá el nombre ok-web.group1.web.front. Y el servicio del servidor de aplicaciones al que accede se llamará ok-app.group1.web.front. Cada servicio tiene un manifiesto, que especifica toda la información necesaria para su colocación en máquinas específicas: cuántos recursos consume esta tarea, qué configuración se necesita para ello, cuántas réplicas debe haber, propiedades para manejar fallas de este servicio. Y después de que el servicio se coloca directamente en las máquinas, aparecen sus instancias. También se nombran de forma inequívoca, como el número de instancia y el nombre del servicio: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front,…

Esto es muy conveniente: con solo mirar el nombre del contenedor en ejecución, podemos descubrir muchas cosas de inmediato.

Ahora echemos un vistazo más de cerca a lo que realmente realizan estas instancias: tareas.

Clases de aislamiento de tareas

Todas las tareas en OK (y, probablemente, en todas partes) se pueden dividir en grupos:

  • Tareas de latencia corta - prod. Para tales tareas y servicios, el retraso de respuesta (latencia), es decir, la rapidez con la que el sistema procesará cada una de las solicitudes, es muy importante. Ejemplos de tareas: frentes web, cachés, servidores de aplicaciones, almacenamiento OLTP, etc.
  • Problemas de cálculo - lote. Aquí, la velocidad de procesamiento de cada solicitud específica no es importante. Para ellos, es importante cuántos cálculos realizará esta tarea en un determinado (largo) período de tiempo (rendimiento). Estas serán cualquier tarea de MapReduce, Hadoop, aprendizaje automático, estadísticas.
  • Tareas en segundo plano: inactivas. Para este tipo de tareas, ni la latencia ni el rendimiento son muy importantes. Esto incluye varias pruebas, migraciones, recálculos y conversión de datos de un formato a otro. Por un lado, son similares a los calculados, por otro lado, realmente no nos importa qué tan rápido se completen.

Veamos cómo este tipo de tareas consumen recursos, por ejemplo, el procesador central.

Tareas de corta demora. Una tarea de este tipo tendrá un patrón de consumo de CPU similar a este:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Se recibe una solicitud del usuario para su procesamiento, la tarea comienza a utilizar todos los núcleos de CPU disponibles, la procesa, devuelve una respuesta, espera la siguiente solicitud y se detiene. Llegó la siguiente solicitud: nuevamente elegimos todo lo que había allí, lo calculamos y estamos esperando la siguiente.

Para garantizar la latencia mínima para dicha tarea, debemos tomar el máximo de recursos que consume y reservar la cantidad requerida de núcleos en el minion (la máquina que ejecutará la tarea). Entonces la fórmula de reserva para nuestro problema será la siguiente:

alloc: cpu = 4 (max)

y si tenemos una máquina Minion con 16 núcleos, entonces se le pueden asignar exactamente cuatro de esas tareas. Destacamos especialmente que el consumo medio de procesador de este tipo de tareas suele ser muy bajo, lo cual es obvio, ya que una parte importante del tiempo la tarea espera una solicitud y no hace nada.

Tareas de cálculo. Su patrón será ligeramente diferente:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

El consumo medio de recursos de CPU para este tipo de tareas es bastante alto. A menudo queremos que una tarea de cálculo se complete en un tiempo determinado, por lo que debemos reservar la cantidad mínima de procesadores que necesita para que todo el cálculo se complete en un tiempo aceptable. Su fórmula de reserva se verá así:

alloc: cpu = [1,*)

“Por favor, colócalo en un minion donde haya al menos un núcleo libre, y luego, cuantos haya, lo devorará todo”.

Aquí la eficiencia de uso ya es mucho mejor que en tareas con un breve retraso. Pero la ganancia será mucho mayor si combinas ambos tipos de tareas en una máquina minion y distribuyes sus recursos sobre la marcha. Cuando una tarea con un breve retraso requiere un procesador, lo recibe inmediatamente, y cuando los recursos ya no son necesarios, se transfieren a la tarea computacional, es decir, algo como esto:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

¿Pero cómo hacerlo?

Primero, veamos prod y su asignación: cpu = 4. Necesitamos reservar cuatro núcleos. En la ejecución de Docker, esto se puede hacer de dos maneras:

  • Usando la opción --cpuset=1-4, es decir, asignar cuatro núcleos específicos en la máquina a la tarea.
  • Utilizar --cpuquota=400_000 --cpuperiod=100_000, asigne una cuota de tiempo de procesador, es decir, indique que cada 100 ms de tiempo real la tarea no consuma más de 400 ms de tiempo de procesador. Se obtienen los mismos cuatro núcleos.

¿Pero cuál de estos métodos es el adecuado?

cpuset parece bastante atractivo. La tarea tiene cuatro núcleos dedicados, lo que significa que los cachés del procesador funcionarán de la manera más eficiente posible. Esto también tiene una desventaja: tendríamos que asumir la tarea de distribuir los cálculos entre los núcleos descargados de la máquina en lugar del sistema operativo, y esta es una tarea bastante no trivial, especialmente si intentamos colocar tareas por lotes en tal máquina. Las pruebas han demostrado que la opción con cuota es más adecuada en este caso: de esta manera el sistema operativo tiene más libertad para elegir el núcleo para realizar la tarea en el momento actual y el tiempo del procesador se distribuye de manera más eficiente.

Averigüemos cómo hacer reservas en Docker según la cantidad mínima de núcleos. La cuota para tareas por lotes ya no es aplicable, porque no es necesario limitar el máximo, basta con garantizar el mínimo. Y aquí encaja bien la opción. docker run --cpushares.

Acordamos que si un lote requiere garantía para al menos un núcleo, entonces indicamos --cpushares=1024, y si hay al menos dos núcleos, entonces indicamos --cpushares=2048. Las cuotas de CPU no interfieren de ninguna manera con la distribución del tiempo del procesador siempre que haya suficiente. Por lo tanto, si prod no está usando actualmente sus cuatro núcleos, no hay nada que limite las tareas por lotes y pueden usar tiempo de procesador adicional. Pero en una situación en la que hay escasez de procesadores, si prod ha consumido sus cuatro núcleos y ha alcanzado su cuota, el tiempo restante del procesador se dividirá proporcionalmente a las cpushares, es decir, en una situación de tres núcleos libres, uno será asignados a una tarea con 1024 cpushares, y los dos restantes se asignarán a una tarea con 2048 cpushares.

Pero utilizar cuotas y acciones no es suficiente. Necesitamos asegurarnos de que una tarea con un retraso breve reciba prioridad sobre una tarea por lotes al asignar tiempo de procesador. Sin dicha priorización, la tarea por lotes ocupará todo el tiempo del procesador en el momento en que el producto la necesite. No hay opciones de priorización de contenedores en la ejecución de Docker, pero las políticas del programador de CPU de Linux son útiles. Puedes leer sobre ellos en detalle. aquí, y en el marco de este artículo los repasaremos brevemente:

  • SCHED_OTHER
    De forma predeterminada, todos los procesos de usuario normales en una máquina Linux reciben.
  • SCHED_BATCH
    Diseñado para procesos que consumen muchos recursos. Al colocar una tarea en un procesador, se introduce la llamada penalización de activación: es menos probable que dicha tarea reciba recursos del procesador si actualmente está siendo utilizada por una tarea con SCHED_OTHER
  • SCHED_IDLE
    Un proceso en segundo plano con una prioridad muy baja, incluso inferior a la agradable -19. Usamos nuestra biblioteca de código abierto. uno-nio, para establecer la política necesaria al iniciar el contenedor llamando

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Pero incluso si no programas en Java, puedes hacer lo mismo usando el comando chrt:

chrt -i 0 $pid

Resumamos todos nuestros niveles de aislamiento en una tabla para mayor claridad:

Clase de aislamiento
Ejemplo de asignación
Opciones de ejecución de Docker
sched_setscheduler chrt*

Aguijón
procesador = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Lote
CPU = [1, *)
--cpushares=1024
SCHED_BATCH

Idle
CPU= [2, *)
--cpushares=2048
SCHED_IDLE

*Si está haciendo chrt desde dentro de un contenedor, es posible que necesite la capacidad sys_nice, porque de forma predeterminada Docker elimina esta capacidad al iniciar el contenedor.

Pero las tareas no sólo consumen procesador, sino también tráfico, lo que afecta la latencia de una tarea de red incluso más que la asignación incorrecta de recursos del procesador. Por lo tanto, naturalmente queremos obtener exactamente la misma imagen del tráfico. Es decir, cuando una tarea de producción envía algunos paquetes a la red, limitamos la velocidad máxima (fórmula asignación: lan=[*,500mbps) ), con el que prod puede hacer esto. Y para lotes garantizamos solo el rendimiento mínimo, pero no limitamos el máximo (fórmula asignación: lan=[10Mbps,*) ) En este caso, el tráfico de producción debe tener prioridad sobre las tareas por lotes.
Aquí Docker no tiene ninguna primitiva que podamos usar. Pero viene en nuestra ayuda. Control de tráfico de Linux. Pudimos lograr el resultado deseado con la ayuda de la disciplina. Curva Jerárquica de Servicio Justo. Con su ayuda, distinguimos dos clases de tráfico: producción de alta prioridad y lote/inactivo de baja prioridad. Como resultado, la configuración para el tráfico saliente es la siguiente:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

aquí 1:0 es la “qdisc raíz” de la disciplina hsfc; 1:1 - clase secundaria hsfc con un límite de ancho de banda total de 8 Gbit/s, bajo el cual se ubican las clases secundarias de todos los contenedores; 1:2: la clase secundaria hsfc es común a todas las tareas inactivas y por lotes con un límite "dinámico", que se analiza a continuación. Las clases secundarias de hsfc restantes son clases dedicadas para ejecutar contenedores de productos actualmente con límites correspondientes a sus manifiestos: 450 y 400 Mbit/s. A cada clase hsfc se le asigna una cola qdisc fq o fq_codel, según la versión del kernel de Linux, para evitar la pérdida de paquetes durante las ráfagas de tráfico.

Normalmente, las disciplinas TC sirven para priorizar únicamente el tráfico saliente. Pero también queremos priorizar el tráfico entrante; después de todo, algunas tareas por lotes pueden seleccionar fácilmente todo el canal entrante y recibir, por ejemplo, un gran lote de datos de entrada para map&reduce. Para esto utilizamos el módulo ifb, que crea una interfaz virtual ifbX para cada interfaz de red y redirige el tráfico entrante desde la interfaz al tráfico saliente en ifbX. Además, para ifbX, todas las mismas disciplinas funcionan para controlar el tráfico saliente, para lo cual la configuración de hsfc será muy similar:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Durante los experimentos, descubrimos que hsfc muestra los mejores resultados cuando la clase 1:2 de tráfico por lotes/inactivo sin prioridad se limita en las máquinas minion a no más de un determinado carril libre. De lo contrario, el tráfico no prioritario tiene demasiado impacto en la latencia de las tareas de producción. miniond determina la cantidad actual de ancho de banda libre cada segundo, midiendo el consumo de tráfico promedio de todas las tareas de producción de un minion determinado. One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki y restándolo del ancho de banda de la interfaz de red One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki con un pequeño margen, es decir

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Las bandas se definen de forma independiente para el tráfico entrante y saliente. Y según los nuevos valores, miniond reconfigura el límite de clases no prioritarias 1:2.

Por lo tanto, implementamos las tres clases de aislamiento: prod, lote e inactivo. Estas clases influyen en gran medida en las características de desempeño de las tareas. Por lo tanto, decidimos colocar este atributo en la parte superior de la jerarquía, de modo que al mirar el nombre de la cola jerárquica quedara inmediatamente claro con qué estamos tratando:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

todos nuestros amigos web и música A continuación, los frentes se colocan en la jerarquía bajo prod. Por ejemplo, bajo lote, coloquemos el servicio catálogo de música, que periódicamente compila un catálogo de pistas de un conjunto de archivos mp3 cargados en Odnoklassniki. Un ejemplo de un servicio inactivo sería transformador de música, que normaliza el nivel de volumen de la música.

Con las líneas adicionales eliminadas nuevamente, podemos escribir los nombres de nuestros servicios de manera más plana agregando la clase de aislamiento de tareas al final del nombre completo del servicio: web.front.prod, catalogo.musica.lote, transformador.musica.inactivo.

Y ahora, mirando el nombre del servicio, entendemos no solo qué función realiza, sino también su clase de aislamiento, lo que significa su criticidad, etc.

Todo es genial, pero hay una amarga verdad. Es imposible aislar completamente las tareas que se ejecutan en una máquina.

Lo que logramos lograr: si el lote consume intensamente sólo Recursos de CPU, entonces el programador de CPU de Linux integrado hace su trabajo muy bien y prácticamente no hay impacto en la tarea de producción. Pero si esta tarea por lotes comienza a trabajar activamente con la memoria, entonces ya aparece la influencia mutua. Esto sucede porque la tarea de producción se "borra" de las memorias caché del procesador; como resultado, los errores de caché aumentan y el procesador procesa la tarea de producción más lentamente. Una tarea por lotes de este tipo puede aumentar la latencia de nuestro contenedor de producción típico en un 10 %.

Aislar el tráfico es aún más difícil debido al hecho de que las tarjetas de red modernas tienen una cola interna de paquetes. Si el paquete de la tarea por lotes llega primero, será el primero en transmitirse por cable y no se podrá hacer nada al respecto.

Además, hasta ahora sólo hemos logrado resolver el problema de priorizar el tráfico TCP: el enfoque hsfc no funciona para UDP. E incluso en el caso del tráfico TCP, si la tarea por lotes genera mucho tráfico, esto también produce un aumento de aproximadamente el 10 % en el retraso de la tarea de producción.

Tolerancia a fallos

Uno de los objetivos al desarrollar one-cloud era mejorar la tolerancia a fallos de Odnoklassniki. Por lo tanto, a continuación me gustaría considerar con más detalle posibles escenarios de fallas y accidentes. Comencemos con un escenario simple: una falla en el contenedor.

El propio contenedor puede fallar de varias maneras. Esto podría ser algún tipo de experimento, error o error en el manifiesto, debido a que la tarea de producción comienza a consumir más recursos de los indicados en el manifiesto. Tuvimos un caso: un desarrollador implementó un algoritmo complejo, lo modificó muchas veces, pensó demasiado y se confundió tanto que al final el problema se retorció de una manera nada trivial. Y dado que la tarea de producción tiene una prioridad más alta que todas las demás en los mismos minions, comenzó a consumir todos los recursos disponibles del procesador. En esta situación, el aislamiento, o más bien la cuota de tiempo de CPU, salvó el día. Si a una tarea se le asigna una cuota, la tarea no consumirá más. Por lo tanto, las tareas por lotes y otras tareas de producción que se ejecutaron en la misma máquina no notaron nada.

El segundo posible problema es la caída del contenedor. Y aquí las políticas de reinicio nos salvan, todo el mundo las conoce, el propio Docker hace un gran trabajo. Casi todas las tareas de producción tienen una política de reinicio siempre. A veces usamos on_failure para tareas por lotes o para depurar contenedores de productos.

¿Qué puedes hacer si un minion completo no está disponible?

Obviamente, ejecute el contenedor en otra máquina. La parte interesante aquí es lo que sucede con las direcciones IP asignadas al contenedor.

Podemos asignar a los contenedores las mismas direcciones IP que las máquinas minion en las que se ejecutan estos contenedores. Luego, cuando el contenedor se inicia en otra máquina, su dirección IP cambia y todos los clientes deben comprender que el contenedor se ha movido y ahora deben ir a una dirección diferente, lo que requiere un servicio de descubrimiento de servicios independiente.

El descubrimiento de servicios es conveniente. Existen en el mercado muchas soluciones con distintos grados de tolerancia a fallos para organizar un registro de servicios. A menudo, estas soluciones implementan lógica de equilibrador de carga, almacenan configuraciones adicionales en forma de almacenamiento KV, etc.
Sin embargo, nos gustaría evitar la necesidad de implementar un registro separado, porque esto significaría introducir un sistema crítico que es utilizado por todos los servicios en producción. Esto significa que este es un punto potencial de falla y es necesario elegir o desarrollar una solución muy tolerante a fallas, lo cual obviamente es muy difícil, requiere mucho tiempo y es costoso.

Y un gran inconveniente más: para que nuestra antigua infraestructura funcione con la nueva, tendríamos que reescribir absolutamente todas las tareas para utilizar algún tipo de sistema de descubrimiento de servicios. Hay MUCHO trabajo y, en algunos lugares, es casi imposible cuando se trata de dispositivos de bajo nivel que funcionan a nivel del kernel del sistema operativo o directamente con el hardware. Implementación de esta funcionalidad utilizando patrones de solución establecidos, como sidecar En algunos lugares significaría una carga adicional, en otros, una complicación de operación y escenarios de falla adicionales. No queríamos complicar las cosas, por lo que decidimos hacer que el uso de Service Discovery fuera opcional.

En una nube, la IP sigue al contenedor, es decir, cada instancia de tarea tiene su propia dirección IP. Esta dirección es “estática”: se asigna a cada instancia cuando el servicio se envía por primera vez a la nube. Si un servicio tuvo un número diferente de instancias durante su vida, al final se le asignarán tantas direcciones IP como máximo de instancias haya.

Posteriormente, estas direcciones no cambian: se asignan una vez y continúan existiendo durante toda la vida del servicio en producción. Las direcciones IP siguen contenedores a través de la red. Si el contenedor se transfiere a otro minion, la dirección lo seguirá.

Por lo tanto, la asignación de un nombre de servicio a su lista de direcciones IP cambia muy raramente. Si vuelve a mirar los nombres de las instancias de servicio que mencionamos al principio del artículo (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), notaremos que se parecen a los FQDN utilizados en DNS. Así es, para asignar los nombres de las instancias de servicio a sus direcciones IP, utilizamos el protocolo DNS. Además, este DNS devuelve todas las direcciones IP reservadas de todos los contenedores, tanto en ejecución como detenidos (digamos que se utilizan tres réplicas y tenemos cinco direcciones reservadas allí; se devolverán las cinco). Los clientes, una vez recibida esta información, intentarán establecer una conexión con las cinco réplicas y así determinar cuáles están funcionando. Esta opción para determinar la disponibilidad es mucho más confiable, no involucra DNS ni Service Discovery, lo que significa que no hay problemas difíciles de resolver para garantizar la relevancia de la información y la tolerancia a fallas de estos sistemas. Además, en servicios críticos de los que depende el funcionamiento de todo el portal, no podemos utilizar ningún DNS, sino simplemente introducir direcciones IP en la configuración.

Implementar dicha transferencia de IP detrás de contenedores no puede ser trivial, y veremos cómo funciona con el siguiente ejemplo:

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Digamos que el maestro de una nube le da la orden al minion M1 para que ejecute 1.ok-web.group1.web.front.prod con dirección 1.1.1.1. Funciona en minion PÁJARO, que anuncia esta dirección en servidores especiales reflector de ruta. Estos últimos tienen una sesión BGP con el hardware de red, a la que se traduce la ruta de la dirección 1.1.1.1 en M1. M1 enruta paquetes dentro del contenedor usando Linux. Hay tres servidores reflectores de ruta, ya que esta es una parte muy crítica de la infraestructura de una nube; sin ellos, la red en una nube no funcionará. Los colocamos en diferentes racks, a ser posible ubicados en diferentes salas del centro de datos, para reducir la probabilidad de que los tres fallen al mismo tiempo.

Supongamos ahora que se pierde la conexión entre el maestro de una nube y el minion M1. El maestro de una nube ahora actuará bajo el supuesto de que M1 ha fallado por completo. Es decir, le dará la orden al minion M2 para que se lance. web.group1.web.front.prod con la misma dirección 1.1.1.1. Ahora tenemos dos rutas en conflicto en la red para 1.1.1.1: en M1 y en M2. Para resolver dichos conflictos, utilizamos el discriminador de salida múltiple, que se especifica en el anuncio de BGP. Este es un número que muestra el peso de la ruta anunciada. Entre las rutas en conflicto, se seleccionará la ruta con el valor MED más bajo. El maestro de una nube admite MED como parte integral de las direcciones IP del contenedor. Por primera vez, la dirección se escribe con un valor suficientemente grande MED = 1 000 000. En una situación de transferencia de contenedor de emergencia, el maestro reduce el MED y M2 ya recibirá el comando para anunciar la dirección 1.1.1.1 con MED = 999 999. La instancia que se ejecuta en M1 permanecerá en este caso sin conexión, y su futuro destino nos interesa poco hasta que se restablezca la conexión con el maestro, cuando se detendrá como una toma antigua.

Accidente

Todos los sistemas de gestión de centros de datos siempre manejan fallas menores de manera aceptable. El desbordamiento de contenedores es la norma en casi todas partes.

Veamos cómo manejamos una emergencia, como un corte de energía en una o más salas de un centro de datos.

¿Qué significa un accidente para el sistema de gestión de un centro de datos? En primer lugar, se trata de un fallo masivo y único de muchas máquinas, y el sistema de control necesita migrar muchos contenedores al mismo tiempo. Pero si el desastre es de gran escala, puede suceder que todas las tareas no puedan reasignarse a otros minions, porque la capacidad de recursos del centro de datos cae por debajo del 100% de la carga.

A menudo los accidentes van acompañados de un fallo de la capa de control. Esto puede suceder debido a una falla de su equipo, pero más a menudo debido al hecho de que los accidentes no se prueban y la propia capa de control cae debido al aumento de carga.

¿Qué puedes hacer ante todo esto?

Las migraciones masivas significan que hay una gran cantidad de actividades, migraciones e implementaciones que ocurren en la infraestructura. Cada una de las migraciones puede llevar algún tiempo necesario para entregar y descomprimir imágenes de contenedores a minions, iniciar e inicializar contenedores, etc. Por lo tanto, es deseable que las tareas más importantes se inicien antes que las menos importantes.

Miremos nuevamente la jerarquía de servicios que conocemos e intentemos decidir qué tareas queremos ejecutar primero.

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

Por supuesto, estos son los procesos que están directamente involucrados en el procesamiento de las solicitudes de los usuarios, es decir, prod. Esto lo indicamos con prioridad de colocación — un número que se puede asignar a la cola. Si una cola tiene una prioridad más alta, sus servicios se colocan en primer lugar.

Al presionar asignamos prioridades más altas, 0; por lote: un poco menos, 100; en inactivo, incluso menos, 200. Las prioridades se aplican jerárquicamente. Todas las tareas inferiores en la jerarquía tendrán la prioridad correspondiente. Si queremos que los cachés dentro de prod se inicien antes que los frontends, entonces asignamos prioridades al caché = 0 y a las subcolas frontales = 1. Si, por ejemplo, queremos que el portal principal se inicie primero desde los frentes y solo el frente de música entonces podemos asignarle una prioridad más baja a este último: 10.

El siguiente problema es la falta de recursos. Entonces, una gran cantidad de equipos, salas enteras del centro de datos, fallaron y relanzamos tantos servicios que ahora no hay suficientes recursos para todos. Debe decidir qué tareas sacrificar para mantener en funcionamiento los principales servicios críticos.

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

A diferencia de la prioridad de ubicación, no podemos sacrificar indiscriminadamente todas las tareas por lotes; algunas de ellas son importantes para el funcionamiento del portal. Por lo tanto, hemos resaltado por separado prioridad de preferencia tareas. Cuando se coloca, una tarea de mayor prioridad puede adelantarse, es decir, detener, una tarea de menor prioridad si no hay más minions libres. En este caso, una tarea con una prioridad baja probablemente quedará sin asignar, es decir, ya no habrá un minion adecuado para ella con suficientes recursos libres.

En nuestra jerarquía, es muy sencillo especificar una prioridad de preferencia tal que las tareas de producción y por lotes se adelanten o detengan las tareas inactivas, pero no entre sí, especificando una prioridad para inactivas igual a 200. Al igual que en el caso de la prioridad de ubicación, nosotros Podemos utilizar nuestra jerarquía para describir reglas más complejas. Por ejemplo, indiquemos que sacrificamos la función de música si no tenemos suficientes recursos para el portal web principal, fijando la prioridad para los nodos correspondientes en un nivel inferior: 10.

Accidentes completos en DC

¿Por qué podría fallar todo el centro de datos? Elemento. Fue una buena publicación el huracán afectó el trabajo del centro de datos. Los elementos pueden considerarse personas sin hogar que una vez quemaron la óptica en el colector y el centro de datos perdió por completo el contacto con otros sitios. La causa del fallo también puede ser un factor humano: el operador dará una orden tal que todo el centro de datos se caerá. Esto podría suceder debido a un gran error. En general, el colapso de los centros de datos no es infrecuente. Esto nos sucede una vez cada pocos meses.

Y esto es lo que hacemos para evitar que alguien tuitee #alive.

La primera estrategia es el aislamiento. Cada instancia de una nube está aislada y puede administrar máquinas en un solo centro de datos. Es decir, la pérdida de una nube debido a errores o comandos incorrectos del operador es la pérdida de un solo centro de datos. Estamos preparados para ello: tenemos una política de redundancia en la que las réplicas de la aplicación y los datos se ubican en todos los centros de datos. Utilizamos bases de datos tolerantes a fallas y realizamos pruebas periódicas para detectar fallas.
Dado que hoy tenemos cuatro centros de datos, eso significa cuatro instancias separadas y completamente aisladas de una sola nube.

Este enfoque no sólo protege contra fallas físicas, sino que también puede proteger contra errores del operador.

¿Qué más se puede hacer con el factor humano? Cuando un operador le da a la nube alguna orden extraña o potencialmente peligrosa, es posible que de repente se le pida que resuelva un pequeño problema para ver qué tan bien pensó. Por ejemplo, si se trata de algún tipo de parada masiva de muchas réplicas o simplemente de un comando extraño, reducir la cantidad de réplicas o cambiar el nombre de la imagen, y no solo el número de versión en el nuevo manifiesto.

One-cloud: sistema operativo a nivel de centro de datos en Odnoklassniki

resultados

Características distintivas de una nube:

  • Esquema de nomenclatura jerárquica y visual para servicios y contenedores., que permite saber muy rápidamente cuál es la tarea, a qué se refiere, cómo funciona y quién es responsable de ella.
  • Aplicamos nuestro Técnica de combinación de productos y lotes.tareas en minions para mejorar la eficiencia del uso compartido de máquinas. En lugar de cpuset utilizamos cuotas de CPU, recursos compartidos, políticas de programación de CPU y QoS de Linux.
  • No fue posible aislar completamente los contenedores que funcionan en la misma máquina, pero su influencia mutua se mantiene dentro del 20%.
  • Organizar los servicios en una jerarquía ayuda con la recuperación automática de desastres usando prioridades de colocación y preferencia.

FAQ

¿Por qué no adoptamos una solución ya preparada?

  • Las diferentes clases de aislamiento de tareas requieren una lógica diferente cuando se aplican a los minions. Si las tareas de producción se pueden realizar simplemente reservando recursos, entonces se deben realizar tareas por lotes e inactivas, rastreando la utilización real de los recursos en las máquinas minion.
  • La necesidad de tener en cuenta los recursos consumidos por tareas, tales como:
    • ancho de banda de la red;
    • tipos y “husillos” de discos.
  • La necesidad de indicar las prioridades de los servicios durante la respuesta a emergencias, los derechos y cuotas de comandos de recursos, que se resuelve mediante colas jerárquicas en una sola nube.
  • La necesidad de contar con denominación humana de los contenedores para reducir el tiempo de respuesta ante accidentes e incidentes
  • La imposibilidad de una implementación generalizada por única vez de Service Discovery; la necesidad de coexistir durante mucho tiempo con tareas alojadas en hosts de hardware, algo que se resuelve mediante direcciones IP "estáticas" que siguen a los contenedores y, como consecuencia, la necesidad de una integración única con una gran infraestructura de red.

Todas estas funciones requerirían modificaciones significativas de las soluciones existentes para adaptarlas a nosotros y, después de evaluar la cantidad de trabajo, nos dimos cuenta de que podíamos desarrollar nuestra propia solución con aproximadamente los mismos costos laborales. Pero su solución será mucho más fácil de operar y desarrollar: no contiene abstracciones innecesarias que admitan funciones que no necesitamos.

A quienes leyeron las últimas líneas, ¡gracias por su paciencia y atención!

Fuente: habr.com

Añadir un comentario