Análisis en profundidad de los registros

Cuando me uní a Amazon después de la universidad, uno de los primeros ejercicios de incorporación a la empresa era poner en funcionamiento el servidor web de amazon.com en mi escritorio de desarrollador. No pude hacerlo bien la primera vez que lo intenté y no sabía en qué me había equivocado. Un compañero de trabajo me aconsejó que mirara los registros para identificar el problema. Para hacerlo, me dijo que debía usar “cat en el archivo de registro”. Estaba convencido de que se estaban burlando de mí o que estaban haciendo una broma que yo no entendía. En la universidad, solo había usado Linux para la recopilación y el uso del control de código fuente y del editor de textos. Por eso, no sabía que “cat” es un comando para imprimir un archivo en la terminal que podía introducir en otro programa para buscar patrones.

Mis compañeros de trabajo me mostraron herramientas como cat, grep, sed y awk. Con este nuevo conjunto de herramientas, me adentré en los registros del servidor web de amazon.com en mi escritorio de desarrollador. La aplicación del servidor web ya estaba equipada para emitir todo tipo de información útil en sus registros. Esto me permitió ver la configuración que me faltaba y que impedía que el servidor web arrancara, que mostraba dónde podría haber fallado el servidor, o que indicaba dónde fallaba la comunicación con algún servicio posterior. El sitio web está compuesto por muchas piezas movibles y, al comienzo, era básicamente un cuarto oscuro para mí. Sin embargo, luego de analizar en detalle el sistema, aprendí a descubrir cómo funcionaba el servidor y cómo interactuar con sus dependencias solo mirando los resultados de la instrumentación.

¿Para qué sirve la instrumentación?

Dado que me he trasladado de equipo en equipo durante estos años en Amazon, descubrí que la instrumentación es una lente invaluable a través de la cual miramos todos en Amazon para aprender cómo funciona un sistema. Sin embargo, la instrumentación no solamente es útil para aprender acerca de los sistemas. Es el núcleo de la cultura operativa de Amazon. Una buena instrumentación nos ayuda a saber qué tipo de experiencia le brindamos a nuestros clientes.
 
Este enfoque en el rendimiento operativo se extiende por toda la empresa. Dentro de los servicios asociados con amazon.com, el aumento de la latencia se traduce en una mala experiencia de compra y, por lo tanto, reduce las tasas de conversión. En el caso de los clientes que utilizan AWS, dependen de la alta disponibilidad y la baja latencia de los servicios de AWS.
 
En Amazon, no solo consideramos la latencia promedio. Nos centramos aún más en los valores atípicos de latencia, como los percentiles 99,90 y 99,99. Esto se debe a que, si una solicitud de cada 1000 o 10 000 es lenta, la experiencia no deja de ser mala. Observamos que cuando reducimos la latencia de percentil alto en un sistema, esta iniciativa tiene el efecto secundario de reducir la latencia media. Por el contrario, cuando reducimos la latencia media, no se reduce con tanta frecuencia la latencia de percentil alto.
 
La otra razón por la que nos enfocamos en la latencia de percentil alto es que la latencia alta en un servicio se puede extender a los demás. Amazon se basa en una arquitectura basada en los servicios. Muchos servicios colaboran entre sí para llevar a cabo diferentes tareas, como renderizar una página web en amazon.com. Como resultado, el aumento en la latencia de un servicio en lo profundo de la cadena de llamada, incluso si se trata de un aumento en un percentil alto, produce un efecto dominó en la latencia que experimenta el usuario final.
 
Los grandes sistemas de Amazon están formados por muchos servicios que cooperan entre sí. Hay un solo equipo que desarrolla y gestiona cada servicio (los servicios “grandes” están compuestos por múltiples servicios o componentes en segundo plano). El equipo que posee un servicio se conoce como el propietario del servicio. Cada miembro de ese equipo piensa como un propietario y operador del servicio, sin importar si es un desarrollador de software, un ingeniero de red, un administrador o cumple cualquier otra función. Como propietarios, los equipos establecen objetivos sobre el rendimiento operativo de todos los servicios asociados. También nos aseguramos de tener visibilidad sobre las operaciones del servicio para garantizar que podremos cumplir esos objetivos, resolver cualquier problema que pueda surgir y considerar objetivos aún más altos el año siguiente. Para fijar los objetivos y obtener esa visibilidad, los equipos deben instrumentar los sistemas.
La instrumentación también nos permite detectar los eventos operativos y brindar una respuesta estratégica.
 
La instrumentación suministra datos a los paneles operativos, de manera que los operadores puedan visualizar las métricas en tiempo real. También suministra datos a las alarmas, que se accionan e involucran a los operadores cuando el sistema se comporta de forma inesperada. Los operadores utilizan los resultados detallados de la instrumentación para diagnosticar rápidamente el error. A partir de allí, podemos mitigar el problema y volver a abordarlo más tarde para evitar que vuelva a producirse. Sin una buena instrumentación en todo el código, perdemos tiempo valioso en diagnosticar los problemas.

¿Qué debemos medir?

Para gestionar los servicios de acuerdo con nuestros altos estándares de disponibilidad y latencia, como propietarios de los servicios, debemos medir el comportamiento de nuestros sistemas.

