La surveillance des pings entre les hôtes Kubernetes est notre recette



Lors du diagnostic de problèmes dans un cluster Kubernetes, nous remarquons souvent que parfois l'un des nœuds du cluster dégouline * et, bien sûr, cela est rare et étrange. Nous sommes donc arrivés à la nécessité d'un outil qui exécuterait un ping de chaque nœud à chaque nœud et présenterait les résultats de son travail sous la forme de métriques Prometheus . Nous n'aurions qu'à dessiner des graphiques dans Grafana et localiser rapidement le nœud défaillant (et, si nécessaire, en supprimer tous les pods, puis faire le travail correspondant **) ...

* Par "bruine", je comprends que le nœud peut passer en état NotReady et retourner soudainement au travail. Ou, par exemple, une partie du trafic des pods peut ne pas atteindre les pods sur les nœuds voisins.

** Pourquoi de telles situations surviennent-elles? L'une des causes courantes peut être des problèmes de réseau sur le commutateur dans le centre de données. Par exemple, une fois dans Hetzner, nous avons configuré vswitch, mais à un moment merveilleux, l'un des nœuds a cessé d'être accessible sur ce port vswitch: à cause de cela, il s'est avéré que le nœud était complètement inaccessible sur le réseau local.

De plus, nous aimerions lancer un tel service directement dans Kubernetes , afin que l'ensemble du déploiement se fasse à l'aide de l'installation du Helm-chart. (Anticipation des questions - si vous utilisez le même Ansible, nous aurions à écrire des rôles pour différents environnements: AWS, GCE, bare metal ...) Ayant un peu cherché sur Internet des outils prêts à l'emploi pour la tâche, nous n'avons rien trouvé de convenable. Par conséquent, ils ont fait le leur.

Script et configs


Ainsi, le composant principal de notre solution est un script qui surveille le changement dans tous les nœuds du champ .status.addresses et, si un nœud a changé ce champ (c'est-à-dire qu'un nouveau nœud a été ajouté), envoie des valeurs Helm au graphique à l'aide cette liste de nœuds sous forme de ConfigMap:

 --- apiVersion: v1 kind: ConfigMap metadata: name: node-ping-config namespace: kube-prometheus data: nodes.json: > {{ .Values.nodePing.nodes | toJson }} 

