La lucha por soluciones de calidad en Erlang / Elixir


@jcutrer


Hoy hablaremos sobre registros de eventos, métricas cuantitativas y monitoreo de todo esto para aumentar la tasa de respuesta del equipo a los incidentes y reducir el tiempo de inactividad del sistema objetivo.


Erlang / OTP como marco y la ideología de construir sistemas distribuidos nos brinda enfoques regulados para el desarrollo, herramientas e implementación de componentes estándar. Digamos que usamos el potencial de OTP y pasamos del prototipo a la producción. Nuestro proyecto Erlang se siente genial en los servidores de batalla, la base del código está en constante evolución, aparecen nuevos requisitos y funcionalidades, nuevas personas llegan al equipo y todo parece estar bien. Pero a veces algo sale mal y los problemas técnicos, multiplicados por el factor humano, pueden provocar un accidente.


Dado que es imposible colocar pajillas para absolutamente todos los casos posibles de fallas y problemas, o no es económicamente factible, es necesario reducir el tiempo de inactividad del sistema en caso de fallas de la administración y las soluciones de software.


En los sistemas de información siempre habrá una probabilidad de ocurrencia de fallas de diversa naturaleza:


  • Fallas de hardware y fallas de energía
  • Fallos de red: errores de configuración, curvas de firmware
  • Errores lógicos: desde errores de codificación de algoritmos hasta problemas arquitectónicos que surgen en los límites de subsistemas y sistemas.
  • Problemas de seguridad y ataques y ataques relacionados, incluido el fraude interno.

Inmediatamente distinguimos la responsabilidad: el monitoreo de la infraestructura, por ejemplo, organizado por zabbix, será responsable de la operación de los equipos informáticos y las redes de transmisión de datos. Se ha escrito mucho sobre la instalación y configuración de dicho monitoreo, no lo repetiremos.


Desde el punto de vista del desarrollador, el problema de accesibilidad y calidad radica en el plano de la detección temprana de errores y problemas de rendimiento y la rápida respuesta a los mismos. Esto requiere enfoques y medios de evaluación. Entonces, tratemos de obtener métricas cuantitativas, analizando qué en diferentes etapas del desarrollo y operación del proyecto, podemos mejorar significativamente la calidad.


Sistemas de montaje


Permítame recordarle una vez más la importancia del enfoque de ingeniería y las pruebas en el desarrollo de software. Erlang / OTP ofrece dos marcos de prueba a la vez: eunit y prueba común.


Como métrica para la evaluación inicial del estado de la base del código y su dinámica, puede usar la cantidad de pruebas exitosas y problemáticas, su tiempo de ejecución y el porcentaje de cobertura del código con las pruebas. Ambos marcos permiten guardar los resultados de las pruebas en formato Junit.
Por ejemplo, para rebar3 y ct, agregue las siguientes líneas a rebar.config:


{cover_enabled, true}. {cover_export_enabled, true}. {ct_opts,[ {ct_hooks, [{cth_surefire, [{path, "report.xml"}]}]} ]}. 

La cantidad de pruebas exitosas y no exitosas le permitirá construir un gráfico de tendencias:

observando qué, puede evaluar la dinámica del equipo y la regresión de las pruebas. Por ejemplo, en Jenkins, este gráfico se puede obtener utilizando el complemento Test Results Analyzer.


Si las pruebas se vuelven rojas o comienzan a ejecutarse durante mucho tiempo, las métricas permitirán que se finalice el lanzamiento incluso en la etapa de ensamblaje y prueba automática.


Métricas de aplicación


Además de las métricas del sistema operativo, el monitoreo debe incluir métricas de la aplicación, como la cantidad de vistas por segundo, la cantidad de pagos y otros indicadores críticos.


En mis proyectos, uso una plantilla como ${application}.${metrics_type}.${name} para nombrar las métricas. Este nombre le permite obtener listas de métricas


 messaging.systime_subs.messages.delivered = 1654 messaging.systime_subs.messages.proxied = 0 messaging.systime_subs.messages.published = 1655 messaging.systime_subs.messages.skipped = 3 