Para obtener la telemetría necesaria, los propietarios de los servicios miden el rendimiento operativo desde varios lugares para obtener diferentes perspectivas de cómo se comportan los elementos en todo el sistema. Este proceso es complejo incluso en una arquitectura simple. Pensemos en un servicio al que los clientes llaman a través de un balanceador de carga: el servicio se comunica con una memoria caché y una base de datos remotas. Queremos que cada componente emita métricas respecto de su comportamiento. También queremos obtener métricas acerca de cómo percibe cada componente el comportamiento de los demás. Cuando se reúnen las métricas de todas estas perspectivas, el propietario de un servicio puede rastrear el origen de los problemas en poco tiempo y comenzar la investigación para encontrar la causa.

Muchos servicios de AWS proporcionan información operativa acerca de sus recursos automáticamente. Por ejemplo, Amazon DynamoDB proporciona métricas de Amazon CloudWatch sobre las tasas de éxito y error y la latencia, medidas por el servicio. Sin embargo, cuando creamos sistemas que utilizan estos servicios, necesitamos mucha más visibilidad acerca de cómo se comportan nuestros sistemas. La instrumentación requiere un código explícito que registre el tiempo que demoran las tareas, la frecuencia con la que se ejercen ciertas rutas de código, los metadatos sobre el trabajo de las tareas, y las partes de las tareas que se ejecutaron correctamente y las que fallaron. Si un equipo no agrega instrumentación explícita, se verá obligado a gestionar su propio servicio a ciegas.

Por ejemplo, si implementamos una operación de API de servicio que recupera la información de los productos a partir de su ID, el código podría lucir como en el siguiente ejemplo. Este código busca la información de los productos en una memoria caché local, luego en una memoria caché remota y, finalmente, en una base de datos:

public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {

  // check our local cache
  ProductInfo info = localCache.get(request.getProductId());
  
  // check the remote cache if we didn't find it in the local cache
  if (info == null) {
    info = remoteCache.get(request.getProductId());
	
	localCache.put(info);
  }
  
  // finally check the database if we didn't have it in either cache
  if (info == null) {
    info = db.query(request.getProductId());
	
	localCache.put(info);
	remoteCache.put(info);
  }
  
  return info;
}

Si gestionara este servicio, necesitaría mucha instrumentación en este código para poder comprender su comportamiento en producción. Debería tener la capacidad para resolver los problemas de las solicitudes lentas o fallidas y para detectar tendencias o señales que indiquen que diferentes dependencias no han escalado lo suficiente o se comportan de manera incorrecta. Este es el mismo código, con anotaciones de algunas preguntas que necesitarían respuestas acerca del sistema de producción como un todo o acerca de una solicitud en particular:

public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {

  // Which product are we looking up?
  // Who called the API? What product category is this in?

  // Did we find the item in the local cache?
  ProductInfo info = localCache.get(request.getProductId());
  
  if (info == null) {
    // Was the item in the remote cache?
    // How long did it take to read from the remote cache?
    // How long did it take to deserialize the object from the cache?
    info = remoteCache.get(request.getProductId());
	
    // How full is the local cache?
    localCache.put(info);
  }
  
  // finally check the database if we didn't have it in either cache
  if (info == null) {
    // How long did the database query take?
    // Did the query succeed? 
    // If it failed, is it because it timed out? Or was it an invalid query? Did we lose our database connection?
    // If it timed out, was our connection pool full? Did we fail to connect to the database? Or was it just slow to respond?
    info = db.query(request.getProductId());
	
    // How long did populating the caches take? 
    // Were they full and did they evict other items? 
    localCache.put(info);
    remoteCache.put(info);
  }
  
  // How big was this product info object? 
  return info;
}

El código para responder todas esas preguntas (y más) es un poco más largo que la lógica empresarial en sí. Algunas bibliotecas pueden ayudar a reducir la cantidad de código de instrumentación, pero el desarrollador aún debe formular las preguntas referidas a la visibilidad que necesitarán las bibliotecas y, luego, debe conectar la instrumentación deliberadamente.

Cuando resuelve los problemas de una solicitud que circula a través de un sistema distribuido, puede ser difícil comprender qué sucedió si solo observa la solicitud en función de una interacción. Para poder armar el rompecabezas, es útil agrupar en un solo lugar todas las mediciones de todos estos sistemas. Antes de poder hacerlo, se debe instrumentar cada servicio para registrar un ID de seguimiento para cada tarea y transmitir ese ID de seguimiento a los demás servicios que colaboran en esa tarea. Se puede recopilar la instrumentación de todos los sistemas para un ID de seguimiento determinado después del hecho, según se necesite, o casi en tiempo real, utilizando un servicio como AWS X-Ray.

Profundización en el tema

La instrumentación permite resolver problemas en múltiples niveles, desde observar las métricas en busca de anomalías que son demasiado sutiles para accionar las alarmas, hasta llevar a cabo una investigación para descubrir la causa de esas anomalías.

En el nivel más alto, la instrumentación se agrega a las métricas que pueden activar las alarmas y mostrarse en los paneles. Estas métricas combinadas permiten que los operadores monitoreen la tasa general de solicitudes, la latencia de las llamadas al servicio y las tasas de error. Estas alarmas y métricas nos permiten tomar conocimiento de las anomalías o los cambios que deberíamos investigar.

Luego de descubrir una anomalía, debemos descubrir por qué se produce. Para responder esa pregunta, utilizamos las métricas que proporciona la instrumentación. Si instrumentamos el tiempo que lleva ejecutar varias partes del proceso de cumplir una solicitud, podemos ver qué parte del proceso es más lenta de lo normal o produce errores con mayor frecuencia.