Script Python lui-même:
 #!/usr/bin/env python3 import subprocess import prometheus_client import re import statistics import os import json import glob import better_exchook import datetime better_exchook.install() FPING_CMDLINE = "/usr/sbin/fping -p 1000 -A -C 30 -B 1 -q -r 1".split(" ") FPING_REGEX = re.compile(r"^(\S*)\s*: (.*)$", re.MULTILINE) CONFIG_PATH = "/config/nodes.json" registry = prometheus_client.CollectorRegistry() prometheus_exceptions_counter = \ prometheus_client.Counter('kube_node_ping_exceptions', 'Total number of exceptions', [], registry=registry) prom_metrics = {"sent": prometheus_client.Counter('kube_node_ping_packets_sent_total', 'ICMP packets sent', ['destination_node', 'destination_node_ip_address'], registry=registry), "received": prometheus_client.Counter( 'kube_node_ping_packets_received_total', 'ICMP packets received', ['destination_node', 'destination_node_ip_address'], registry=registry), "rtt": prometheus_client.Counter( 'kube_node_ping_rtt_milliseconds_total', 'round-trip time', ['destination_node', 'destination_node_ip_address'], registry=registry), "min": prometheus_client.Gauge('kube_node_ping_rtt_min', 'minimum round-trip time', ['destination_node', 'destination_node_ip_address'], registry=registry), "max": prometheus_client.Gauge('kube_node_ping_rtt_max', 'maximum round-trip time', ['destination_node', 'destination_node_ip_address'], registry=registry), "mdev": prometheus_client.Gauge('kube_node_ping_rtt_mdev', 'mean deviation of round-trip times', ['destination_node', 'destination_node_ip_address'], registry=registry)} def validate_envs(): envs = {"MY_NODE_NAME": os.getenv("MY_NODE_NAME"), "PROMETHEUS_TEXTFILE_DIR": os.getenv("PROMETHEUS_TEXTFILE_DIR"), "PROMETHEUS_TEXTFILE_PREFIX": os.getenv("PROMETHEUS_TEXTFILE_PREFIX")} for k, v in envs.items(): if not v: raise ValueError("{} environment variable is empty".format(k)) return envs @prometheus_exceptions_counter.count_exceptions() def compute_results(results): computed = {} matches = FPING_REGEX.finditer(results) for match in matches: ip = match.group(1) ping_results = match.group(2) if "duplicate" in ping_results: continue splitted = ping_results.split(" ") if len(splitted) != 30: raise ValueError("ping returned wrong number of results: \"{}\"".format(splitted)) positive_results = [float(x) for x in splitted if x != "-"] if len(positive_results) > 0: computed[ip] = {"sent": 30, "received": len(positive_results), "rtt": sum(positive_results), "max": max(positive_results), "min": min(positive_results), "mdev": statistics.pstdev(positive_results)} else: computed[ip] = {"sent": 30, "received": len(positive_results), "rtt": 0, "max": 0, "min": 0, "mdev": 0} if not len(computed): raise ValueError("regex match\"{}\" found nothing in fping output \"{}\"".format(FPING_REGEX, results)) return computed @prometheus_exceptions_counter.count_exceptions() def call_fping(ips): cmdline = FPING_CMDLINE + ips process = subprocess.run(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) if process.returncode == 3: raise ValueError("invalid arguments: {}".format(cmdline)) if process.returncode == 4: raise OSError("fping reported syscall error: {}".format(process.stderr)) return process.stdout envs = validate_envs() files = glob.glob(envs["PROMETHEUS_TEXTFILE_DIR"] + "*") for f in files: os.remove(f) labeled_prom_metrics = [] while True: with open("/config/nodes.json", "r") as f: config = json.loads(f.read()) if labeled_prom_metrics: for node in config: if (node["name"], node["ipAddress"]) not in [(metric["node_name"], metric["ip"]) for metric in labeled_prom_metrics]: for k, v in prom_metrics.items(): v.remove(node["name"], node["ipAddress"]) labeled_prom_metrics = [] for node in config: metrics = {"node_name": node["name"], "ip": node["ipAddress"], "prom_metrics": {}} for k, v in prom_metrics.items(): metrics["prom_metrics"][k] = v.labels(node["name"], node["ipAddress"]) labeled_prom_metrics.append(metrics) out = call_fping([prom_metric["ip"] for prom_metric in labeled_prom_metrics]) computed = compute_results(out) for dimension in labeled_prom_metrics: result = computed[dimension["ip"]] dimension["prom_metrics"]["sent"].inc(computed[dimension["ip"]]["sent"]) dimension["prom_metrics"]["received"].inc(computed[dimension["ip"]]["received"]) dimension["prom_metrics"]["rtt"].inc(computed[dimension["ip"]]["rtt"]) dimension["prom_metrics"]["min"].set(computed[dimension["ip"]]["min"]) dimension["prom_metrics"]["max"].set(computed[dimension["ip"]]["max"]) dimension["prom_metrics"]["mdev"].set(computed[dimension["ip"]]["mdev"]) prometheus_client.write_to_textfile( envs["PROMETHEUS_TEXTFILE_DIR"] + envs["PROMETHEUS_TEXTFILE_PREFIX"] + envs["MY_NODE_NAME"] + ".prom", registry) 

Il s'exécute sur chaque nœud et envoie des paquets ICMP à toutes les autres instances de cluster Kubernetes 2 fois par seconde, et les résultats sont écrits dans les fichiers texte.

