Prometheus, la tour de guet d’une infrastructure Swarm

Prometheus, la tour de guet d’une infrastructure Swarm

Si vous avez lu mon précédent article sur traefik, vous connaissez déjà les problématiques de “volatilité” associées aux workload docker. Rappelons brièvement qu’à Geco-iT nous avons en gestion des clusters docker swarm, et donc des containers qui peuvent se trouver sur plusieurs nœuds différents, apparaître et disparaître, bref des infrastructures dynamiques. Cela apporte son lot d’avantages, mais aussi d’inconvénients. Traefik nous solutionne la question du routage des flux jusqu’à ces services et de leur sécurisation via le SSL/TLS avec des certificats let’s encrypt. Là où traefik ne nous aide pas, c’est dans la manière de superviser et d’obtenir des métriques sur ces services. Aujourd’hui, nous utilisons un système de supervision fonctionnel, mais sous un modèle un peu vieillissant : celui de nagios. Ce dernier nous contraint, à chaque ajout de serveur chez nous ou chez un client, de déclarer à la main un host et ses services. Une fois la configuration écrite, elle est chargée dans l’outil et restera fixe jusqu’au prochain update / reload. Pas pratique du tout dans un environnement qui peut se transformer comme avec swarm !

La réponse se trouve dans une technologie alien : prometheus

Geco-iT a porté son choix, comme beaucoup d’autres d’ailleurs, sur prometheus. Les raisons de ce choix sont multiples :

  • Le projet est full open-source, c’est un critère obligatoire chez nous;
  • Le projet est issu de soundcloud, qui en plus d’être un très bon service de streaming musical est forcément une entité qui sait produire des solutions fiables pour manager des infrastructures à très fortes charge (comme netflix par exemple);
  • Le projet est gradué CNCF, ce fut le deuxième à avoir cet “honneur” juste après un certain kubernetes, vous savez l’orchestrator qui a littéralement écrasé le “marché” !
  • Il est adopté par une vaste communauté !
  • Du point précédent en découle un nombre d’intégrations presque infini : les exporters, les libs clientes pour nos confrères développeurs et les intégrations à proprement parler!
  • La capacité à faire du “service discovery”, comprendre par là : générer une partie de sa configuration tout seul comme un grand, et ça, c’est bien l’objet de notre problématique !
  • Il est écrit en GO et on a beau ne pas être développeurs à Geco-IT, il y a quand même des langages que nous affectionnons plus que d’autres… 

Architecture du produit

Prometheus est un système basé sur le “pull”, c’est-à-dire que c’est lui qui se connecte aux agents pour récupérer les métriques et non l’inverse, comme dans le cas d’un système push. Pour cela, il s’appuie sur 5 composants :

  • En premier lieu, le serveur, qui s’occupe de récupérer les métriques auprès des exporters, de les stocker sur disque ou ailleurs (on a une fonction remote write), et aussi d’exposer un endpoint HTTP pour permettre aux autres composants d’exécuter des requêtes PromQL pour accéder aux métriques. Le serveur se charge aussi de générer la configuration dynamique via le service discovery et de pousser les alertes à l’alertmanager;
  • En deuxième lieu, les exporters qui exposent les métriques en HTTP. Un exporter peut être soit un agent comme le node-exporter qui va collecter des métriques sur un OS et les exposer, ou directement une application qui expose ses propres métriques comme gitea ou grafana par exemple;
  • La webui qui permet de voir l’état de votre instance prometheus, mais aussi de requêter les métriques, souvent, on y préférera grafana qui apporte beaucoup de fonctionnalités en terme de visualisation;
  • L’alertmanager, qui va recevoir les alertes du serveur et les envoyer vers les services concernés pour vous faire parvenir des notifications sur les canaux de votre choix, teams, slack, mails , etc…
  • Et enfin la pushgateway qui est un composant un peu particulier puisqu’il permet de faire du push pour les éléments qui le nécessitent vraiment. Par exemple, vous avez un job qui tourne dans un container, admettons une tâche cron qui fait le ménage des utilisateurs non utilisés depuis plus de 3 ans par exemple. Ce job peut s’exécuter, renvoyer à la fin une métrique “number_of_deleted_users” et la pousser dans la gateway. Cette dernière va stocker le résultat pour que le serveur prometheus puisse venir le lire en pull après coup. Ça ne permet pas de transformer le modèle de prometheus de pull vers push, mais ça solutionne les rares cas comme celui que j’ai pris en exemple où l’on a besoin de faire du push ou de l’asynchrone.