Si bien agregar temporizadores y métricas puede ayudarnos a descartar algunas causas y resaltar ciertas áreas de investigación, no siempre nos ofrece una explicación completa. Por ejemplo, a partir de las métricas, podríamos descubrir que los errores provienen de una operación de API en particular, pero es posible que las métricas no revelen los detalles específicos acerca de la causa de la falla. En este punto, observamos los datos detallados de registro sin procesar que emite el servicio para ese período. Entonces, los registros sin procesar muestran el origen del problema, ya sea el error en concreto o los aspectos particulares de la solicitud que desencadenan algún caso extremo.

¿Cómo instrumentamos?

La instrumentación requiere de la creación de código. Esto significa que cuando implementamos funciones nuevas, necesitamos tomarnos el tiempo para agregar código adicional a fin de indicar qué sucedió, si el resultado fue exitoso o no y cuánto tiempo tomó. Dado que la instrumentación es una tarea de codificación muy común, a lo largo de los años, surgió en Amazon la práctica de abordar los patrones comunes: la estandarización para las bibliotecas comunes de instrumentación y la estandarización para los informes de métricas estructurados y basados en los registros.

La estandarización de las bibliotecas para la instrumentación de métricas permite que los autores de las bibliotecas brinden a los consumidores visibilidad acerca de cómo trabaja la biblioteca. Por ejemplo, los clientes HTTP más utilizados se integran con estas bibliotecas comunes, de manera que si el equipo de un servicio implementa una llamada remota a otro servicio, obtienen instrumentación acerca de esas llamadas automáticamente.

Cuando una aplicación instrumentada ejecuta y lleva a cabo un trabajo, los datos de telemetría resultantes se incluyen en un archivo de registro estructurado. Generalmente, este archivo se emite como una entrada de registro por “unidad de trabajo”, ya sea si se trata de una solicitud a un servicio HTTP o un mensaje extraído de una cola.

En Amazon, las mediciones de la aplicación no se agregan ni se cargan ocasionalmente a un sistema de combinación de métricas. Todos los temporizadores y medidores para cada uno de los trabajos se incluyen en un archivo de registro. A partir de allí, algún otro sistema procesa los archivos y computa las métricas combinadas después de los hechos. De esta manera, obtenemos todo, desde las métricas operativas de alto nivel combinadas hasta la resolución detallada de los problemas de los datos a nivel de la solicitud, con un único enfoque en el código de instrumentación. En Amazon, primero efectuamos los registros y, luego, producimos las métricas combinadas.

Instrumentación a través de los registros

Generalmente, instrumentamos nuestros servicios para emitir dos tipos de datos de registro: los datos de solicitud y los de depuración. Los datos de registro de solicitud suelen estar representados como una única entrada de registro estructurada para cada unidad de trabajo. Estos datos contienen propiedades acerca de la solicitud, quién la emitió y qué se solicitó. Además, cuentan con medidores que indican cuán seguido suceden las cosas y temporizadores que indican cuánto duran. Los registros de solicitud sirven como un registro de auditoría y un seguimiento de todo lo que sucede en el servicio. La depuración de los datos incluye los datos sin estructurar o de estructura flexible acerca de cualquier línea de depuración que emita la aplicación. Generalmente, se trata de entradas de registro sin estructurar, como los errores Log4j o las líneas de registro de advertencia. En Amazon, estos dos tipos de datos suelen emitirse en archivos de registro separados, en parte por la tradición, pero también porque puede ser conveniente analizar los registros en un formato homogéneo de las entradas de registro.

Los agentes como el agente de CloudWatch Logs procesan ambos tipos de datos de registro en tiempo real y envían los registros a CloudWatch Logs. A cambio, CloudWatch Logs produce métricas combinadas acerca del servicio casi en tiempo real. Las alarmas de Amazon CloudWatch leen estas métricas combinadas y activan las alarmas.

Si bien puede ser costoso registrar tantos detalles de cada una de las solicitudes, en Amazon pensamos que es un procedimiento muy importante. Después de todo, debemos investigar las irregularidades en la disponibilidad, los picos de latencia y los problemas que informan los clientes. Sin registros detallados, no podemos brindar respuestas a los clientes ni mejorar el servicio que les proporcionamos.  

Más detalles

El tema del monitoreo y las alarmas es muy amplio. En este artículo, no cubriremos temas como la configuración y el ajuste de los umbrales de alarma, la organización de los paneles operativos, la medición del rendimiento tanto desde el lado del servidor como del cliente, que ejecutan aplicaciones “canary” constantemente, ni tampoco la elección del sistema adecuado para combinar métricas y analizar los registros.

Este artículo se enfoca en la necesidad de instrumentar nuestras aplicaciones para producir los datos de medición sin procesar adecuados. Describiremos las cosas que los equipos de Amazon procuran incluir (o evitar) cuando instrumentan las aplicaciones.

Prácticas recomendadas para el registro de solicitud

En esta sección, describiré los buenos hábitos que hemos adquirido con el tiempo en Amazon acerca del registro de nuestros datos estructurados “por unidad de trabajo”. Un registro que cumple con estos estándares contiene los medidores que indican cuán seguido suceden las cosas, los temporizadores que señalan cuánto tiempo llevan y las propiedades que incluyen los metadatos acerca de cada unidad de trabajo.

