Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud
Ola, son Sergey Elantsev, desenvolvo equilibrador de carga de rede en Yandex.Cloud. Anteriormente, dirixín o desenvolvemento do equilibrador L7 para o portal Yandex; os compañeiros bromean dicindo que faga o que faga, resulta ser un equilibrador. Direilles aos lectores de Habr como xestionar a carga nunha plataforma na nube, cal consideramos a ferramenta ideal para acadar este obxectivo e como estamos avanzando para construír esta ferramenta.

En primeiro lugar, imos introducir algúns termos:

  • VIP (IP virtual) - enderezo IP do equilibrador
  • Servidor, backend, instancia: unha máquina virtual que executa unha aplicación
  • RIP (IP real) - enderezo IP do servidor
  • Healthcheck: comproba a preparación do servidor
  • Availability Zone, AZ - infraestrutura illada nun centro de datos
  • Rexión - unha unión de diferentes AZ

Os equilibradores de carga resolven tres tarefas principais: realizan o propio equilibrado, melloran a tolerancia a fallos do servizo e simplifican o seu escalado. A tolerancia a fallos garante a xestión automática do tráfico: o equilibrador supervisa o estado da aplicación e exclúe do balance as instancias que non superen a comprobación de vida. A escala está garantida pola distribución uniforme da carga entre as instancias, así como pola actualización da lista de instancias sobre a marcha. Se o equilibrio non é suficientemente uniforme, entón algunhas das instancias recibirán unha carga que supera o seu límite de capacidade e o servizo será menos fiable.

Un equilibrador de carga adoita clasificarse pola capa de protocolo do modelo OSI no que se executa. O Cloud Balancer opera a nivel TCP, que corresponde á cuarta capa, L4.

Pasemos a unha visión xeral da arquitectura do equilibrador de nube. Iremos aumentando gradualmente o nivel de detalle. Dividimos os compoñentes do equilibrador en tres clases. A clase do plano de configuración é responsable da interacción do usuario e almacena o estado de destino do sistema. O plano de control almacena o estado actual do sistema e xestiona os sistemas desde a clase de plano de datos, que son directamente responsables de entregar o tráfico dos clientes ás túas instancias.

Plano de datos

O tráfico acaba en dispositivos caros chamados enrutadores de fronteira. Para aumentar a tolerancia a fallos, varios destes dispositivos funcionan simultáneamente nun centro de datos. A continuación, o tráfico vai aos equilibradores, que anuncian enderezos IP anycast a todos os AZ a través de BGP para os clientes. 

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

O tráfico transmítese a través de ECMP: esta é unha estratexia de enrutamento segundo a cal pode haber varias rutas igualmente boas ata o destino (no noso caso, o destino será o enderezo IP de destino) e pódense enviar paquetes por calquera deles. Tamén apoiamos o traballo en varias zonas de dispoñibilidade segundo o seguinte esquema: anunciamos un enderezo en cada zona, o tráfico vai á máis próxima e non supera os seus límites. Máis adiante na publicación analizaremos con máis detalle o que acontece co tráfico.

Plano de configuración

 
O compoñente clave do plano de configuración é a API, a través da cal se realizan operacións básicas con equilibradores: creación, eliminación, modificación da composición de instancias, obtención de resultados de comprobacións de saúde, etc. Por unha banda, trátase dunha API REST, e por outra noutros, na Nube usamos con moita frecuencia o marco gRPC, polo que "traducimos" REST a gRPC e despois usamos só gRPC. Calquera solicitude leva á creación dunha serie de tarefas idempotentes asíncronas que se executan nun grupo común de traballadores de Yandex.Cloud. As tarefas están escritas de forma que se poden suspender en calquera momento e reiniciarse despois. Isto garante escalabilidade, repetibilidade e rexistro de operacións.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Como resultado, a tarefa da API fará unha solicitude ao controlador do servizo de balance, que está escrita en Go. Pode engadir e eliminar equilibradores, cambiar a composición dos backends e a configuración. 

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

O servizo almacena o seu estado en Yandex Database, unha base de datos xestionada distribuída que pronto poderás usar. En Yandex.Cloud, como xa contou, aplícase o concepto de comida para cans: se nós mesmos utilizamos os nosos servizos, os nosos clientes tamén estarán encantados de utilizalos. Yandex Database é un exemplo da implementación deste concepto. Almacenamos todos os nosos datos en YDB, e non temos que pensar en manter e escalar a base de datos: estes problemas resólvenos, usamos a base de datos como servizo.