Quizás, cuanto más métricas, más fácil es comprender lo que sucede en un sistema complejo.


Erlang VM Metrics


Se debe prestar especial atención al monitoreo de Erlang VM. La ideología de dejar que se bloquee es hermosa, y el uso adecuado de OTP ciertamente ayudará a levantar las partes caídas de la aplicación dentro de Erlang VM. Pero no te olvides de Erlang VM, porque es difícil dejarlo, pero es posible. Todas las opciones se basan en el agotamiento de los recursos. Enumeramos los principales:


  • Atom table overflow.
    Los átomos son identificadores cuyo propósito principal es mejorar la legibilidad del código. Los átomos creados una vez permanecen para siempre en la memoria de la instancia de Erlang VM, ya que el recolector de basura no los borra. ¿Por qué está pasando esto? El recolector de basura funciona por separado en cada proceso con datos de este proceso, mientras que los átomos se pueden distribuir a través de las estructuras de datos de muchos procesos.
    Por defecto, se pueden crear 1.048.576 átomos. En los artículos sobre matar a Erlang VM, generalmente puedes encontrar algo como esto.


     [list_to_atom(integer_to_list(I)) || I <- lists:seq(erlang:system_info(atom_count), erlang:system_info(atom_limit))] 

    como una ilustración de este efecto. Parece que un problema artificial es inalcanzable en sistemas reales, pero hay casos ... Por ejemplo, en el controlador de API externo, cuando se analizan solicitudes, binary_to_atom/2 binary_to_existing_atom/2 lugar de binary_to_existing_atom/2 o list_to_atom/1 lugar de list_to_existing_atom/1 .
    Los siguientes parámetros deben usarse para monitorear el estado de los átomos:


    1. erlang:memory(atom_used) - cantidad de memoria utilizada para átomos
    2. erlang:system_info(atom_count) : el número de átomos creados en el sistema. Junto con erlang:system_info(atom_limit) , se puede calcular la utilización del átomo.

  • Proceso de fugas.
    Me gustaría decir de inmediato que cuando se alcanza process_limit (+ P, el argumento erl) erlang vm no cae, pero entra en un estado de emergencia, por ejemplo, lo más probable es que sea imposible conectarse a él. Finalmente, quedarse sin memoria disponible cuando se asigna a procesos filtrados hará que erlang vm se bloquee.


    1. erlang:system_info(process_count) : la cantidad de procesos activos en este momento. Junto con erlang:system_info(process_limit) , se puede calcular la utilización del proceso.
    2. erlang:memory(processes) - memoria asignada para procesos
    3. erlang:memory(processes_used) : memoria utilizada para procesos.

  • Desbordamiento del proceso de buzón.
    Un ejemplo típico de tal problema es que el proceso del remitente envía mensajes al proceso del destinatario sin esperar la confirmación, mientras que receive en el proceso del receptor ignora todos estos mensajes debido a un patrón faltante o incorrecto. Como resultado, los mensajes se acumulan en el buzón. Aunque erlang tiene un mecanismo para ralentizar al remitente en caso de que el controlador no pueda manejar el procesamiento, de todos modos, después del agotamiento de la memoria disponible, vm se bloquea.
    Para comprender si hay problemas con el desbordamiento del buzón, etop lo ayudará.


     $ erl -name etop@host -hidden -s etop -s erlang halt -output text -node dest@host -setcookie some_cookie -tracing off -sort msg_q -interval 1 -lines 25 


    Como medida para la supervisión continua, puede tomar la cantidad de procesos problemáticos. Para identificarlos, puede usar la siguiente función:


     top_msq_q()-> [{P, RN, L, IC, ST} || P <- processes(), { _, L } <- [ process_info(P, message_queue_len) ], L >= 1000, [{_, RN}, {_, IC}, {_, ST}] <- [process_info(P, [registered_name, initial_call, current_stacktrace]) ] ]. 

    Además, esta lista se puede registrar, luego, al recibir notificaciones de monitoreo, se simplifica el análisis del problema.


  • Binarios con fugas.
    La memoria para binarios grandes (más de 64 bytes) se asigna en el montón general. El bloque asignado tiene un contador de referencia que muestra la cantidad de procesos que tienen acceso a él. Después de reiniciar el contador, se realiza la limpieza. El sistema más simple, pero, como dicen, hay matices. En principio, existe la posibilidad de que un proceso genere tanta basura en el montón que el sistema no tenga suficiente memoria para realizar la limpieza.
    erlang:memory(binary) actúa como una métrica para el monitoreo, mostrando la memoria asignada para binarios.



