Monitoriza tus sistemas (I)

Monitoriza tus sistemas (I)

Pedro Miguel Rodríguez, Ingeniero de Software Senior en SNGULAR

Pedro Miguel Rodríguez

Ingeniero de Software Senior en SNGULAR

23 de julio de 2021

¿Qué vamos a ver?

¿Alguna vez te has encontrado analizando un escurridizo ‘bug’ mientras buceabas entre interminables líneas de log?. ¿Has tenido que relacionar cronológicamente eventos de diferentes servicios y te has sentido identificado con el siguiente personaje?

monitoring-blog-post

Tranquilo, no estás solo.

Este proceso, además de complejo, puede requerir mucho tiempo y a la larga resulta bastante frustrante. Y si a esto sumamos el estrés causado cuando el ‘bug’ proviene del entorno de producción... ¡Faltaría más!.

¿Pero, en serio no hay ninguna solución para esto? Cuando un proyecto crece, ¿las empresas (grandes y pequeñas) no tienen más alternativas que aumentar los recursos dedicados a este tipo de tareas?

¿Y si te dijera que existen herramientas que además de hacerte la vida más fácil te aportan información extra muy útil para el control de tus aplicaciones?, ¿te gustaría tener acceso a un panel en el que de un vistazo puedas identificar cualquier problema?. Vamos a aprender paso a paso a convertir tu aplicación, de una forma rápida y práctica, en un sistema monitorizado eficaz.

Contexto

En cualquier software es primordial conocer qué ocurre en todo momento y detectar cuándo y porqué algo no funciona correctamente. Tanto en aplicaciones de escritorio como en arquitecturas monolíticas, el proceso siempre ha sido tan ‘sencillo’ como revisar una consola o algún fichero de log. En arquitecturas orientadas a servicios, la cosa comenzaba a complicarse, dado que debíamos realizar esta tarea por cada uno de los servicios que lo componían. Para arquitecturas de microservicios el problema se dispara dados los aspectos de efimeridad, escalabilidad y modularidad. Por ello, se hace imprescindible un sistema centralizado con el que sea posible coleccionar, analizar y relacionar datos del sistema que permita interpretar el funcionamiento o estado de nuestra aplicación y nos facilite la detección de errores.

Conceptos

A continuación vamos a aclarar algunos conceptos importantes:

Diferencia entre Observabilidad y Monitorización

Seguramente ya has escuchado hablar sobre estos dos conceptos y quizás no has sabido cuál es la diferencia. Vamos a identificar los matices:

Observabilidad: según wikipedia podría definirse como “medición que determina cómo los estados internos de un sistema pueden ser deducidos a través de las salidas externas”.

Monitorización: acción de seguimiento de estas señales o salidas externas para evaluar el estado o salud del sistema.

Quizás pueda parecer algo similar. Lo veremos más fácil con un ejemplo. Si lo trasladamos al mundo de la medicina diríamos que si un paciente (sistema observable) no facilita a su doctor (sistema de monitorización) sus síntomas, alimentación, conductas, etc. (señales o salidas externas) éste no podrá diagnosticar con eficacia qué le está ocurriendo.

monitoring-blog-post-ilustration

«La monitorización te dice si el sistema funciona. La observabilidad te permite preguntar por qué no funciona».

Baron Schwartz

Los tres pilares de la observabilidad

Como vimos en el apartado anterior, es de gran importancia que nuestro sistema tenga un adecuado grado de observabilidad. A continuación describiremos brevemente las principales señales utilizadas para conseguirlo:

Logs

Registros temporales de eventos. Se trata del componente básico de trazabilidad de un sistema. Permite almacenar en un fichero la información, estados y valores de contexto de un momento determinado.

Paciente: “Doctor, en los últimos meses no me encuentro bien. Me siento hinchado y me duele la tripa con frecuencia”.

Métricas

Las métricas son una representación numérica de datos medidos en intervalos de tiempo. Permiten recopilar una serie de valores de diferentes conceptos que nos facilitan la realización de consultas más precisas.