¿Cómo emitimos los registros?

• Emitimos una entrada de registro de solicitud para cada unidad de trabajo. En general, una unidad de trabajo consiste en una solicitud que recibe uno de nuestros servicios o un mensaje que extrae de una cola. Escribimos una entrada de registro de servicio por cada solicitud que recibe el servicio. No combinamos múltiples unidades de trabajo. De esta manera, cuando solucionamos los problemas de una solicitud fallida, tenemos que analizar una sola entrada de registro. Esta entrada contiene los parámetros de entrada relevantes acerca de la solicitud para que conozcamos cuál era su intención, información sobre quién era el intermediario y también toda la información acerca de los tiempos y conteos en un solo lugar.
• No emitimos más de una entrada de registro de solicitud para una solicitud determinada. En la implementación de un servicio sin bloqueo, puede parecer conveniente emitir una entrada de registro independiente para cada etapa de una canalización de procesamiento. Sin embargo, obtenemos mejores resultados si resolvemos los problemas de estos sistemas mediante la instalación de un mando en un solo “objeto de métrica” entre las etapas de la canalización y la serialización de las métricas como una unidad luego de haber completado todas las etapas. Tener varias entradas de registro por unidad de trabajo dificulta su análisis e incrementa aún más el gasto operativo del registro con un multiplicador. Si escribimos un nuevo servicio sin bloqueo, intentamos planificar con anticipación el ciclo de vida de los registros de las métricas debido a que se torna muy difícil refactorizarlo y repararlo más adelante.
• Dividimos las tareas de larga duración en varias entradas de registro. Al contrario de la recomendación anterior, si tenemos una tarea de carga de trabajo de larga duración de varios minutos u horas, podemos decidir emitir una entrada de registro independiente de forma periódica, de manera que podamos determinar si hay un progreso o demoras.
• Registramos los detalles acerca de la solicitud antes de ejecutar actividades como la validación. Es importante para la resolución de los problemas y el registro de las auditorías registrar información suficiente acerca de la solicitud de manera que comprendamos qué se intentaba obtener. También es fundamental registrar esta información lo más pronto posible, antes de que la solicitud pueda ser rechazada en las etapas de validación, autenticación o lógica de limitación controlada. Si registramos información de la solicitud entrante, nos aseguramos de sanear la entrada (con codificación, evitación y truncado) antes de registrarla. Por ejemplo, no queremos incluir cadenas de 1 MB de largo en la entrada de registro de nuestro servicio si el intermediario envió alguna. Si lo hacemos, corremos el riesgo de llenar los discos y que el almacenamiento de registros tenga un costo mayor de lo esperado. Otro ejemplo de saneamiento es filtrar los caracteres de control ASCII o evitar las secuencias pertinentes para el formato del registro. Sería confuso que el intermediario enviara una entrada de registro de servicio propia y la introdujera en nuestros registros. Véase también: https://xkcd.com/327/
• Planificamos una forma de emitir registros con un mayor nivel de detalle. Para resolver algunos tipos de problemas, el registro no tendrá los detalles suficientes acerca de las solicitudes problemáticas para descubrir por qué fallaron. Es posible que esa información esté disponible en el servicio, pero el volumen de información puede ser demasiado grande para que se justifique mantener un registro permanente. Podría ser útil tener un botón de configuración que pueda accionar para incrementar el nivel de detalle de los registros temporalmente mientras investiga un problema. Puede accionar el botón para hosts individuales, clientes determinados o una frecuencia de muestreo en toda la flota. No debemos olvidarnos de desactivar el botón al finalizar.
• Usamos nombres cortos para las métricas (pero no demasiado cortos). Amazon ha utilizado la misma serialización de los registros de servicios durante más de 15 años. En esta serialización, el nombre de cada medidor y temporizador se repite en el texto sin formato de todas las entradas de registro de servicio. Para minimizar la sobrecarga del registro, usamos nombres de temporizadores cortos pero descriptivos. Amazon está comenzando a adoptar nuevos formatos de serialización basados en un protocolo de serialización binario, conocido como Amazon Ion. Finalmente, es importante elegir un formato que las herramientas de análisis de registros puedan comprender y que también sea tan eficiente para serializar, deserializar y almacenar los registros como sea posible.
• Garantizamos que los volúmenes de los registros tengan el tamaño suficiente para gestionar los registros con un máximo rendimiento. Hacemos pruebas de carga máxima sostenida (o incluso sobrecarga) durante horas en nuestros servicios. Debemos asegurarnos de que, cuando el servicio gestione un exceso de tráfico, todavía contará con los recursos para enviar los registros a la velocidad con la que producen nuevas entradas de registro. De lo contrario, los discos se llenarán eventualmente. También puede establecer que el registro se efectúe en una partición de sistema de archivos diferente a la partición raíz, de manera que el sistema no deje de funcionar ante una cantidad excesiva de registros. Más adelante, discutiremos otras formas de mitigación, como utilizar el muestreo dinámico proporcional al rendimiento; pero, independientemente de la estrategia, es fundamental realizar pruebas.
• Consideramos el comportamiento del sistema cuando los discos se llenan. Si el disco de un servidor se llena, el servidor no puede efectuar registros. Cuando esto sucede, ¿el servicio debería dejar de aceptar solicitudes? o ¿debería dejar de emitir registros y continuar funcionando sin monitoreo? Operar sin emitir registros es riesgoso, por lo que probamos nuestros sistemas para detectar los servidores cuyos discos están casi llenos.
• Sincronizamos los relojes. La noción de “tiempo” en los sistemas distribuidos es muy complicada. No dependemos de la sincronización de los relojes en los algoritmos distribuidos, pero es necesaria para que los registros tengan una lógica. Ejecutamos programas como Chrony o ntpd para la sincronización de los relojes y monitoreamos los servidores para detectar los cambios en los relojes. Para comprender mejor este concepto, consulte Amazon Time Sync Service.
• Emitimos recuentos iguales a cero para las métricas de disponibilidad. Los recuentos de errores son útiles, pero los porcentajes de error también pueden serlo. Una estrategia que resulta útil para instrumentar una métrica “de porcentaje de disponibilidad” es emitir un 1 cuando la solicitud se ejecuta correctamente y un 0, cuando falla. Entonces, la estadística “promedio” de la métrica resultante es la tasa de disponibilidad. Emitir un punto de datos 0 de forma intencional también puede ser útil en otras situaciones. Por ejemplo, si una aplicación lleva a cabo la elección de líder, emitir un 1 periódicamente cuando un proceso es líder y un 0 cuando no lo es puede servir para monitorear el estado de los sucesores. De esta manera, si un proceso deja de emitir 0, es más fácil saber si algo no está funcionando bien y ese proceso no podrá hacerse cargo si algo le sucede al líder.