Por lo tanto, los casos que conducen a la caída de vm se resuelven, sin embargo, además de ellos, es bueno monitorear parámetros no menos importantes que afectan directa o indirectamente el funcionamiento correcto de sus aplicaciones:


  • La memoria utilizada por las erlang:memory(ets) ETS: erlang:memory(ets) .
  • Memoria para módulos compilados: erlang:memory(code) .
    Si sus soluciones no utilizan la compilación de código dinámico, entonces esta opción puede excluirse.
    También me gustaría mencionar erlydtl. Si compila plantillas dinámicamente, la compilación crea una viga que se carga en la memoria vm. También puede causar pérdidas de memoria.
  • Memoria del sistema: erlang:memory(system) . Muestra el consumo de memoria de tiempo de ejecución erlang.
  • Memoria total consumida: erlang:memory(total) . Esta es la cantidad de memoria consumida por los procesos y el tiempo de ejecución.
  • Información sobre reducciones: erlang:statistics(reductions) .
  • Número de procesos y puertos listos para la ejecución: erlang:statistics(run_queue) .
  • El tiempo de actividad de la erlang:statistics(runtime) vm: erlang:statistics(runtime) permite comprender si hubo un reinicio sin análisis de registro.
  • Actividad de red: erlang:statistics(io) .

Enviar métricas a zabbix


Crearemos un archivo que contenga las métricas de la aplicación y las métricas de erlang vm, que actualizaremos cada N segundos. Para cada nodo erlang, el archivo de métricas debe contener las métricas de las aplicaciones que se ejecutan en él y las métricas de la instancia de erlang vm. El resultado debería ser algo como esto:


 messaging.systime_subs.messages.delivered = 1654 messaging.systime_subs.messages.proxied = 0 messaging.systime_subs.messages.published = 1655 messaging.systime_subs.messages.skipped = 3 …. erlang.io.input = 2205723664 erlang.io.output = 1665529234 erlang.memory.binary = 1911136 erlang.memory.ets = 1642416 erlang.memory.processes = 23596432 erlang.memory.processes_used = 23598864 erlang.memory.system = 50883752 erlang.memory.total = 74446048 erlang.processes.count = 402 erlang.processes.run_queue = 0 erlang.reductions = 148412771 .... 

Usando zabbix_sender enviaremos este archivo a zabbix, donde ya estará disponible una representación gráfica y la capacidad de crear disparadores de automatización y notificación.


Ahora que tenemos métricas en el sistema de monitoreo y disparadores de automatización y eventos de notificación creados sobre su base, tenemos la oportunidad de evitar accidentes al reaccionar con anticipación a todas las desviaciones peligrosas de un estado completamente funcional.


Recolección central de troncos


Cuando hay 1-2 servidores en un proyecto, es probable que aún pueda vivir sin una recopilación central de registros, pero tan pronto como aparece un sistema distribuido con muchos servidores, clústeres y entornos, se hace necesario resolver el problema de recopilación y visualización conveniente de registros.