Le script est inclus dans l' image Docker :

 FROM python:3.6-alpine3.8 COPY rootfs / WORKDIR /app RUN pip3 install --upgrade pip && pip3 install -r requirements.txt && apk add --no-cache fping ENTRYPOINT ["python3", "/app/node-ping.py"] 

De plus, un ServiceAccount a été créé et un rôle pour celui-ci, qui ne permet de recevoir qu'une liste de nœuds (afin de connaître leurs adresses):

 --- apiVersion: v1 kind: ServiceAccount metadata: name: node-ping namespace: kube-prometheus --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: kube-prometheus:node-ping rules: - apiGroups: [""] resources: ["nodes"] verbs: ["list"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: kube-prometheus:kube-node-ping subjects: - kind: ServiceAccount name: node-ping namespace: kube-prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-prometheus:node-ping 

Enfin, vous avez besoin de DaemonSet , qui s'exécute sur toutes les instances du cluster:

 --- apiVersion: extensions/v1beta1 kind: DaemonSet metadata: name: node-ping namespace: kube-prometheus labels: tier: monitoring app: node-ping version: v1 spec: updateStrategy: type: RollingUpdate template: metadata: labels: name: node-ping spec: terminationGracePeriodSeconds: 0 tolerations: - operator: "Exists" serviceAccountName: node-ping priorityClassName: cluster-low containers: - resources: requests: cpu: 0.10 image: private-registry.flant.com/node-ping/node-ping-exporter:v1 imagePullPolicy: Always name: node-ping env: - name: MY_NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: PROMETHEUS_TEXTFILE_DIR value: /node-exporter-textfile/ - name: PROMETHEUS_TEXTFILE_PREFIX value: node-ping_ volumeMounts: - name: textfile mountPath: /node-exporter-textfile - name: config mountPath: /config volumes: - name: textfile hostPath: path: /var/run/node-exporter-textfile - name: config configMap: name: node-ping-config imagePullSecrets: - name: antiopa-registry 

Résumé des coups de mots:

  • Résultats du script Python - c.-à-d. les fichiers texte placés sur la machine hôte dans le répertoire /var/run/node-exporter-textfile exporter- textfile entrent dans l'exportateur de nœuds DaemonSet. Les arguments pour l'exécuter indiquent --collector.textfile.directory /host/textfile , où /host/textfile est le /host/textfile accès /host/textfile sur /var/run/node-exporter-textfile . (Vous pouvez en savoir plus sur le collecteur de fichiers texte dans l'exportateur de nœuds ici .)
  • En conséquence, le nœud-exportateur lit ces fichiers et Prometheus collecte toutes les données du nœud-exportateur.

Que s'est-il passé?


Maintenant - au résultat tant attendu. Lorsque de telles métriques ont été créées, nous pouvons les regarder et, bien sûr, dessiner des graphiques visuels. Voilà à quoi ça ressemble.

Tout d'abord, il existe un bloc général avec la possibilité (à l'aide du sélecteur) de sélectionner une liste de nœuds à partir desquels le ping est effectué et sur lesquels. Voici le tableau récapitulatif du ping entre les nœuds sélectionnés pour la période spécifiée dans le tableau de bord Grafana:



Et voici les graphiques avec des informations générales sur les nœuds sélectionnés :



Nous avons également une liste de lignes, dont chacune est un graphique pour un nœud distinct du sélecteur de nœud source :



Si vous développez une telle ligne, vous pouvez voir des informations sur les pings d'un nœud spécifique à tous les autres qui ont été sélectionnés dans le sélecteur de nœuds de destination :



Ces informations sont dans des graphiques:



Enfin, à quoi ressembleront les graphiques chéris avec un mauvais ping entre les nœuds?





Si vous observez cela dans un environnement réel - il est temps de comprendre les raisons.

PS


Lisez aussi dans notre blog:

Source: https://habr.com/ru/post/fr442798/


All Articles