¿Qué registramos?

• Registramos la disponibilidad y la latencia de todas las dependencias. Descubrimos que esto es útil, en especial, cuando respondemos las preguntas “¿por qué estuvo lenta la solicitud?” o “¿por qué falló la solicitud?”. Sin este registro, solo podemos comparar los gráficos de las dependencias con los gráficos de un servicio y adivinar si un pico en la latencia de un servicio dependiente condujo al error de la solicitud que estamos investigando. Muchos marcos de servicios y clientes asocian las métricas de manera automática, pero otros marcos, como el de AWS SDK, requieren una instrumentación manual.
• Desglosamos las métricas de dependencia por llamada, recurso, código de estado, etc. Si interactuamos con la misma dependencia varias veces en la misma unidad de trabajo, incluimos métricas acerca de cada llamada por separado y especificamos con qué recurso interactuó cada solicitud. Por ejemplo, cuando hacen una llamada a Amazon DynamoDB, algunos equipos consideran útil incluir métricas de tiempo y latencia por tabla, por código de error e incluso por la cantidad de reintentos. Esto facilita la solución de problemas en los casos en que los reintentos de un servicio fueron lentos debido a los errores de verificación condicional. Estas métricas también revelaron casos en los que los aumentos de la latencia percibidos por los clientes se debieron en realidad a los reintentos de la limitación controlada o a la paginación a través de un conjunto de resultados, y no a la pérdida de paquetes o la latencia de la red.
• Registramos la profundidad de las colas de memoria cuando accedemos a ellas. Si una solicitud interactúa con una cola, y extraemos un objeto de ella o colocamos uno en ella, registramos la profundidad actual de la cola en el objeto de métrica mientras nos encontramos en ella. Para las colas en memoria, obtener esta información no genera grandes costos. Para las colas distribuidas, estos metadatos pueden estar disponibles sin costo en las respuestas a las llamadas a la API. Estos registros ayudarán a encontrar las demoras y las fuentes de latencia en el futuro. Además, cuando retiramos elementos de una cola, medimos el tiempo durante el que estuvieron en ella. Esto significa que debemos agregar nuestra propia métrica de “tiempo en cola” en el mensaje antes de colocarlo en la cola en primer lugar.
• Agregamos un medidor adicional para cada una de las razones de error. Considere agregar un código que cuente las razones específicas de error para cada solicitud fallida. El registro de la aplicación incluirá la información que condujo al error y un mensaje detallado de la excepción. Sin embargo, también es útil observar las tendencias en las razones de error en las métricas con el paso del tiempo, sin tener que buscar esa información en los registros de la aplicación. Es práctico comenzar con una métrica independiente para cada clase de excepción de error.
• Organizamos los errores por categoría de causa. Si todos los errores se agrupan en la misma métrica, esta se torna ruidosa y poco útil. Descubrimos que es importante separar, al menos, los errores que fueron “culpa del cliente” de los que fueron “culpa del servidor”. Más allá de eso, un desglose adicional también podría ser útil. Por ejemplo, en DynamoDB, los clientes pueden efectuar solicitudes de escritura condicional que arrojen un error si el elemento que están modificando no coincide con las condiciones previas de la solicitud. Estos errores son deliberados, y esperamos que se produzcan de vez en cuando. Mientras que los errores de “solicitud inválida” de los clientes son probablemente los errores que debemos corregir.
• Registramos los metadatos importantes acerca de la unidad de trabajo. En un registro métrico estructurado, también incluimos suficientes metadatos acerca de la solicitud para que podamos determinar más adelante de quién era la solicitud y qué intentaba obtener. Esto incluye los metadatos que el cliente esperaría que tengamos en nuestros registros cuando se produzcan problemas. Por ejemplo, DynamoDB registra el nombre de la tabla con la que interactúa una solicitud y los metadatos como los referidos a si la operación de lectura consistió en una lectura uniforme o no. Sin embargo, no registra los datos almacenados en la base de datos o los recuperados de ella.
• Protegemos los registros con el control de acceso y el cifrado. Dado que los registros contienen información con cierto grado de confidencialidad, tomamos medidas para proteger y asegurar esos datos. Estas medidas incluyen cifrar los registros, limitar el acceso a los operadores que estén resolviendo los problemas y establecer regularmente los límites de ese acceso.
• Evitamos incluir información de extrema confidencialidad en los registros. Los registros deben contener cierta información confidencial para ser útiles. En Amazon, consideramos importante que los registros incluyan suficiente información como para saber quién envió determinada solicitud, pero no incluimos información de extrema confidencialidad, como los parámetros de las solicitudes que no influyen en el direccionamiento o el comportamiento del procesamiento de la solicitud. Por ejemplo, si el código analiza el mensaje de un cliente y ese análisis falla, es importante no registrar la carga para proteger la privacidad del cliente, por más que eso pueda dificultar la resolución de los problemas más adelante. Utilizamos herramientas para decidir qué datos pueden registrarse con un método de aceptación y no de rechazo, con el fin de evitar el registro de un nuevo parámetro confidencial que se agregue después. Los servicios como Amazon API Gateway permiten establecer qué datos se incluirán en su registro de acceso, que actúa como un buen mecanismo de aceptación.
• Registramos un ID de seguimiento y lo propagamos en las llamadas de backend. La solicitud de un cliente determinado probablemente implicará que muchos servicios trabajen de manera cooperativa. Pueden ser tan solo dos o tres servicios para muchas de las solicitudes de AWS, o muchos servicios más para las solicitudes de amazon.com. Para comprender qué sucede cuando resolvemos los problemas en un sistema distribuido, propagamos el mismo ID de seguimiento entre estos sistemas a fin de poder alinear los registros de varios sistemas y determinar dónde se produjeron los errores. Un ID de seguimiento es un tipo de ID de metasolicitud que se graba en una unidad de trabajo distribuida por parte del servicio “de puerta de entrada” que fue el punto de partida de la unidad de trabajo. AWS X-Ray es un servicio que ayuda a proporcionar parte de esta propagación. Consideramos importante transmitir el seguimiento a nuestras dependencias. En un entorno de múltiples subprocesos, es muy difícil que el marco realice esta propagación en nuestro nombre y es propenso a errores. Por eso, acostumbramos a transmitir los ID de seguimiento y otro tipo de contenido de las solicitudes (como los objetos de métrica) en nuestras firmas de método. También es práctico transferir un objeto de contexto en nuestras firmas de método, así no es necesaria la refactorización cuando encontremos un patrón similar que debamos transmitir en el futuro. Para los equipos de AWS, no solo se trata de solucionar los problemas de nuestros sistemas, sino también de que los clientes solucionen los suyos. Los clientes confían en que el seguimiento de AWS X-Ray se transmita entre los servicios de AWS cuando interactúan entre sí en nombre de los clientes. Esto requiere que propaguemos los ID de seguimiento de AWS X-Ray entre los servicios de manera que puedan obtener todos los datos de seguimiento.
• Registramos las diferentes métricas de latencia según el código de estado y el tamaño. Los errores suelen ser rápidos, como las respuestas de acceso denegado, limitación controlada y error de validación. Si aumenta la tasa de limitación controlada de los clientes, la latencia puede parecer engañosamente buena. Para evitar esta contaminación de las métricas, registramos un temporizador independiente para las respuestas de operación correcta y nos enfocamos en esas métricas en los paneles y las alarmas, en lugar de utilizar una métrica de tiempo genérica. De manera similar, si alguna operación puede ser más lenta en función del tamaño de la entrada o la respuesta, consideramos emitir una métrica de latencia categorizada, como Latencia de solicitud pequeña y Latencia de solicitud grande. Además, nos aseguramos de que nuestras solicitudes y respuestas estén limitadas de manera adecuada a fin de evitar caídas de tensión y modos de falla complejos. Sin embargo, incluso en un servicio diseñado cuidadosamente, esta técnica de buckets de métricas puede aislar el comportamiento de los clientes y mantener el ruido molesto fuera de los paneles.

