Prometheus, la tour de guet d'une infrastructure swarm
Contexte
Si vous avez lu mon précédant 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. La ou 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, à 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 son 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ée par une vaste communauté !
- Du point précédant en découle un nombre d’intégrations presque infini : les exporters, on verra ce que c'est, les libs clientes pour nos confrères développeurs et les intégrations à proprement parler!
- La capacité à faire du “service discovery” comprendre par la : 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 qu'on affectionne…
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 ou 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:valeur qui vont pouvoir être utilisés 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 deployer 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 config à créer pour 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 config :
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 deploie 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 trace une métrique par exemple le temps cpu :
On se retrouve 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 oeil 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 on ajoute pas un peu de target… Pour se faire on va utiliser plusieurs outils, le premier c'est la fonction d'export metriques du daemon docker lui même ! Pour cela on va éditer le fichier de config 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 conf 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 ou aller chercher les métriques, et c'est la qu'on va 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 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 config 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 l'on retourne voir les target sur les webui de prometheus on retrouve bien tout 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 accès aux quantités de ressources utilisées, cpu, ram, network pour chaque containers. 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 ou l'on a forcement 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 ou 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 où 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 vu 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
La on se retrouve avec les métriques de tout le monde, on va ajouter un filtre par label avec {label=“valeur”}
Prometheus va automatiquement vous proposez 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 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'aimerai 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 ou la série est un compteur donc une droite qui ne fait que monter, on va donc appliquer la fonction rate comme plus haut dans l'article et on va aussi multiplier le résultat par 100 avec “ * 100” pour avoir un pourcentage :
Et 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 demontré une partie des possibilité de prometheus qui sont assez vastes finalement. Je pourrai 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 !