Un autre concept important à saisir, c’est la notion de label! Dans prometheus chaque timeseries (une timeseries design la valeur d’une métrique sur un intervalle de temps) va se voir accrocher un ou plusieurs label(s) qui sont des groupes de clef : valeurs qui vont pouvoir être utilisées pour différencier les timeseries entre elles ! Plusieurs labels sont fournis d’office, vous allez pouvoir les remanier et en ajouter !

Big brother is watching my lab

Pour illustrer les possibilités de prometheus nous allons le déployer sur un cluster swarm :

❯ docker node ls
ID                            HOSTNAME          STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
d011tsg2fpujizh6og10wub7b     docker-manager2   Ready     Active         Leader           20.10.6
610fs14p3cnpuc0fv15pgdwkv     docker-manager3   Ready     Active         Reachable        20.10.6
jgn4jxmdrlpa0hamzlvq6rim5     docker-worker1    Ready     Active                          20.10.6
ikoj4h0j1nactm1pcqabwhsrq     docker-worker2    Ready     Active                          20.10.6
qvn7z1j4yiwt1pmppczcp7jml     docker-worker3    Ready     Active                          20.10.6
u1i03hhiq2e4h0pebzm2y41wb *   geco-swarm        Ready     Active         Reachable        20.10.6

Voici la configuration à créer afin de faire tourner notre premier prometheus :

global:
  scrape_interval:     15s #A quelle frequence on recupère les metriques
 
  # On attache ces labels à tout les timeseries, ca permet de les différencier si on communique avec un système exterieur.
  external_labels:
      monitor: 'geco-swarm'
 
#les scrape configs sont le nerf de la guerre, c'est tout ce qu'on va vouloir interoger, on va definir ici un endpoint statique qui est prometheus lui même pour qu'on puisse le surveiller lui aussi et le tester
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

Et enfin, la stack swarm qui va appeler notre configuration :

version: '3.3'
services:
  prometheus:
    image: prom/prometheus:latest
    command:
     - --config.file=/etc/prometheus/prometheus.yml #specification du fichier de config
     - --storage.tsdb.path=/prometheus_data         #le chemin ou l'on store les data
     - --storage.tsdb.retention.time=90d            #durée de retention des datas
    ports:
     - 9090:9090
    volumes:
     - prometheus-data:/prometheus_data
    networks:
     - metrics
    configs:
     -
      source: geco-prometheus
      target: /etc/prometheus/prometheus.yml # On remonte la config crée plus haut dans le container.
    deploy:
      placement:
        constraints:
         - node.role == manager # On demande explicitement à swarm un déploiement sur un nœud manager de cluster car sinon le socket docker ne sera pas accessible pour prometheus.

On déploie notre stack dans le swarm :

docker stack deploy -c prometheus.yml prometheus

Au lancement de la stack on va avoir accès à l’interface et pouvoir aller sur Status > Targets et ainsi voir notre seul point de collecte pour le moment qui est le prometheus lui-même :

En allant sur l’onglet Graph et en sélectionnant le mode graph, on peut tracer une métrique (par exemple le temps cpu) :

On se retrouve alors avec une droite qui ne fait que monter, car la métrique est de type compteur. Ce compteur étant incrémenté sans fin, il faut lui appliquer la fonction rate() qui porte bien son nom pour obtenir quelque chose de lisible par un œil humain.

De manière générale, il faut toujours faire attention à se poser la question du type de data que l’on manipule pour éviter certaines surprises…

En avant les exporter !

 

Le daemon docker

On a une instance qui marche, elle s’automonitore, c’est beau, mais ça nous avance pas à grand chose si nous n’ajoutons pas un peu de target… Pour ce faire, on va utiliser plusieurs outils, le premier, est la fonction d’export métriques du daemon docker lui-même ! Pour cela, on va éditer le fichier de configuration daemon.json dans /etc/docker :

{
  "metrics-addr" : "0.0.0.0:9323",
  "experimental" : true
} 

Ensuite on restart le daemon :

sudo service docker restart

L’opération est à faire sur chaque node donc vous pouvez utiliser un outil de gestion de confugation pour vous faciliter la vie. On peut ensuite vérifier que docker expose bien les métriques avec un simple navigateur, ça vous permet aussi de voir le format utilisé par prometheus pour lire les métriques, vous voyez comme il est simple d’implémenter sa propre solution vu qu’il suffit d’exposer du texte en HTTP :