Para escribir registros en mis proyectos, uso lager. A menudo, en el camino desde el prototipo hasta la producción, los proyectos pasan por las siguientes etapas de recopilación de registros:


  • Registro más simple con salida a un archivo local o incluso a stdout (lager_file_backend)
  • Recopilación centralizada de registros mediante, por ejemplo, syslogd y envío automático de registros al recopilador. Para tal esquema, lager_syslog es adecuado.
    La principal desventaja del esquema es que debe ir al servidor de recopilación de registros, encontrar el archivo con los registros necesarios y de alguna manera filtrar los eventos en busca de los necesarios para la depuración.
  • Colección centralizada de registros con almacenamiento en el repositorio con la capacidad de filtrar y buscar por registros.

Sobre los inconvenientes, las ventajas y las métricas cuantitativas que se pueden aplicar con este último, y hablaremos en el prisma de una implementación específica: lager_clickhouse , que utilizo en la mayoría de los proyectos en desarrollo. Algunas palabras sobre lager_clickhouse . Este es el back-end lager para guardar eventos en clickhouse. Por el momento, este es un proyecto interno, pero hay planes para hacerlo abierto. Al desarrollar lager_clickhouse, tuve que omitir algunas características de clickhouse, por ejemplo, usar el almacenamiento en búfer de eventos para evitar hacer solicitudes frecuentes en clickhouse. El esfuerzo invertido valió la pena con un funcionamiento estable y un buen rendimiento.


El principal inconveniente del enfoque para guardar en el repositorio es una entidad adicional: clickhouse y la necesidad de desarrollar código para guardar eventos en él y la interfaz de usuario para analizar y buscar eventos. Además, para algunos proyectos, puede ser crítico usar tcp para enviar registros.


Pero los profesionales, me parece, superan todas las desventajas posibles.


  • Búsqueda de eventos fácil y rápida:


    • Filtrar por fecha sin tener que buscar un archivo / archivos en un servidor central que contenga una variedad de eventos.
    • Filtrado por entorno. Los registros de diferentes subsistemas y, a menudo, de diferentes grupos se escriben en un repositorio. Por el momento, la separación se produce de acuerdo con las etiquetas que se establecen en cada nodo del clúster.
    • Filtrar por nombre de host
    • Filtrar por el nombre del módulo que envió el evento
    • Filtrado de eventos
    • Búsqueda de texto

    En la captura de pantalla se muestra una vista de ejemplo de la interfaz de visualización de registros:


  • Capacidad de automatización.
    Con la introducción del almacenamiento de registros, fue posible recibir en tiempo real información sobre la cantidad de errores, la ocurrencia de fallas críticas y la actividad del sistema. Al introducir ciertos límites, podemos generar eventos de emergencia cuando el sistema sale del estado funcional, cuyos manejadores realizarán acciones de automatización para eliminar este estado y enviar notificaciones a los miembros del equipo responsables de la funcionalidad:


    • Cuando ocurre un error crítico.
    • En el caso de errores en masa (la derivada del tiempo aumenta más rápido que un cierto límite).
    • Una métrica separada es la tasa de generación de eventos, es decir, cuántos eventos nuevos aparecen en el registro de eventos. Casi siempre puede saber la cantidad aproximada de registros generados por un proyecto por unidad de tiempo. Si se supera varias veces, lo más probable es que algo salga mal.


El desarrollo ulterior del tema de la automatización del manejo de eventos de emergencia fue el uso de scripts lua. Cualquier desarrollador o administrador puede escribir un script para procesar registros y métricas. Los scripts brindan flexibilidad y le permiten crear scripts y notificaciones de automatización personalizados.


Resumen


Para comprender los procesos que ocurren en el sistema e investigar incidentes, es vital tener indicadores cuantitativos y registros de eventos, así como herramientas convenientes para analizarlos. Cuanta más información recopilemos sobre el sistema, más fácil será analizar su comportamiento y corregir los problemas incluso en la etapa de su ocurrencia. En caso de que nuestras medidas no funcionen, siempre tenemos a nuestra disposición horarios y registros detallados del incidente.


¿Y cómo opera soluciones en Erlang / Elixir y qué casos interesantes encontró en la producción?

Source: https://habr.com/ru/post/437720/


All Articles