Paciente: “Aquí le traigo los resultados del análisis de sangre que me pidió”.

Trazas

Identifican el flujo de eventos en un sistema distribuido. Permite distinguir los componentes/servicios involucrados en un proceso así como las condiciones y efectos que se han ido cumpliendo hasta llegar a la situación final.

Paciente: “El pasado domingo me encontraba genial. Salí a desayunar a una cafetería donde hacen unos croissants riquísimos y al momento comenzó el dolor de estómago”.

Doctor: “Querido Paciente, es probable que usted sea intolerante al gluten”.

Cuantos más datos tengamos sobre un sistema, más fácil será detectar la causa de un problema. Estará en manos de los arquitectos de sistemas, dependiendo de la complejidad del caso, determinar cuántas señales serán suficientes y su grado de detalle.

En este primer post comenzaremos centrándonos en el primero de los pilares que hemos mencionado: los logs.

Monitorización de logs

Como hemos visto anteriormente, los logs han sido históricamente una de las pocas vías para detectar un problema dentro de nuestra aplicación. Dada su sencillez (a simple vista no es más que un mensaje de texto) está en nuestra mano su uso correcto y eficiente. Vamos a ver algunos puntos a tener en cuenta a la hora de trabajar con logs:

  • Mensajes descriptivos: es importante que el mensaje nos ofrezca información de calidad. De nada sirve mantener logs del tipo “pasó por aquí”.
  • Mensajes útiles: aunque no lo parezca, escribir logs no es gratis. A nivel de rendimiento y de almacenamiento conlleva un coste (no entraremos a discutir si éste es más o menos elevado). Es por ello que deberemos elegir bien cuándo la información es útil y cuándo merece la pena imprimirla. Por ejemplo, no es muy recomendable imprimir logs en cada condición if-else sólo para seguir el flujo en una función. Si es realmente importante quizás sólo sea necesario un mensaje más descriptivo que lo resuma.
  • Buen uso de los niveles de log: no siempre necesitamos revisar el mismo grado de detalle en los logs. Por eso es importante categorizarlos según el nivel de TRACE, DEBUG, INFO, WARN, ERROR o FATAL. Así podremos cambiar el nivel según sea necesario y evitar inundarnos de mensajes que sólo sirvan para ofuscar nuestro proceso de análisis.
  • Cuida la información sensible: evita imprimir información sensible como datos personales, contraseñas, etc. No todo el mundo (ni siquiera aquellos que revisarán los logs) puede tener acceso a esta información. En tal caso procura anonimizarla.
  • Usa un patrón: define una estructura de mensajes con un formato común. Esto te aportará claridad en los análisis.

A continuación vamos a proponer un caso de estudio para la monitorización de logs. Aunque el ejemplo se presenta sobre microservicios dentro de un cluster de Kubernetes, la solución propuesta no es exclusiva para este tipo de arquitecturas. Puedes beneficiarte igualmente aunque tu entorno esté basado en monolitos y/o servicios.

archBefore-1

Como puede verse en la imagen, nuestro sistema estará compuesto por dos tipos de microservicios: api-front y backend, comunicados entre sí vía REST y ubicados en el namespace ns1. Para cada uno desplegaremos un servicio y dos réplicas. Finalmente lanzaremos una serie de peticiones desde un cliente para comprobar su comportamiento.

Imaginemos que alguno de nuestros microservicios tuviese un error. Para poder analizarlo correctamentedeberíamos revisar los logs de cada componente:

4-1

(Sí, lo reconozco. Me encanta la consola con apariencia de Matrix.)

Como ya habréis imaginado, ahora deberíamos identificar fechas, relacionar peticiones y hacernos un mapa del flujo hasta llegar al error detectado.

¿Y si ahora te dijera que en lugar de dos tipos de pods se desplegarán 20, 50 ó 100?; ¿y si en lugar de dos réplicas por cada pod fuesen 10?. Resultaría mucho más complejo, ¿verdad?

**¿Qué es EFK?