Volvamos ao controlador do equilibrador. A súa tarefa é gardar información sobre o equilibrador e enviar unha tarefa para comprobar a preparación da máquina virtual ao controlador de comprobación de saúde.

Controlador de saúde

Recibe solicitudes para cambiar as regras de comprobación, gárdaas en YDB, distribúe tarefas entre os nodos de comprobación de saúde e agrega os resultados, que despois se gardan na base de datos e se envían ao controlador de balance de carga. Á súa vez, envía unha solicitude para cambiar a composición do clúster no plano de datos ao nodo equilibrador de carga, que comentarei a continuación.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Falemos máis sobre os controis de saúde. Pódense dividir en varias clases. As auditorías teñen diferentes criterios de éxito. As comprobacións TCP precisan establecer con éxito unha conexión nun período de tempo fixo. As comprobacións HTTP requiren unha conexión correcta e unha resposta cun código de estado 200.

Ademais, os cheques difiren na clase de acción: son activos e pasivos. As comprobacións pasivas simplemente supervisan o que está a suceder co tráfico sen tomar ningunha medida especial. Isto non funciona moi ben na L4 porque depende da lóxica dos protocolos de nivel superior: na L4 non hai información sobre canto tempo levou a operación nin se a finalización da conexión foi boa ou mala. As comprobacións activas requiren que o equilibrador envíe solicitudes a cada instancia do servidor.

A maioría dos equilibradores de carga realizan por si mesmos controis de actividade. En Cloud, decidimos separar estas partes do sistema para aumentar a escalabilidade. Este enfoque permitiranos aumentar o número de equilibradores mantendo o número de solicitudes de revisión sanitaria ao servizo. As comprobacións realízanse mediante nodos de comprobación de saúde separados, a través dos cales os obxectivos de comprobación se dividen e replícanse. Non pode realizar comprobacións desde un servidor, xa que pode fallar. Entón non obteremos o estado das instancias que comprobou. Realizamos comprobacións en calquera das instancias de polo menos tres nodos de comprobación de saúde. Repartimos os propósitos das comprobacións entre nós utilizando algoritmos de hash consistentes.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Separar o equilibrio e o control de saúde pode levar a problemas. Se o nodo de comprobación de saúde fai solicitudes á instancia, evitando o equilibrador (que actualmente non está atendendo tráfico), entón xorde unha situación estraña: o recurso parece estar vivo, pero o tráfico non o chegará. Resolvemos este problema deste xeito: temos a garantía de iniciar o tráfico de comprobacións de saúde a través dos equilibradores. Noutras palabras, o esquema para mover paquetes con tráfico dos clientes e dos controles de saúde difire mínimamente: en ambos os casos, os paquetes chegarán aos equilibradores, que os entregarán aos recursos de destino.

A diferenza é que os clientes fan solicitudes a VIP, mentres que os controles de saúde fan solicitudes a cada RIP individual. Aquí xorde un problema interesante: damos aos nosos usuarios a oportunidade de crear recursos en redes IP grises. Imaxinemos que hai dous propietarios de nube diferentes que ocultaron os seus servizos detrás dos equilibradores. Cada un deles ten recursos na subrede 10.0.0.1/24, cos mesmos enderezos. Debe ser capaz de distinguilos dalgún xeito e aquí debe mergullarse na estrutura da rede virtual Yandex.Cloud. É mellor coñecer máis detalles en vídeo de about:cloud event, é importante para nós agora que a rede ten varias capas e ten túneles que se poden distinguir polo ID de subrede.

Os nodos de Healthcheck contactan os equilibradores mediante os chamados enderezos cuasi-IPv6. Un cuasi-enderezo é un enderezo IPv6 cun enderezo IPv4 e un ID de subrede de usuario incrustados nel. O tráfico chega ao equilibrador, que extrae del o enderezo do recurso IPv4, substitúe IPv6 por IPv4 e envía o paquete á rede do usuario.

O tráfico inverso vai do mesmo xeito: o equilibrador ve que o destino é unha rede gris dos comprobadores de saúde e converte IPv4 en IPv6.

VPP - o corazón do plano de datos

O equilibrador implícase mediante a tecnoloxía de procesamento de paquetes vectoriales (VPP), un marco de Cisco para o procesamento por lotes do tráfico de rede. No noso caso, o marco funciona enriba da biblioteca de xestión de dispositivos de rede de espazo de usuario: Kit de desenvolvemento de planos de datos (DPDK). Isto garante un alto rendemento de procesamento de paquetes: ocorren moitas menos interrupcións no núcleo e non hai cambios de contexto entre o espazo do núcleo e o espazo do usuario. 

