Este artículo contiene un breve extracto de mi propia experiencia y la experiencia de mis colegas, con quienes pasé días y noches rastrillando incidentes. Y muchos incidentes nunca habrían ocurrido si todos amaran que sus microservicios se escribieran al menos con un poco más de precisión.
Desafortunadamente, algunos programadores de bajo nivel creen seriamente que un Dockerfile con algún tipo de comando dentro es en sí mismo un microservicio y puede implementarse incluso ahora. Los estibadores giran, el banco está embarrado. Este enfoque está plagado de problemas que van desde una caída en el rendimiento, la incapacidad para depurar y la denegación de servicio hasta una pesadilla llamada inconsistencia de datos.
Si cree que ha llegado el momento de lanzar otra aplicación en Kubernetes / ECS / lo que sea, entonces tengo algo a lo que objetar.
La versión en inglés también está disponible .
Formé para mí un cierto conjunto de criterios para evaluar la preparación de las aplicaciones para su lanzamiento en producción. Algunos puntos de esta lista de verificación no se pueden aplicar a todas las aplicaciones, sino solo a las especiales. Otros generalmente se aplican a todo. Estoy seguro de que puede agregar sus opciones en los comentarios o disputar algunos de estos puntos.
Si su microservicio no cumple al menos uno de los criterios, no permitiré que esté en mi clúster ideal, construido en un búnker a 2000 metros bajo tierra con calefacción por suelo radiante y un sistema cerrado de suministro de Internet autónomo.
Vamos ...
Nota: el orden de los artículos no importa. De todos modos, para mi.
Léame Descripción breve
Contiene una breve descripción de sí mismo al comienzo de Readme.md en su repositorio.
Dios, parece tan simple. Pero con qué frecuencia me di cuenta de que el repositorio no contiene la más mínima explicación de por qué es necesario, qué tareas resuelve, etc. No es necesario hablar de algo más complicado, como las opciones de configuración.
Integración con un sistema de monitoreo.
Envía métricas a DataDog, NewRelic, Prometheus, etc.
Análisis del consumo de recursos, fugas de memoria, stacktraces, interdependencia del servicio, tasa de error: sin comprender todo esto (y no solo) es extremadamente difícil controlar lo que sucede en una aplicación distribuida de gran tamaño.
Alertas configuradas
El servicio incluye alertas que cubren todas las situaciones estándar más situaciones únicas conocidas.
Las métricas son buenas, pero nadie las seguirá. Por lo tanto, recibimos automáticamente llamadas / push / sms si:
- El consumo de CPU / memoria ha aumentado dramáticamente.
- El tráfico aumentó / cayó bruscamente.
- El número de transacciones procesadas por segundo ha cambiado drásticamente en cualquier dirección.
- El tamaño del artefacto después del ensamblaje ha cambiado drásticamente (exe, aplicación, jar, ...).
- El porcentaje de errores o su frecuencia excedió el umbral permitido.
- El servicio ha dejado de enviar métricas (situación que a menudo se pasa por alto).
- Se viola la regularidad de ciertos eventos esperados (el trabajo cron no funciona, no se procesan todos los eventos, etc.)
- ...
Runbooks creados
Se ha creado un documento para el servicio que describe contingencias conocidas o esperadas.
- cómo asegurarse de que el error es interno y no depende de terceros;
- si depende de dónde, a quién y qué escribir;
- cómo reiniciarlo de manera segura;
- cómo restaurar desde una copia de seguridad y dónde se encuentran las copias de seguridad;
- Qué paneles / consultas especiales se crean para monitorear este servicio;
- ¿El servicio tiene su propio panel de administración y cómo llegar allí?
- ¿hay una API / CLI y cómo usarla para solucionar problemas conocidos?
- Y así sucesivamente.
La lista puede variar mucho entre organizaciones, pero al menos las cosas básicas deberían estar ahí.
Todos los registros están escritos en STDOUT / STDERR
El servicio no crea ningún archivo de registro en modo de producción, no los envía a ningún servicio externo, no contiene abstracciones redundantes para la rotación de registros, etc.
Cuando una aplicación crea archivos de registro, estos registros son inútiles. No entrará en 5 contenedores que se ejecutan en paralelo, con la esperanza de detectar el error que necesita (y aquí está, llorando ...). Reiniciar el contenedor dará como resultado una pérdida completa de estos registros.
Si una aplicación escribe sus propios registros en un sistema de terceros, por ejemplo, en Logstash, esto crea redundancia inútil. El servicio vecino no sabe cómo hacer esto, porque ¿Tiene un marco diferente? Tienes un zoológico.
La aplicación escribe parte de los registros en archivos y parte en stdout porque es conveniente para el desarrollador ver INFO en la consola y DEPURAR en los archivos. Esta es generalmente la peor opción. Nadie necesita complejidad y códigos y configuraciones completamente redundantes que necesita conocer y mantener.
Los registros son Json
Cada línea de registro está escrita en formato Json y contiene un conjunto consistente de campos.
Hasta ahora, casi todos escriben registros en texto sin formato. Este es un verdadero desastre. Me alegraría no saber nunca sobre Grok Patterns . Sueño con ellos a veces y me congelo, tratando de no moverme, para no atraer su atención. Simplemente intente analizar las excepciones Java en los registros una vez.
Json es bueno, es fuego dado del cielo. Solo agregue allí:
- marca de tiempo de milisegundos según RFC 3339 ;
- nivel: información, advertencia, error, depuración
- user_id;
- nombre_aplicación
- y otros campos
Descargue a cualquier sistema adecuado (ElasticSearch configurado correctamente, por ejemplo) y disfrute. Conecte los registros de muchos microservicios y vuelva a sentir lo que eran buenas aplicaciones monolíticas.
(Y puede agregar Request-Id y obtener seguimiento ...)
Registros con niveles de verbosidad
La aplicación debe admitir una variable de entorno, por ejemplo LOG_LEVEL, con al menos dos modos de funcionamiento: ERRORES y DEPURACIÓN.
Es deseable que todos los servicios en el mismo ecosistema admitan la misma variable de entorno. No es una opción de configuración, no es una opción en la línea de comando (aunque esto es reversible, por supuesto), pero de forma predeterminada desde el entorno. Debería poder obtener tantos registros como sea posible si algo sale mal y tan pocos registros como sea posible, si todo está bien.
Versiones de dependencia fija
Las dependencias para los administradores de paquetes son fijas, incluidas las versiones menores (por ejemplo, cool_framework = 2.5.3).
Esto ya se ha discutido mucho, por supuesto. Algunas reparaciones dependen de las versiones principales, con la esperanza de que solo las correcciones de errores menores y las correcciones de seguridad estén en versiones menores. Esto esta mal.
Cada cambio en cada dependencia debe reflejarse en una confirmación por separado . Para que pueda cancelarse en caso de problemas. ¿Es difícil de controlar con las manos? Hay robots útiles, como este , que realizarán un seguimiento de las actualizaciones y crearán solicitudes de extracción para cada uno de ustedes.
Dockerized
El repositorio contiene Dockerfile y docker-compose.yml listos para producción.
Docker ha sido durante mucho tiempo el estándar para muchas empresas. Hay excepciones, pero incluso si no tiene Docker en producción, cualquier ingeniero debería ser capaz de componer Docker y no pensar en otra cosa para obtener un ensamblaje de desarrollo para la verificación local. Y el administrador del sistema debe tener el ensamblado ya verificado por los desarrolladores con las versiones necesarias de bibliotecas, utilidades, etc., en las que la aplicación al menos de alguna manera funciona para adaptarla a la producción.
Configuración del entorno
Todas las opciones de configuración importantes se leen desde el entorno y el entorno tiene una prioridad superior a los archivos de configuración (pero inferior a los argumentos de la línea de comandos al inicio).
Nadie querrá leer sus archivos de configuración y estudiar su formato. Solo acéptalo.
Más detalles aquí: https://12factor.net/config
Sondas de preparación y vitalidad.
Contiene puntos finales apropiados o comandos cli para evaluar la disponibilidad para atender solicitudes durante el inicio y el tiempo de actividad durante toda la vida.
Si la aplicación atiende solicitudes HTTP, debería tener dos interfaces por defecto:
Para verificar que la aplicación esté activa y no se congele, se utiliza una prueba de Liveness. Si la aplicación no responde, los orquestadores como Kubernetes pueden detenerla automáticamente, " pero esto no es exacto ". De hecho, matar una aplicación congelada puede causar un efecto dominó y poner permanentemente su servicio. Pero este no es un problema de desarrollador, solo haz este punto final.
Para verificar que la aplicación no acaba de iniciarse, sino que está lista para aceptar solicitudes, se realiza una prueba de preparación. Si la aplicación ha establecido una conexión con la base de datos, el sistema de colas, etc., debería responder con un estado de 200 a 400 (para Kubernetes).
Límites de recursos
Contiene límites en el consumo de memoria, CPU, espacio en disco y cualquier otro recurso disponible en un formato consistente.
La implementación específica de este elemento será muy diferente en diferentes organizaciones y para diferentes orquestadores. Sin embargo, estos límites deben establecerse en un solo formato para todos los servicios, ser diferentes para diferentes entornos (prod, dev, test, ...) y estar fuera del repositorio con el código de la aplicación .
El montaje y la entrega están automatizados.
El sistema CI / CD utilizado en su organización o proyecto está configurado y puede entregar la aplicación al entorno deseado de acuerdo con el flujo de trabajo aceptado.
Nunca se entrega nada a la producción manualmente.
No importa cuán difícil sea automatizar el ensamblaje y la entrega de su proyecto, esto debe hacerse antes de que este proyecto entre en producción. Este elemento incluye crear y ejecutar Ansible / Chef cookbooks / Salt / ..., crear aplicaciones para dispositivos móviles, crear una bifurcación del sistema operativo, crear imágenes de máquinas virtuales, lo que sea.
No se puede automatizar? Así que no puedes llevar esto al mundo. Después de ti, nadie lo recogerá.
Apagado elegante - apagado correcto
La aplicación puede procesar SIGTERM y otras señales e interrumpir sistemáticamente su trabajo después del final del procesamiento de la tarea actual.
Este es un punto extremadamente importante. Los procesos de Docker quedan huérfanos y funcionan durante meses en segundo plano donde nadie los ve. Las operaciones no transaccionales se rompen en el medio de la ejecución, creando inconsistencia de datos entre servicios y bases de datos. Esto conduce a errores que no se pueden prever y pueden ser muy, muy caros.
Si no controla ninguna dependencia y no puede garantizar que su código procesará SIGTERM correctamente, use algo como dumb-init .
Más información aquí:
Conexión de base de datos verificada regularmente
La aplicación hace ping constantemente a la base de datos y responde automáticamente a la excepción de "pérdida de conexión" para cualquier solicitud, tratando de restaurarla por sí sola o finaliza su trabajo correctamente
Vi muchos casos (esto no es solo un cambio de discurso) cuando los servicios creados para procesar colas o eventos perdieron su conexión por tiempo de espera y comenzaron a verter errores sin fin en los registros, devolviendo mensajes a las colas, enviándolos a Dead Letter Queue o simplemente no haciendo su trabajo.
Escalado horizontalmente
Con una carga creciente, es suficiente ejecutar más instancias de aplicación para garantizar que se procesen todas las solicitudes o tareas.
No todas las aplicaciones pueden escalar horizontalmente. Un ejemplo sorprendente son los consumidores de Kafka . Esto no es necesariamente malo, pero si una aplicación en particular no se puede iniciar dos veces, todas las partes interesadas deben saberlo con anticipación. Esta información debe ser una monstruosidad, colgar en el archivo Léame y siempre que sea posible. Algunas aplicaciones en general no pueden iniciarse en paralelo bajo ninguna circunstancia, lo que crea serias dificultades en su soporte.
Es mucho mejor si la aplicación en sí misma controla estas situaciones o si se escribe un contenedor que monitorea efectivamente a los "competidores" y simplemente no permite que el proceso comience o comience a trabajar hasta que otro proceso complete su trabajo o hasta que alguna configuración externa permita que N procesos trabajen simultáneamente.
Colas de mensajes no entregados y resistencia a mensajes malos
Si el servicio escucha colas o responde a eventos, cambiar el formato o el contenido de los mensajes no conduce a su caída. Los intentos fallidos de procesar la tarea se repiten N veces, después de lo cual el mensaje se envía a Dead Letter Queue.
Muchas veces he visto reiniciar sin cesar consumidores y líneas que se han inflado a tal tamaño que su procesamiento posterior tomó muchos días. Cualquier oyente de cola debe estar preparado para cambiar el formato, a errores aleatorios en el mensaje en sí (escribiendo datos en json, por ejemplo), o cuando se procesa por código hijo. Incluso me encontré con una situación en la que la biblioteca estándar para trabajar con RabbitMQ para un marco extremadamente popular no admitía reintentos, intentos de contadores, etc.
Peor aún, cuando un mensaje simplemente se destruye en caso de falla.
Limitación en la cantidad de mensajes procesados y tareas por proceso
Admite una variable de entorno, que se puede obligar a limitar el número máximo de tareas procesadas, después de lo cual el servicio se cerrará correctamente.
Todo fluye, todo cambia, especialmente la memoria. El gráfico de crecimiento continuo del consumo de memoria y OOM Killed al final es la norma en las mentes kuberneticas modernas. La implementación de una prueba primitiva que simplemente le ahorraría incluso la necesidad de examinar todas estas pérdidas de memoria facilitaría la vida. A menudo he visto a personas dedicar mucho tiempo y esfuerzo (y dinero) para detener esta rotación, pero no hay garantías de que el próximo compromiso de su colega no empeore. Si la aplicación puede sobrevivir una semana, este es un gran indicador. Deje que se termine y se reinicie. Esto es mejor que SIGKILL (sobre SIGTERM, ver arriba) o la excepción "sin memoria". Durante un par de décadas, este enchufe es suficiente para ti.
No utiliza integración de terceros con filtrado por direcciones IP
Si la aplicación realiza solicitudes a un servicio de terceros que permite el acceso desde direcciones IP limitadas, el servicio realiza estas llamadas indirectamente a través de un proxy inverso.
Este es un caso raro, pero extremadamente desagradable. Es muy inconveniente cuando un pequeño servicio bloquea la posibilidad de cambiar el clúster o mover toda la infraestructura a otra región. Si necesita comunicarse con alguien que no sabe cómo usar oAuth o VPN, configure el proxy inverso de antemano. No implemente en su programa la adición / eliminación dinámica de tales integraciones externas, ya que al hacerlo se clava en el único tiempo de ejecución disponible. Es mejor automatizar inmediatamente estos procesos para administrar las configuraciones de Nginx y, en su aplicación, contactarlo.
Agente de usuario HTTP obvio
El servicio reemplaza el encabezado User-agent con uno personalizado para todas las solicitudes a cualquier API, y este encabezado contiene suficiente información sobre el servicio en sí y su versión.
Cuando tiene 100 aplicaciones diferentes que se comunican entre sí, puede volverse loco al ver en los registros algo así como "Go-http-client / 1.1" y la dirección IP dinámica del contenedor de Kubernetes. Identifique siempre su aplicación y su versión explícitamente.
No viola la licencia
No contiene dependencias que limiten excesivamente la aplicación, no es una copia del código de otra persona, etc.
Este es un caso evidente, pero resultó que incluso el abogado que escribió la NDA ahora tiene hipo.
No usa dependencias no compatibles
Cuando inicia el servicio por primera vez, no incluye dependencias que ya están desactualizadas.
Si la biblioteca que ingresó al proyecto ya no es compatible con nadie, busque otra forma de lograr el objetivo o desarrollar la biblioteca en sí.
Conclusión
Hay algunas comprobaciones muy específicas en mi lista para tecnologías o situaciones específicas, pero me olvidé de agregar algo. Estoy seguro de que también encontrará algo para recordar.