Para bien o para mal, no existe una llave ‘maestra’ que resuelva todo el problema de raíz. La solución que proponemos estará compuesta por un conjunto de herramientas que nos aportarán flexibilidad y diferentes características según necesitemos.

Llamamos EFK al conjunto de herramientas compuesto por ElasticSearch, Fluentd y Kibana que nos ayudará a interpretar de manera conjunta todos los logs generados por los elementos de nuestro sistema.

Cada uno de ellos cumple con una función concreta:

ElasticSearch: almacena los logs resultantes para realizar búsquedas más eficientes.

Fluentd: recolecta, trata y envía los logs desde las diferentes fuentes de información que se configuren.

Kibana: es una interfaz gráfica desde donde poder visualizar e interpretar los datos, crear gráficos, etc.

5

Requisitos

Instalación

ElasticSearch

ElasticSearch provee una base de datos no relacional y un motor de búsqueda donde se almacenarán los logs que vamos a recolectar. Como verás más adelante con los ejemplos, estos logs se organizan en índices o colecciones de documentos con características similares. Para aquellos que no estén familiarizados con este tipo de estructura, podríamos decir que los índices se ‘asemejan’ a las tablas de las base de datos relacionales.

Si quieres saber más, puedes encontrar información detallada en la página oficial de elastic.

Ahora bien, vamos a lo práctico. A continuación mostraremos cómo desplegar un servidor de ElasticSearch en nuestro cluster de Kubernetes.

elastic.yaml

apiVersion: apps/v1

kind: StatefulSet

metadata:

name: elasticsearch

namespace: monitoring

spec:

serviceName: elasticsearch-svc

replicas: 1

selector:

matchLabels:

app: elasticsearch

template:

metadata:

labels:

app: elasticsearch

spec:

containers:

- name: elasticsearch

image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1

env:

- name: discovery.type

value: single-node

ports:

- containerPort: 9200

name: http

protocol: TCP

resources:

limits:

cpu: 512m

memory: 2Gi

requests:

cpu: 512m

memory: 2Gi

---

kind: Service<br></br>
metadata:<br></br>
  name: elasticsearch-svc<br></br>
  namespace: monitoring<br></br>
  labels:<br></br>
    service: elasticsearch-svc<br></br>
spec:<br></br>
  type: NodePort<br></br>
  selector:<br></br>
    app: elasticsearch<br></br>
  ports:<br></br>
  - port: 9200<br></br>
    targetPort: 9200<br></br></code></p>

Podremos aplicar los cambios ejecutando el siguiente comando:

kubectl apply -f elastic.yaml

Vamos a comentar algunas de sus características:

  • Previamente hemos creado un nuevo namespace llamado monitoring donde alojaremos los componentes que serán necesarios para la monitorización.
  • Expondremos este componente a través de un servicio. Es importante recordar el nombre (elasticsearch-svc) y el puerto (9200) del servicio dado que vamos a tener que acceder a él desde otros componentes.
  • Por último, es muy importante tener en cuenta los recursos que le asignaremos. Quizá éste sea el elemento que más requiera de ellos. Para esta prueba será más que suficiente con 2Gb de RAM y 512m de CPU.

Perfecto, ya tenemos una base de datos. ¿Ahora quién se encarga de alimentarla?

Fluentd

Se trata del agente encargado de recolectar información, en nuestro caso de los logs de Kubernetes, transformar y enviarla a ElasticSearch. Decidí trabajar con Fluentd ya que ofrece muy buen comportamiento con Kubernetes, bajo footprint, fácil configuración y gran cantidad de plugins. Sin embargo existen otras alternativas también muy utilizadas como Logstash (formando el conocido ELK) o FileBeat.

Si quieres saber más sobre fluentd, puedes encontrar información detallada en su página oficial.

Para comenzar, si queremos acceder a la información de Kubernetes, será necesario generar unas credenciales adecuadas mediante su correspondiente rbac (Role-based Access Control):