Prácticas recomendadas para el registro de las aplicaciones

En esta sección, describimos los buenos hábitos que hemos adquirido en Amazon acerca de cómo registrar los datos de registro de depuración sin estructura.

• Mantenemos el registro de las aplicaciones libre de spam. Si bien podemos tener las instrucciones de registro de nivel INFO y DEBUG en la ruta de la solicitud para ayudar con el desarrollo y la depuración en los entornos de prueba, consideramos desactivar estos niveles de registro en la producción. En lugar de depender del registro de las aplicaciones para obtener información de seguimiento de las solicitudes, pensamos en el registro del servicio como una ubicación para la información de seguimiento donde podemos producir métricas fácilmente y observar las tendencias combinadas a lo largo del tiempo. Sin embargo, no todo es blanco y negro. Nuestra estrategia es revisar constantemente los registros para ver si tienen mucho ruido (o no tiene el suficiente) y ajustar los niveles de registro a lo largo del tiempo. Por ejemplo, cuando analizamos los registros, a menudo encontramos instrucciones de registro que son muy ruidosas, o métricas que quisiéramos tener. Por fortuna, estas mejoras muchas veces son fáciles de implementar, por lo que nos acostumbramos a archivar los elementos de los trabajos pendientes de seguimiento rápido para mantener limpios los registros.
• Incluimos el ID de solicitud correspondiente. Generalmente, cuando estamos solucionando un error en el registro de la aplicación, queremos ver los detalles acerca de la solicitud o del intermediario que desencadenó el error. Si ambos registros contienen el mismo ID de solicitud, podemos pasar de un registro al otro con facilidad. Las bibliotecas de registros de las aplicaciones escribirán el ID de solicitud correspondiente si está configurado de manera correcta, y se establecerá el ID como ThreadLocal. Si una aplicación tiene varios subprocesos, tenga especial cuidado en establecer el ID de solicitud correcto cuando uno de los subprocesos comience a trabajar en una nueva solicitud.
• Limitamos la tasa de spam de error del registro de una aplicación. Por lo general, un servicio no emitirá mucho al registro de la aplicación, pero si de repente exhibe un gran volumen de errores, puede comenzar a escribir una alta tasa de entradas de registros muy grandes con el seguimiento de las pilas. Una forma de protegernos contra este siniestro es limitar la frecuencia con la que un registrador determinado efectúa registros.
• Preferimos las cadenas de formato en lugar de String#format o la concatenación de cadenas. Las operaciones más antiguas de la API de registro de la aplicación aceptan un solo mensaje de cadena en vez de la API de cadena de formato varargs de log4j2. Si el código se instrumenta con instrucciones DEBUG, pero la producción se configura al nivel de ERROR, es posible que se desperdicie trabajo cuando se formateen las cadenas de mensajes DEBUG que son ignoradas. Algunas operaciones de registro de la API admiten el envío de objetos arbitrarios cuyos métodos toString() se llamarán solo si se escribe la entrada de registros.
• Registramos los ID de solicitud de las llamadas de servicios fallidas. Si se llama a un servicio y este arroja un error, probablemente el servicio devolvió un ID de solicitud. Es útil incluir el ID de solicitud en nuestros registros, de manera que, si necesitamos hacer un seguimiento con el propietario de ese servicio, ofrecemos un método fácil para que pueda encontrar sus propias entradas de registro de servicio correspondientes. Los errores de tiempo de espera complican esto porque puede que el servicio aún no haya devuelto un ID de solicitud o que la biblioteca del cliente no lo haya analizado. No obstante, si el servicio devuelve un ID de solicitud, lo registramos.