Vient le moment d’indiquer à prometheus où aller chercher les métriques, et c’est là que nous allons pouvoir utiliser son mécanisme de service discovery pour qu’il aille chercher les IPs des nœuds de notre cluster swarm tout seul. Je ne le ferai pas car l’article risque déjà d’être long mais si je supprime ou je déploie un node de plus, prometheus va le détecter seul ! Voici le morceau de configuration en question :

  - job_name: 'docker'
    dockerswarm_sd_configs:
      - host: "tcp://socket-proxy_socket-proxy:2375"  # On specifie la methode de connexion au socket docker, et le mode "nodes".
        role: nodes                                   # Le mode permet de choisir le type d'objet decouvert entre node, task et service.
    relabel_configs:
      # Fetch metrics on port 9323.
      - source_labels: [__meta_dockerswarm_node_address]   # Ici on indique que l'on veut remplacer l’adresse de collecte des métriques par les adresses des nodes docker
        target_label: __address__
        replacement: $1:9323
      # Set hostname as instance label
      - source_labels: [__meta_dockerswarm_node_hostname]  # et que l'on va remplir la valeur du label "instance" avec le hostname du node docker, pour les identifier facilement!
        target_label: instance

Si nous retournons voir les target sur les webui de prometheus, on retrouve bien tous nos nodes :

Cadvisor, le hibou aux grand yeux

Cadvisor, pour container advisor est un outil qui va collecter des métriques sur les containers lancés sur un node docker, on aura alors accès aux quantités de ressources utilisées, cpu, ram, network pour chaque container. Nous allons donc ajouter un service, de type global à notre swarm, le mode global va permettre de déployer une instance du service en question sur chaque node du cluster. C’est souvent le cas pour les outils de supervision ou de logging où l’on a forcément besoin d’un container par host pour pouvoir auditer l’infrastructure dans sa totalité.

  cadvisor:
    image: google/cadvisor:latest
    volumes:
     - /var/run/docker.sock:/var/run/docker.sock:ro    #Remonté de certaines volumes nécessaires à la collecte d'info de cadvisor
     - /:/rootfs:ro
     - /var/run:/var/run
     - /sys:/sys:ro
     - /var/lib/docker:/var/lib/docker:ro
    networks:
     - metrics
    deploy:
      mode: global
      labels:
        prometheus-job: cadvisor
        prometheus-job-port: 8080

Et mes nodes physiques dans tout ca ?

Avec les deux solutions précédentes, on possède les informations nécessaires sur les containers mais pas sur les hosts eux-mêmes, pour combler ce manque on va utiliser le node exporter de prometheus qui est en quelque sorte l’agent pour les OS de prometheus. Il va permettre d’exporter les métriques de nos hôtes linux. Comme il existe sous forme de container et que nous sommes dans un cluster swarm on va aussi le déployer en tant que service comme cadvisor. Si vous préférez utiliser un binaire standalone c’est possible aussi car il est fourni par prometheus.

  node-exporter:
    image: prom/node-exporter:latest
    command:
     - --path.sysfs=/host/sys     #Definition des chemin des points de montage ci-dessous
     - --path.procfs=/host/proc
     - --collector.filesystem.mount-points-exclude=^/(dev|host|proc|run/credentials/.+|sys|var/lib/docker/.+)($$|/)
    environment:
      NODE_ID: '{{.Node.ID}}'
    volumes:
     - /proc:/host/proc:ro        #Remonté de certaines volumes nécessaires à la collecte d'info comme pour cadvisor
     - /sys:/host/sys:ro
     - /:/rootfs:ro
     - /etc/hostname:/etc/nodename
    networks:
     - metrics
    deploy:
      mode: global
      labels:
        prometheus-job: node-exporter
        prometheus-job-port: 9100

Config de prometheus pour scrapper nos deux autres exporter fraichement déployés

La dernière chose qu’il nous reste à faire c’est de dire à prometheus où collecter les données de cadvisor et node-exporter, comme dans le cas du daemon docker on va pouvoir utiliser le service discovery mais on va pousser un peu plus la chose. Si vous reprenez les configs des services ci-dessus vous allez voir à la fin :

    deploy:
      mode: global
      labels:
        prometheus-job: cadvisor
        prometheus-job-port: 8080

et aussi :

    deploy:
      mode: global
      labels:
        prometheus-job: node-exporter
        prometheus-job-port: 9100