fluentd-rbac.yaml
 
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd-sa
  namespace: kube-system
 
---
 
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: fluentd-cr
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch
 
---
 
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: fluentd-crb
roleRef:
  kind: ClusterRole
  name: fluentd-cr
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: fluentd-sa
  namespace: kube-system

Una forma de asegurarnos de que siempre se mantenga ejecutándose en alguno de los nodos del cluster es desplegarlo como un DaemonSet.


fluentd-daemonset.yaml
 
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        version: v1
        kubernetes.io/cluster-service: "true"
        app: fluentd
    spec:
      serviceAccount: fluentd-sa
      serviceAccountName: fluentd-sa
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1.12-debian-elasticsearch7-1
        env:
          - name:  FLUENT_ELASTICSEARCH_HOST
            value: "elasticsearch-svc.monitoring"
          - name:  FLUENT_ELASTICSEARCH_PORT
            value: "9200"
          - name: FLUENT_ELASTICSEARCH_SCHEME
            value: "http"
          - name: FLUENT_UID
            value: "0"
        resources:
          limits:
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
       - name: config-volume
          mountPath: /fluentd/etc/kubernetes.conf
          subPath: kubernetes.conf
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: config-volume
        configMap:
        name: fluentd-config

Podremos aplicar los cambios con los siguientes comandos:

<p>kubectl apply -f fluentd-rbac.yaml</p>
<p>kubectl apply -f fluentd-daemonset.yaml</p>

Tal y como hicimos en el apartado anterior, vamos a comentar algunas de sus características:

  • A diferencia del resto de componentes del EFK que alojaremos en el namespace monitoring, Fluentd estará alojado en el namespace de Kubernetes (kube-system).
  • Es importante indicar el host [FLUENT_ELASTICSEARCH_HOST] y el puerto del servicio [FLUENT_ELASTICSEARCH_PORT] que definimos para ElasticSearch.
  • La ruta de donde extraer los logs está indicada mediante /var/log.
  • Hemos creado un fichero en el que describiremos las reglas de filtrado de datos y vamos a acceder a ellas a través del ConfigMap config-volume. Explicaremos esto más adelante.

Una vez aplicado el yaml, podremos comprobar que conectó correctamente con ElasticSearch accediendo a sus logs y revisando que se mostró el siguiente mensaje:

kubectl logs fluentd-xxxx -n kube-system

Connection opened to Elasticsearch cluster =>

  {:host=>"elasticsearch.logging", :port=>9200, :scheme=>"http"}

Cómo filtrar los datos:

Por el momento lo único que hemos hecho es encargar a Fluentd que recoja los logs de cada contenedor y los guarde en ElasticSearch. Este comportamiento es perfectamente válido pero poco útil.

Imaginemos Fluentd como una factoría de madera. Si aplicamos la analogía a lo explicado en el párrafo anterior, tendríamos una entrada de mercancía (troncos de árboles) y una salida exactamente igual. ¡Pues vaya factoría! Sin embargo, en la vida real no pasa esto. Los troncos son examinados para detectar el tipo de madera que se quiere trabajar, se cortan en listones e incluso se marcan para conocer su procedencia. Pues todas estas tareas pueden aplicarse en Fluentd. Vamos a ver nuestro caso:


