Bioyino: agregador de métricas distribuido y escalable

Entonces recopilas métricas. Como somos. También recopilamos métricas. Por supuesto, necesario para los negocios. Hoy hablaremos sobre el primer enlace de nuestro sistema de monitoreo: un servidor de agregación compatible con statsd. bioyino, por qué lo escribimos y por qué abandonamos brubeck.

Bioyino: agregador de métricas distribuido y escalable

De nuestros artículos anteriores (1, 2) puedes descubrir que hasta algún momento recogíamos marcas usando Brubeck. Está escrito en C. Desde el punto de vista del código, es tan simple como un enchufe (esto es importante cuando quieres contribuir) y, lo más importante, maneja nuestros volúmenes de 2 millones de métricas por segundo (MPS) en su punto máximo. sin ningún problema. La documentación indica soporte para 4 millones de MPS con un asterisco. Esto significa que obtendrá la cifra indicada si configura la red correctamente en Linux. (No sabemos cuántos MPS puedes obtener si dejas la red como está). A pesar de estas ventajas, hemos tenido varias quejas serias sobre Brubeck.

Reclamación 1. Github, el desarrollador del proyecto, dejó de respaldarlo: publicó parches y correcciones, aceptó las nuestras y (no solo las nuestras) PR. En los últimos meses (entre febrero y marzo de 2018), la actividad se ha reanudado, pero antes de eso hubo casi dos años de completa calma. Además, el proyecto se está desarrollando para necesidades internas de Gihub, lo que puede convertirse en un serio obstáculo para la introducción de nuevas funciones.

Reclamación 2. Precisión de los cálculos. Brubeck recopila un total de 65536 valores para la agregación. En nuestro caso, para algunas métricas, durante el período de agregación (30 segundos), pueden llegar muchos más valores (1 en el pico). Como resultado de este muestreo, los valores máximo y mínimo parecen inútiles. Por ejemplo, así:

Bioyino: agregador de métricas distribuido y escalable
Como era

Bioyino: agregador de métricas distribuido y escalable
Como debería haber sido

Por la misma razón, los importes generalmente se calculan incorrectamente. Agregue aquí un error con un desbordamiento flotante de 32 bits, que generalmente envía al servidor a un error de segmentación cuando recibe una métrica aparentemente inocente, y todo se vuelve genial. El error, por cierto, no se ha solucionado.

Y, finalmente, Reclamo X. Al momento de escribir este artículo, estamos listos para presentarlo a las 14 implementaciones de estadísticas más o menos funcionales que pudimos encontrar. Imaginemos que alguna infraestructura ha crecido tanto que aceptar 4 millones de MPS ya no es suficiente. O incluso si aún no ha crecido, pero las métricas ya son tan importantes para usted que incluso caídas breves de 2 a 3 minutos en los gráficos pueden volverse críticas y causar ataques de depresión insuperable entre los gerentes. Dado que tratar la depresión es una tarea ingrata, se necesitan soluciones técnicas.

En primer lugar, la tolerancia a fallos, para que un problema repentino en el servidor no provoque un apocalipsis zombi psiquiátrico en la oficina. En segundo lugar, escalar para poder aceptar más de 4 millones de MPS, sin profundizar en la pila de red de Linux y crecer tranquilamente "en amplitud" hasta el tamaño requerido.

Como teníamos espacio para escalar, decidimos comenzar con la tolerancia a fallas. "¡ACERCA DE! ¡Tolerancia a fallos! Es simple, podemos hacerlo”, pensamos y lanzamos 2 servidores, generando una copia de brubeck en cada uno. Para hacer esto, tuvimos que copiar el tráfico con métricas a ambos servidores e incluso escribir para esto. pequeña utilidad. Resolvimos el problema de tolerancia a fallos con esto, pero... no muy bien. Al principio, todo parecía genial: cada brubeck recopila su propia versión de agregación, escribe datos en Graphite una vez cada 30 segundos, sobrescribiendo el intervalo anterior (esto se hace en el lado de Graphite). Si un servidor falla repentinamente, siempre tenemos otro con su propia copia de los datos agregados. Pero aquí está el problema: si el servidor falla, aparece una “sierra” en los gráficos. Esto se debe al hecho de que los intervalos de 30 segundos de Brubeck no están sincronizados y, en el momento de una falla, uno de ellos no se sobrescribe. Cuando se inicia el segundo servidor, sucede lo mismo. Bastante tolerable, ¡pero quiero algo mejor! El problema de la escalabilidad tampoco ha desaparecido. Todas las métricas todavía "vuelan" a un solo servidor y, por lo tanto, estamos limitados a los mismos 2-4 millones de MPS, dependiendo del nivel de red.