Prácticas recomendadas para un alto rendimiento de los servicios

Para la gran mayoría de los servicios de Amazon, registrar cada solicitud no implica un costo exageradamente alto. Los servicios de más alto rendimiento ingresan en un área más gris, pero generalmente registramos todas las solicitudes de igual modo. Por ejemplo, es natural asumir que DynamoDB, atendiendo en su punto máximo más de 20 millones de solicitudes por segundo solo del tráfico interno de Amazon, no efectúa muchos registros; pero, en realidad, registra todas las solicitudes con el fin de resolver los problemas y también por razones de auditoría y cumplimiento. A continuación, presentamos algunos consejos que aplicamos en Amazon para aumentar la eficiencia y el rendimiento por host del proceso de registro:

• Muestreo de registros. En lugar de escribir todas las entradas, considere escribir cada cierto número de entradas. Cada entrada también incluye cuántas se omitieron, de manera que los sistemas de combinación de métricas puedan estimar el verdadero volumen de registros en las muestras que computan. Algunos algoritmos de muestreo, como el muestreo de reservas, proporcionan muestras más representativas. Otros algoritmos priorizan los errores de registro o las solicitudes lentas por sobre las solicitudes rápidas y correctas. Sin embargo, con el muestreo, se pierde la capacidad de ayudar a los clientes y de solucionar los errores específicos. Algunos requisitos de cumplimiento prohíben esta técnica por completo.
• Descarga de la serialización y los registros en un subproceso diferente. Este es un cambio fácil que se usa comúnmente.
• Rotación frecuente de los registros. La rotación de los archivos de registro cada una hora puede parecer conveniente, ya que tendrá menos archivos que gestionar, pero rotarlos cada un minuto puede mejorar muchas cosas. Por ejemplo, el agente que lee y comprime el archivo de registro leerá el archivo desde la memoria caché de la página y no desde el disco, y los procesos de CPU y E/S de compresión y envío de los registros se repartirán en toda la hora en lugar de activarse siempre al final de la hora.
• Escritura de registros comprimidos previamente. Si un agente que envía registros los comprime antes de enviarlos a un servicio de archivado, la CPU y el disco del sistema alcanzarán el pico de forma periódica. Es posible amortizar este costo y reducir los procesos de E/S del disco a la mitad si se transmiten los registros comprimidos al disco. Sin embargo, esto acarrea algunos riesgos. Descubrimos que es útil usar un algoritmo de compresión que pueda gestionar los archivos truncados en el caso de que una aplicación deje de funcionar.
• Escritura en una memoria RAM o un sistema tmpfs. Puede ser más fácil que un servicio escriba los registros en la memoria hasta que se envíen fuera del servidor, en lugar de escribirlos en el disco. Según nuestra experiencia, esto funciona mejor con la rotación de registros cada un minuto en lugar de cada una hora.
• Combinaciones en memoria. Si es necesario gestionar cientos de miles de operaciones por segundo en un solo equipo, podría resultar muy costoso escribir una entrada de registro única por cada solicitud. Sin embargo, se pierde mucha visibilidad al omitir este procedimiento, por lo que resulta útil no acudir a la optimización prematura.
• Monitoreo del uso de los recursos. Estamos atentos a cuán cerca estamos de alcanzar algún límite de escalado. Medimos los procesos de E/S y CPU por servicio, así como cuántos de esos recursos ocupan los agentes de registros. Cuando realizamos pruebas de carga, las ejecutamos durante el tiempo suficiente para probar que los agentes de envío de registros pueden seguir el ritmo de nuestro rendimiento.