VPP vai aínda máis alá e espreme aínda máis o rendemento do sistema ao combinar paquetes en lotes. As ganancias de rendemento veñen do uso agresivo de cachés nos procesadores modernos. Utilízanse tanto cachés de datos (os paquetes son procesados ​​en “vectores”, os datos están preto uns dos outros) como cachés de instrucións: en VPP, o procesamento de paquetes segue un gráfico, cuxos nodos conteñen funcións que realizan a mesma tarefa.

Por exemplo, o procesamento dos paquetes IP en VPP prodúcese na seguinte orde: primeiro, as cabeceiras dos paquetes analízanse no nodo de análise e despois envíanse ao nodo, que reenvía os paquetes segundo as táboas de enrutamento.

Un pouco hardcore. Os autores de VPP non toleran compromisos no uso de cachés do procesador, polo que o código típico para procesar un vector de paquetes contén vectorización manual: hai un bucle de procesamento no que se procesa unha situación como "temos catro paquetes na cola". entón o mesmo para dous, entón - para un. As instrucións de captación previa úsanse a miúdo para cargar datos na caché para acelerar o acceso a eles en iteracións posteriores.

n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
    vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
    // ...
    while (n_left_from >= 4 && n_left_to_next >= 2)
    {
        // processing multiple packets at once
        u32 next0 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        u32 next1 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        // ...
        /* Prefetch next iteration. */
        {
            vlib_buffer_t *p2, *p3;

            p2 = vlib_get_buffer (vm, from[2]);
            p3 = vlib_get_buffer (vm, from[3]);

            vlib_prefetch_buffer_header (p2, LOAD);
            vlib_prefetch_buffer_header (p3, LOAD);

            CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, STORE);
            CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, STORE);
        }
        // actually process data
        /* verify speculative enqueues, maybe switch current next frame */
        vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                to_next, n_left_to_next,
                bi0, bi1, next0, next1);
    }

    while (n_left_from > 0 && n_left_to_next > 0)
    {
        // processing packets by one
    }

    // processed batch
    vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}

Entón, Healthchecks fala por IPv6 co VPP, o que os converte en IPv4. Isto faise por un nodo do gráfico, que chamamos NAT algorítmico. Para o tráfico inverso (e a conversión de IPv6 a IPv4) existe o mesmo nodo NAT algorítmico.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

O tráfico directo dos clientes do equilibrador pasa polos nodos gráficos, que realizan o propio equilibrio. 

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

O primeiro nodo son as sesións pegajosas. Almacena o hash de 5-tupla para sesións establecidas. 5-tupla inclúe o enderezo e o porto do cliente desde o que se transmite a información, o enderezo e os portos dos recursos dispoñibles para recibir tráfico, así como o protocolo de rede. 

O hash de 5 tuplas axúdanos a realizar menos cálculos no nodo de hash consistente posterior, así como a xestionar mellor os cambios na lista de recursos detrás do equilibrador. Cando un paquete para o que non hai sesión chega ao equilibrador, envíase ao nodo hash consistente. Aquí é onde se produce o equilibrio mediante hash consistente: seleccionamos un recurso da lista de recursos "en directo" dispoñibles. A continuación, os paquetes envíanse ao nodo NAT, que en realidade substitúe o enderezo de destino e recalcula as sumas de verificación. Como podes ver, seguimos as regras de VPP: como gustar, agrupando cálculos similares para aumentar a eficiencia das cachés do procesador.

Hashing consistente

Por que o eliximos e que é mesmo? En primeiro lugar, consideremos a tarefa anterior: seleccionar un recurso da lista. 

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Con hash inconsistente, calcúlase o hash do paquete entrante e selecciónase un recurso da lista polo resto de dividir este hash polo número de recursos. Mentres a lista permaneza sen cambios, este esquema funciona ben: sempre enviamos paquetes coa mesma tupla 5 á mesma instancia. Se, por exemplo, algún recurso deixou de responder aos controis de saúde, a elección cambiará para unha parte significativa dos hash. As conexións TCP do cliente romperanse: un paquete que previamente chegou á instancia A pode comezar a chegar á instancia B, que non está familiarizada coa sesión deste paquete.