Si piensa un poco en el problema y al mismo tiempo excava nieve con una pala, puede que se le ocurra la siguiente idea obvia: necesita un statsd que pueda funcionar en modo distribuido. Es decir, uno que implemente sincronización entre nodos en tiempo y métricas. “Por supuesto, tal solución probablemente ya exista”, dijimos y fuimos a Google…. Y no encontraron nada. Después de revisar la documentación de diferentes estadísticas (https://github.com/etsy/statsd/wiki#server-implementations al 11.12.2017 de diciembre de XNUMX), no encontramos absolutamente nada. Aparentemente, ni los desarrolladores ni los usuarios de estas soluciones se han encontrado todavía con TANTAS métricas, de lo contrario definitivamente se les ocurriría algo.

Y luego recordamos las estadísticas del "juguete" - bioyino, que se escribió en el hackathon Just for Fun (el nombre del proyecto fue generado por el guión antes del inicio del hackathon) y nos dimos cuenta de que necesitábamos urgentemente nuestras propias estadísticas. ¿Para qué?

  • porque hay muy pocos clones de estadísticas en el mundo,
  • porque es posible proporcionar la escalabilidad y la tolerancia a fallas deseadas o cercanas a las deseadas (incluida la sincronización de métricas agregadas entre servidores y la resolución del problema de envío de conflictos),
  • porque es posible calcular métricas con mayor precisión que brubeck,
  • porque usted mismo puede recopilar estadísticas más detalladas, que brubeck prácticamente no nos proporcionó,
  • porque tuve la oportunidad de programar mi propia aplicación de laboratorio de escala distribuida de hiperrendimiento, que no repetirá completamente la arquitectura de otra hiperfor similar... bueno, eso es todo.

¿Sobre qué escribir? Por supuesto, en Rust. ¿Por qué?

  • porque ya existía un prototipo de solución,
  • porque el autor del artículo ya conocía Rust en ese momento y estaba ansioso por escribir algo para producción con la oportunidad de ponerlo en código abierto,
  • porque los idiomas con GC no nos convienen debido a la naturaleza del tráfico recibido (casi en tiempo real) y las pausas de GC son prácticamente inaceptables,
  • porque necesita el máximo rendimiento comparable a C
  • porque Rust nos proporciona una concurrencia intrépida, y si comenzamos a escribirlo en C/C++, habríamos acumulado aún más vulnerabilidades, desbordamientos de búfer, condiciones de carrera y otras palabras aterradoras que brubeck.

También hubo un argumento contra Rust. La empresa no tenía experiencia en la creación de proyectos en Rust y ahora tampoco planeamos utilizarlo en el proyecto principal. Por lo tanto, teníamos serios temores de que nada saliera bien, pero decidimos arriesgarnos y intentarlo.

El tiempo pasó...

Finalmente, después de varios intentos fallidos, la primera versión funcional estuvo lista. ¿Qué pasó? Esto es lo que pasó.

Bioyino: agregador de métricas distribuido y escalable

Cada nodo recibe su propio conjunto de métricas y las acumula, y no agrega métricas para aquellos tipos en los que se requiere su conjunto completo para la agregación final. Los nodos están conectados entre sí mediante algún tipo de protocolo de bloqueo distribuido, que permite seleccionar entre ellos el único (aquí lloramos) que es digno de enviar métricas al Grande. Actualmente este problema está siendo resuelto por Consul, pero en el futuro las ambiciones del autor se extienden a propio implementación Balsa, donde el más digno será, por supuesto, el nodo líder del consenso. Además del consenso, los nodos con bastante frecuencia (una vez por segundo de forma predeterminada) envían a sus vecinos aquellas partes de métricas preagregadas que lograron recopilar en ese segundo. Resulta que se conservan el escalado y la tolerancia a fallos: cada nodo todavía contiene un conjunto completo de métricas, pero las métricas ya se envían agregadas, a través de TCP y codificadas en un protocolo binario, por lo que los costos de duplicación se reducen significativamente en comparación con UDP. A pesar de la cantidad bastante grande de métricas entrantes, la acumulación requiere muy poca memoria e incluso menos CPU. Para nuestros mertics altamente comprimibles, esto es solo unas pocas decenas de megabytes de datos. Como beneficio adicional, no obtenemos reescrituras de datos innecesarias en Graphite, como fue el caso con Burbeck.

Los paquetes UDP con métricas se desequilibran entre los nodos del equipo de red mediante un simple Round Robin. Por supuesto, el hardware de red no analiza el contenido de los paquetes y, por lo tanto, puede extraer mucho más de 4 millones de paquetes por segundo, sin mencionar las métricas sobre las que no sabe nada en absoluto. Si tenemos en cuenta que las métricas no vienen una a la vez en cada paquete, entonces no prevemos ningún problema de rendimiento en este lugar. Si un servidor falla, el dispositivo de red detecta rápidamente (en 1 o 2 segundos) este hecho y elimina el servidor fallado de la rotación. Como resultado de esto, los nodos pasivos (es decir, no líderes) se pueden activar y desactivar prácticamente sin notar caídas en los gráficos. Lo máximo que perdemos es parte de las métricas que llegaron en el último segundo. Una pérdida/apagado/cambio repentino de un líder seguirá creando una anomalía menor (el intervalo de 30 segundos aún no está sincronizado), pero si hay comunicación entre nodos, estos problemas se pueden minimizar, por ejemplo, enviando paquetes de sincronización. .

Un poco sobre la estructura interna. La aplicación es, por supuesto, multiproceso, pero la arquitectura de subprocesos es diferente de la utilizada en Brubeck. Los hilos en Brubeck son los mismos: cada uno de ellos es responsable tanto de la recopilación como de la agregación de información. En bioyino los trabajadores se dividen en dos grupos: los responsables de la red y los responsables de la agregación. Esta división le permite administrar la aplicación de manera más flexible según el tipo de métricas: cuando se requiere una agregación intensiva, puede agregar agregadores, donde hay mucho tráfico de red, puede agregar la cantidad de flujos de red. Actualmente, en nuestros servidores trabajamos en 8 flujos de red y 4 de agregación.

La parte de conteo (responsable de la agregación) es bastante aburrida. Los buffers llenados por los flujos de la red se distribuyen entre los flujos de conteo, donde posteriormente se analizan y agregan. A pedido, se proporcionan métricas para enviar a otros nodos. Todo esto, incluido el envío de datos entre nodos y el trabajo con Consul, se realiza de forma asíncrona y se ejecuta en el marco. Tokio.

Muchos más problemas durante el desarrollo fueron causados ​​por la parte de la red responsable de recibir métricas. El objetivo principal de separar los flujos de red en entidades separadas era el deseo de reducir el tiempo que pasa un flujo. no para leer datos del socket. Las opciones que utilizan UDP asíncrono y recvmsg regular desaparecieron rápidamente: la primera consume demasiado espacio de CPU para el procesamiento de eventos, la segunda requiere demasiados cambios de contexto. Por eso ahora se utiliza recvmmsg con grandes topes (¡y los topes, señores oficiales, no son nada para ustedes!). La compatibilidad con UDP normal está reservada para casos ligeros en los que no se necesita recvmmsg. En el modo multimensaje, es posible lograr lo principal: la gran mayoría de las veces, el subproceso de la red rastrea la cola del sistema operativo: lee datos del socket y los transfiere al búfer del espacio de usuario, y solo ocasionalmente cambia para entregar el búfer lleno a agregadores. La cola en el socket prácticamente no se acumula, la cantidad de paquetes descartados prácticamente no aumenta.

Nota

En la configuración predeterminada, el tamaño del búfer está configurado para ser bastante grande. Si de repente decide probar el servidor usted mismo, puede encontrarse con el hecho de que después de enviar una pequeña cantidad de métricas, no llegarán a Graphite y permanecerán en el búfer de flujo de la red. Para trabajar con una pequeña cantidad de métricas, debe configurar bufsize y task-queue-size en valores más pequeños en la configuración.

Finalmente, algunos gráficos para los amantes de los gráficos.

Estadísticas sobre el número de métricas entrantes para cada servidor: más de 2 millones de MPS.

Bioyino: agregador de métricas distribuido y escalable

Deshabilitar uno de los nodos y redistribuir las métricas entrantes.

Bioyino: agregador de métricas distribuido y escalable

Estadísticas sobre métricas salientes: siempre envía solo un nodo: el jefe de la incursión.

Bioyino: agregador de métricas distribuido y escalable

Estadísticas del funcionamiento de cada nodo, teniendo en cuenta errores en varios módulos del sistema.

Bioyino: agregador de métricas distribuido y escalable

Detalles de las métricas entrantes (los nombres de las métricas están ocultos).

Bioyino: agregador de métricas distribuido y escalable

¿Qué planeamos hacer con todo esto a continuación? ¡Por supuesto, escribe código, maldita sea...! Originalmente se planeó que el proyecto fuera de código abierto y lo seguirá siendo durante toda su vida. Nuestros planes inmediatos incluyen cambiar a nuestra propia versión de Raft, cambiar el protocolo de pares a uno más portátil, introducir estadísticas internas adicionales, nuevos tipos de métricas, correcciones de errores y otras mejoras.

Por supuesto, todos son bienvenidos a ayudar en el desarrollo del proyecto: crear relaciones públicas, problemas, si es posible responderemos, mejoraremos, etc.

Dicho esto, eso es todo amigos, ¡compren nuestros elefantes!



Fuente: habr.com

Añadir un comentario