J’ajoute ici volontairement deux labels de mon choix pour les utiliser dans le mécanisme de service discovery. La configuration suivante va permettre plusieurs choses :

  • Dire à prometheus de se connecter à l’API docker comme précédemment mais en mode task ce coup-ci pour récupérer les services qui tournent;
  • Garder seulement les containers qui doivent être dans un état “running”, ça évite d’essayer de scrapper des services qu’on a potentiellement éteint volontairement et temporairement;
  • Garder seulement les containers qui ont un label “prometheus-job”, ça permet de choisir au déploiement d’un service si on doit le scrapper ou pas;
  • Garder seulement les containers qui sont dans le network “prometheus-metrics”, de cette manière je transite toutes les métriques dans ce réseau et je n’ai pas besoin d’exposer les ports de mes services qui restent bien au chaud derrière mon traeffik 😉
  • Sans rentrer dans le détail de la fin : remanier les labels pour pouvoir passer à prometheus l’URL ou récupérer les métriques. D’où le label “prometheus-job-port” que j’utilise pour spécifier le port d’écoute du service qui expose les métriques.
  - job_name: 'swarm'
    dockerswarm_sd_configs:
      - host: "tcp://socket-proxy_socket-proxy:2375"         #Methode de connexion au socket docker
        role: tasks
    relabel_configs:
      # On garde seulement les containers qui sont en status désiré "running"
      - source_labels: [__meta_dockerswarm_task_desired_state]
        regex: running
        action: keep
      # On garde seulement les containers qui possèdent un label "prometheus-job"
      - source_labels: [__meta_dockerswarm_service_label_prometheus_job]
        regex: .+
        action: keep
      # On garde seulement les containers attachés au network swarm "metrics"
      - source_labels: [__meta_dockerswarm_network_name]
        regex: prometheus_metrics
        action: keep
      # On remplace le label "job" par le label "prometheus-job" que l'on définie nous mêmes dans la stack
      - source_labels: [__meta_dockerswarm_service_label_prometheus_job]
        target_label: job
      # Et enfin ci-dessous la petite suite d'opérations qui permettent de récupérer l'URL de la target en compilant nos deux labels
      - source_labels: [__address__]
        target_label: real_target
        regex: "([^:]+):\\d+"
      - source_labels: [real_target,__meta_dockerswarm_service_label_prometheus_job_port]
        separator: ":"
        target_label: __address__
        replacement: $1
      - source_labels: [__meta_dockerswarm_node_hostname]
        target_label: hostname
        replacement: $1

Nos targets dans la webui doivent se peupler encore un peu plus :

On retrouvera à la fois les cadvisor et les node-exporter, respectivement sur les bons ports grâce au système de label, à l’avenir pour monitorer un service swarm de plus il suffira d’ajouter les deux labels vus précédemment. Vous avez la déjà un bel exemple de ce que l’on peut faire avec le service discovery de prometheus !

Construction d’une petite requête dans le web ui de prometheus

On va se donner pour but de construire une requête pour avoir le pourcentage d’utilisation cpu du node “geco-swarm” avec le détail (user, iowait, etc…). Pour cela on va commencer par chercher “node” dans le module de construction de graph et vous allez voir qu’il y a un minimum d’auto complétion et c’est très pratique pour construire ses graphs : 

Là on se retrouve avec les métriques de tout le monde, on va ajouter un filtre par label avec {label=“valeur”}

Prometheus ne va automatiquement vous proposer que les labels disponibles, hostname correspond bien à ce qu’on veut faire, garder les métriques d’un seul node : 

Il va aussi nous proposer les valeurs disponibles pour ce label dans lesquelles on va retrouver notre nœud “geco-swarm”

On obtient alors la requête suivante :

Et effectivement, après exécution on n’a plus que les graphiques du noeud “geco-swarm” : 

Maintenant ce qui nous embête, c’est la métrique “idle” car ce que j’aimerais faire c’est stacker toutes les séries les unes sur les autres pour avoir le total d’utilisation CPU. Donc j’ai besoin de toutes les séries SAUF idle. Nous pouvons utiliser les labels pour EXCLURE. Très simple, dans le cas précédent on a fait {label=“valeur”}, donc on inverse avec {label!=“valeur”}, notez bien le “!”

Si on exécute à nouveau le requête on ne voit plus la série “idle” : 

On retrouve la particularité d’avant où la série est un compteur donc une droite qui ne fait que monter, on va donc appliquer la fonction rate (vu précédemment dans l’article) et on va aussi multiplier le résultat par 100 avec “ * 100” pour avoir un pourcentage :

Enfin, en exécutant, puis en passant le mode de graphing à “stack” avec le petit bouton à coté de “show exemplars” on retrouve bien un graphique stacké de tout les temps cpu sauf idle de la machine geco-swarm :

Pour conclure

J’espère vous avoir démontré une partie des possibilités de prometheus qui sont assez vastes finalement. Je pourrais faire un autre article uniquement sur les requêtes possibles avec le language PromQL ou encore sur le système des alertes que propose prometheus. Pour aller plus loin il faudrait aussi reprendre la configuration pour garder seulement les données que l’on veut collecter dans une soucis de place et de performance. On peut aussi envisager de connecter le prometheus à un grafana pour faire des beaux dashboards de l’état de notre infra, voir à un système de monitoring externe comme checmk pour l’utiliser comme source de données supplémentaire. Tout ça pour dire que le sujet est à peine abordé et que l’on peut faire bien d’autre chose avec ce produit très complet !


Hugo Campion, ingénieur systèmes et réseaux.

Comments are closed.