kubernetes.conf : |-
    # This configuration file for Fluentd is used
    # to watch changes to Docker log files that live in the
    # directory /var/lib/docker/containers/ and are symbolically
    # linked to from the /var/log/containers directory using names that capture the
    # pod name and container name.
    
    # we select only the logs from the containers of the namespace 'ns1'
    
      @type tail
      @id in_tail_container_logs
      path /var/log/containers/*ns1*.log
      pos_file /var/log/containers.log.pos
      read_from_head true
      tag kubernetes.*
      
        @type json
        time_format "%Y-%m-%dT%H:%M:%S.%NZ"
        time_type string
      
    
    
    # we split all fields
    
      @type parser
      key_name log
      
        @type regexp
        expression ^(?<time>.+) (?\w*) (?\d+) --- \[(?[^\]]*)\] (?[^\:]*)\:(?[^\|]*)\| requestUuid=(?.+)$
        time_format "%Y-%m-%d %H:%M:%S.%N"
      </time>
    
    
    # we use kubernetes metadata plugin to add metadatas to the log
    
      @type kubernetes_metadata
    
    
    # we discard fluentd logs
    
      @type null
    

En primer lugar hemos definido una entrada de datos <source> en la que indicaremos la procedencia de los logs que vamos a querer tratar. En nuestro caso sólo nos interesan aquellos que provengan del namespace ns1. A estos ficheros vamos a etiquetarlos con el prefijo ’kubernetes.’ para poder seleccionarlos en los posteriores pasos.

A continuación vamos a extraer sus campos mediante una expresión regular. De este modo obtendremos los valores de time, severity, pid, proc, method, message y requestUuid y en un futuro podremos realizar búsquedas, ordenaciones, etc. a partir de estos. Es importante conocer previamente el formato de nuestros logs para adaptar la expresión regular. Si tienes problemas con tu expresión regular al final del post te facilitaré una herramienta online de mucha ayuda.

Por último, incorporamos metadatos de kubernetes a cada registro, de modo que podamos conocer su procedencia: nombre del namespace, del pod, del contenedor, etc.

Una vez terminados estos pasos, ahora sí, los nuevos datos serán enviados a ElasticSearch.

Genial, ya tenemos también los datos. Pero, ¿cómo podemos trabajar con ellos?

Kibana

Es una interfaz open-source, perteneciente a Elastic, que nos permite visualizar y explorar datos que se encuentran indexados en ElasticSearch. Como comprobarás más adelante, es bastante sencilla e intuitiva de utilizar y no es necesario tener un perfil técnico para usarla. Por tanto puede ser un complemento bastante útil para descargar a los desarrolladores de un soporte de nivel 1.

Si quieres saber más, puedes encontrar información detallada en la página oficial de kibana.

A continuación mostraremos cómo desplegar Kibana en nuestro cluster de Kubernetes:


kibana.yaml
 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: kibana
  template:
    metadata:
      labels:
        app: kibana
    spec:
      containers:
      - name: kibana
        image: docker.elastic.co/kibana/kibana:7.12.1
        env:
        - name: ELASTICSEARCH_HOSTS
          value: http://elasticsearch-svc:9200
        - name: XPACK_SECURITY_ENABLED
          value: "true"
        ports:
        - containerPort: 5601
          name: http
          protocol: TCP
 
---
 
apiVersion: v1
kind: Service
metadata:
  name: kibana-svc
  namespace: monitoring
  labels:
    service: kibana-svc
spec:
  type: NodePort
  selector:
    app: kibana
  ports:
  - port: 5601
    targetPort: 5601

Podremos aplicar los cambios con el siguiente comando:


kubectl apply -f kibana.yaml


Algunas de sus características:

  • Asignamos Kibana al namespace monitoring comentado anteriormente.
  • Es importante indicar la url [ELASTICSEARCH_HOSTS] de acceso a ElasticSearch.
  • Definimos el puerto del servicio desde donde podremos acceder a la interfaz de Kibana (5601).

Una vez creados todos los componentes podremos acceder a Kibana realizando un port-forward de su servicio y accediendo desde un navegador a la dirección http://localhost:5601.

kubectl port-forward svc/kibana-svc --namespace=monitoring 5601:5601

Configuración

Terminada toda la instalación estarás deseando ver cómo sacarle partido. Pues ahora viene lo bueno.

En primer lugar será necesario lanzar algo de tráfico para que en ElasticSearch se generen los índices a los que vamos a acceder. A continuación, deberemos configurar éstos desde Kibana:

kibana_init01

Por defecto los logs de cada día se guardan en un nuevo índice de ElasticSearch con el sufijo de la fecha ‘logstash-[fecha]’. Por lo tanto configuraremos un patrón para que tome los índices independientemente de ese sufijo.

También será importante marcar la opción ‘Include system and hidden indices’, ya que nos permitirá utilizar todos los campos que hemos extraído desde Fluentd.

kibana_init11

Finalmente seleccionaremos el filtro de tiempo por defecto ‘@timestamp’.

Búsquedas y paneles de control

Si accedemos al apartado “Discovery” de Kibana podremos ver la siguiente pantalla:

kibana_discover-1

En ella tenemos acceso a los logs de todos nuestros microservicios. También se muestra un gráfico de barras con el volumen de logs recibidos por intervalos de tiempo. Finalmente, en la zona superior, tenemos un buscador desde el que realizar consultas a partir de los campos que nos interesen y seleccionar el intervalo de tiempo de la búsqueda.

Sin embargo, toda esta información aún se ve muy confusa. Vamos a crear una búsqueda en la que mostraremos sólo ciertos datos de cada registro. Por ejemplo, además de la fecha incorporaremos el nombre del pod, el uuid de la petición, el nivel de log y el mensaje. Para ello sólo será necesario pulsar en el icono que aparece al lado de cada uno de ellos:

kibana-addColumn -1

kibana_traffic_overview1

Una vez hecho esto podremos filtrar por los campos visibles de modo que podamos localizar la información que necesitemos. En este caso, si seleccionamos el uuid de una petición que tenga algún error , podremos identificar el flujo que ha tenido y analizar con más precisión la causa del problema.

kibana-filter - 1

error - 1

Otras funcionalidades que ofrece Kibana son los dashboards. Con ellos podrás reunir una serie de gráficos que te ayuden a conocer el estado de tu sistema.

kibana_dashboard1

La interfaz es bastante intuitiva en la edición y creación de los distintos paneles y puedes generar todo tipo de gráficos, histogramas, contadores, etc. Si prefieres importar los que he ido creando, en el repositorio dejaré el fichero .ndjson con la búsqueda y el dashboard y podrás cargarla en tu entorno.

Conclusiones

¿Realmente merece la pena invertir en estas tecnologías?: ¿compensa la relación coste-beneficio?

Las herramientas de monitorización son, desde mi punto de vista, algo fundamental en todos los sistemas. Es cierto que su implantación requiere un esfuerzo inicial para ampliar la arquitectura del sistema (en el siguiente gráfico veremos cómo queda) así como sus recursos. Sin embargo,las ventajas que ofrecen a largo plazo merecen mucho la pena:

  • Control sobre lo que está pasando en tiempo real
  • Agilidad en los procesos de detección de errores
  • Prevención de fallos mucho mayores
  • Mejora de la eficiencia de los sistemas
  • Reducción de costes
  • Liberación de los desarrolladores en cuanto a soporte de nivel 1

A lo largo de mi carrera profesional he trabajado en diferentes proyectos y reconozco que hay una diferencia considerable entre realizar un troubleshooting de forma manual a hacerlo con ayuda de estas herramientas.

12

Para finalizar sólo me gustaría recordar algunos de los puntos importantes que hemos comentado al trabajar con logs:

  • Define un patrón claro de lo que imprimes en tus logs para facilitar su tratamiento.
  • Cuida la cantidad y la calidad de la información que imprimes. Define en qué momento es necesario aplicar los diferentes niveles de log y activa aquel que mantenga un equilibrio entre restrictivo y útil.
  • Define una política de rotación de logs, cuotas, etc. de modo que el almacenamiento de estos no perjudique el rendimiento del sistema.
  • Y por supuesto, evita introducir información sensible en los logs de tus aplicaciones.

«Lo que no se define no se puede medir. Lo que no se mide, no se puede mejorar. Lo que no se mejora, se degrada siempre».

William Thomson Kelvin

En el siguiente post continuaremos hablando sobre otro de los pilares de la observabilidad: las métricas.

Recursos

Pedro Miguel Rodríguez, Ingeniero de Software Senior en SNGULAR

Pedro Miguel Rodríguez

Ingeniero de Software Senior en SNGULAR