La importancia de tener las herramientas de análisis de registros adecuadas

En Amazon, utilizamos los servicios que escribimos, por lo que necesitamos convertirnos en expertos en la solución de sus problemas. Esto incluye ser capaces de efectuar el análisis de los registros sin esfuerzo. Tenemos muchas herramientas a nuestra disposición, desde el análisis local de registros para examinar una cantidad relativamente pequeña de registros, hasta el análisis distribuido de registros para examinar y agregar resultados en un gran volumen de registros.

Consideramos que es importante invertir en las herramientas de los equipos y en los runbooks para el análisis de los registros. Si los registros son pequeños ahora, pero se espera que un servicio crezca con el tiempo, estaremos atentos al momento en que nuestras herramientas actuales dejen de escalar, de manera que podamos invertir en la adopción de una solución de análisis distribuido de registros.

Análisis local de registros

El proceso de análisis de registros puede requerir experiencia en varias utilidades de línea de comandos de Linux. Por ejemplo, una acción común de “encontrar las direcciones IP con más comunicaciones del registro” consiste simplemente en esto:

cat log | grep -P "^RemoteIp=" | cut -d= -f2 | sort | uniq -c | sort -nr | head -n20

Sin embargo, hay muchas otras herramientas que son útiles para responder preguntas más complejas con nuestros registros, incluidas las siguientes:

• jq: https://stedolan.github.io/jq/
• RecordStream: https://github.com/benbernard/RecordStream

Análisis distribuido de registros

Se puede utilizar cualquier servicio de análisis de big data para ejecutar un análisis distribuido de registros; por ejemplo, Amazon EMR, Amazon Athena, Amazon Aurora y Amazon Redshift. Sin embargo, algunos servicios vienen equipados con sistemas de registro, como Amazon CloudWatch Logs.

• CloudWatch Logs Insights
• AWS X-Ray: https://aws.amazon.com/xray/
• Amazon Athena: https://aws.amazon.com/athena/

Conclusión

Como propietario de un servicio y desarrollador de software, paso una gran cantidad de tiempo observando los resultados de la instrumentación, como gráficos en los paneles o archivos individuales de registro, y utilizando las herramientas de análisis distribuido de registros, como CloudWatch Logs Insights. Estas son algunas de las cosas que más disfruto. Cuando necesito un descanso luego de finalizar alguna tarea desafiante, recargo energías y me doy un gusto con alguna investigación sobre los registros. Comienzo con preguntas como “¿por qué esta métrica alcanza el pico en este punto?” o “¿podría ser más baja la latencia de esta operación?”. Cuando las preguntas me llevan a un callejón sin salida, a menudo descubro una medida que podría ser útil en el código, por lo que agrego la instrumentación, la pruebo y envío una revisión del código a mis compañeros de equipo.

A pesar de que muchas métricas vienen con los servicios administrados que utilizamos, debemos invertir mucho tiempo en pensar cómo instrumentar nuestros propios servicios a fin de obtener la visibilidad que necesitamos para gestionarlos con eficiencia. Durante los eventos operativos, debemos determinar rápidamente por qué tenemos un problema y cómo podemos mitigarlo. Es fundamental tener las métricas adecuadas en los paneles para hacer el diagnóstico de manera rápida. Además, dado que modificamos los servicios, agregamos características nuevas y cambiamos la forma en la que interactuamos con sus dependencias constantemente, los procesos de actualizar y agregar la instrumentación adecuada son tareas que no tienen fin.

• “Look at your data”, por John Rauser, exmiembro de Amazon: https://www.youtube.com/watch?v=coNDCIMH8bk (en el minuto 13:22, literalmente imprime los registros para poder observarlos mejor)
• “Investigating anomalies”, por John Rauser, exmiembro de Amazon: https://www.youtube.com/watch?v=-3dw09N5_Aw
• “How humans see data”, por John Rauser, exmiembro de Amazon: https://www.youtube.com/watch?v=fSgEeI2Xpdc
• https://www.akamai.com/uk/en/about/news/press/2017-press/akamai-releases-spring-2017-state-of-online-retail-performance-report.jsp


Acerca del autor

David Yanacek es ingeniero jefe sénior en AWS Lambda. David ha sido desarrollador de software en Amazon desde el año 2006. Anteriormente, trabajó en Amazon DynamoDB y AWS IoT, así como en los marcos de servicios web internos y en los sistemas de automatización de las operaciones de la flota. Una de las actividades preferidas de David en el trabajo es llevar a cabo análisis de registros y examinar las métricas operativas para encontrar formas de mejorar el funcionamiento de los sistemas con el paso del tiempo.

Uso de la eliminación de carga para evitar la sobrecarga Cómo evitar demoras de colas insuperables