O hashing consistente resolve o problema descrito. A forma máis sinxela de explicar este concepto é esta: imaxina que tes un anel ao que distribúes recursos por hash (por exemplo, por IP:port). Seleccionar un recurso é xirar a roda nun ángulo, que está determinado polo hash do paquete.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Isto minimiza a redistribución do tráfico cando cambia a composición dos recursos. A eliminación dun recurso só afectará á parte do anel hash consistente na que se atopaba o recurso. Engadir un recurso tamén cambia a distribución, pero temos un nodo de sesións pegajosas, que nos permite non cambiar as sesións xa establecidas a novos recursos.

Observamos o que ocorre co tráfico directo entre o equilibrador e os recursos. Agora vexamos o tráfico de volta. Segue o mesmo patrón que o tráfico de comprobación: a través de NAT algorítmico, é dicir, a través de NAT 44 inverso para o tráfico de clientes e a través de NAT 46 para o tráfico de comprobacións de saúde. Adherimos ao noso propio esquema: unificamos o tráfico de comprobacións de saúde e o tráfico real de usuarios.

Loadbalancer-nodo e compoñentes ensamblados

O servizo local - loadbalancer-node informa da composición dos equilibradores e dos recursos en VPP. Subscríbese ao fluxo de eventos do loadbalancer-controller e é capaz de representar a diferenza entre o estado VPP actual e o estado de destino recibido do controlador. Temos un sistema pechado: os eventos da API chegan ao controlador de balance, que lle asigna tarefas ao controlador de control de saúde para comprobar a "vivencia" dos recursos. Iso, á súa vez, asigna tarefas ao nodo de comprobación de saúde e agrega os resultados, despois de que os envía de volta ao controlador do equilibrador. Loadbalancer-node subscríbese aos eventos do controlador e cambia o estado do VPP. Neste sistema, cada servizo só sabe o que é necesario sobre os servizos veciños. O número de conexións é limitado e temos a capacidade de operar e escalar diferentes segmentos de forma independente.

Arquitectura dun equilibrador de carga de rede en Yandex.Cloud

Que problemas se evitaron?

Todos os nosos servizos no plano de control están escritos en Go e teñen boas características de escalado e fiabilidade. Go ten moitas bibliotecas de código aberto para construír sistemas distribuídos. Usamos GRPC activamente, todos os compoñentes conteñen unha implementación de código aberto de descubrimento de servizos: os nosos servizos supervisan o rendemento dos outros, poden cambiar a súa composición de forma dinámica e vinculamos isto co equilibrio GRPC. Para as métricas, tamén usamos unha solución de código aberto. No plano de datos, obtivemos un rendemento decente e unha gran reserva de recursos: resultou moi difícil montar un soporte no que poder confiar no rendemento dun VPP, en lugar dunha tarxeta de rede de ferro.

Problemas e solucións

Que non funcionou tan ben? Go ten xestión automática da memoria, pero aínda se producen fugas de memoria. A forma máis sinxela de tratar con eles é executar goroutines e lembrar de finalizalas. Para levar: Observa o consumo de memoria dos teus programas Go. Moitas veces un bo indicador é o número de goroutines. Hai unha vantaxe nesta historia: en Go é fácil obter datos de tempo de execución: o consumo de memoria, o número de goroutines en execución e moitos outros parámetros.

Ademais, Go pode non ser a mellor opción para probas funcionais. Son bastante detallados e o enfoque estándar de "executar todo en CI nun lote" non é moi axeitado para eles. O caso é que as probas funcionais requiren máis recursos e provocan tempo de espera reais. Debido a isto, as probas poden fallar porque a CPU está ocupada coas probas unitarias. Conclusión: se é posible, realice probas "pesadas" por separado das probas unitarias. 

A arquitectura de eventos de microservizos é máis complexa que un monólito: recoller rexistros en ducias de máquinas diferentes non é moi cómodo. Conclusión: se fas microservizos, pensa inmediatamente no rastrexo.

Os nosos plans

Lanzaremos un equilibrador interno, un equilibrador IPv6, engadiremos compatibilidade con scripts de Kubernetes, seguiremos dividindo os nosos servizos (actualmente só se dividen healthcheck-node e healthcheck-ctrl), engadiremos novos controis de saúde e tamén implementaremos a agregación intelixente de comprobacións. Estamos considerando a posibilidade de facer os nosos servizos aínda máis independentes, para que non se comuniquen directamente entre si, senón mediante unha fila de mensaxes. Recentemente apareceu na nube un servizo compatible con SQS Fila de mensaxes Yandex.

Recentemente, tivo lugar o lanzamento público de Yandex Load Balancer. Explora documentación ao servizo, xestiona os equilibradores dun xeito cómodo para ti e aumenta a tolerancia a fallos dos teus proxectos.

Fonte: www.habr.com

Engadir un comentario