Das Überwachen von Pings zwischen Kubernetes-Hosts ist unser Rezept



Bei der Diagnose von Problemen in einem Kubernetes-Cluster stellen wir häufig fest, dass manchmal einer der Clusterknoten nieselt *, und dies ist natürlich selten und seltsam. Wir kamen zu der Notwendigkeit eines Tools, das von jedem Knoten zu jedem Knoten pingt und die Ergebnisse seiner Arbeit in Form von Prometheus-Metriken präsentiert . Wir müssten nur Diagramme in Grafana zeichnen und den ausgefallenen Knoten schnell lokalisieren (und ggf. alle Pods daraus entfernen und dann die entsprechende Arbeit erledigen **) ...

* Unter "Nieselregen" verstehe ich, dass der Knoten in den NotReady Status NotReady und plötzlich zur Arbeit zurückkehren kann. Beispielsweise kann ein Teil des Verkehrs in Pods die Pods auf benachbarten Knoten nicht erreichen.

** Warum entstehen solche Situationen? Eine der häufigsten Ursachen können Netzwerkprobleme auf dem Switch im Rechenzentrum sein. Zum Beispiel haben wir in Hetzner vswitch konfiguriert, aber zu einem wunderbaren Zeitpunkt war einer der Knoten an diesem vswitch-Port nicht mehr zugänglich: Aus diesem Grund stellte sich heraus, dass der Knoten im lokalen Netzwerk vollständig unzugänglich war.

Darüber hinaus möchten wir einen solchen Dienst direkt in Kubernetes starten , damit die gesamte Bereitstellung über die Installation des Helm-Charts erfolgt. (Vorwegnahme von Fragen - Wenn wir denselben Ansible verwenden, müssten wir Rollen für verschiedene Umgebungen schreiben: AWS, GCE, Bare Metal ...) Nachdem wir ein bisschen im Internet nach vorgefertigten Tools für die Aufgabe gesucht hatten, fanden wir nichts Passendes. Deshalb haben sie ihre eigenen gemacht.

Skript und Konfigurationen


Die Hauptkomponente unserer Lösung ist also ein Skript , das die Änderung in allen Knoten des .status.addresses überwacht und, wenn ein Knoten dieses Feld geändert hat (d. H. Ein neuer Knoten hinzugefügt wurde), Helmwerte mit an das Diagramm sendet Diese Liste von Knoten in Form von ConfigMap:

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

Python-Skript selbst:
 #!/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) 

Es wird auf jedem Knoten ausgeführt und sendet zweimal pro Sekunde ICMP-Pakete an alle anderen Kubernetes-Clusterinstanzen. Die Ergebnisse werden in die Textdateien geschrieben.

Das Skript ist im Docker-Image enthalten :

 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"] 

Außerdem wurde ein ServiceAccount und eine Rolle dafür erstellt, mit der nur eine Liste von Knoten empfangen werden kann (um deren Adressen zu kennen):

 --- 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 

Schließlich benötigen Sie DaemonSet , das auf allen Instanzen des Clusters ausgeführt wird:

 --- 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 

Zusammenfassende Striche in Worten:

  • Python-Skriptergebnisse - d.h. Textdateien, die auf dem Hostcomputer im Verzeichnis /var/run/node-exporter-textfile exporter /var/run/node-exporter-textfile textfile abgelegt werden, werden in den DaemonSet-Knotenexporter verschoben. Die Argumente zum Ausführen geben --collector.textfile.directory /host/textfile , wobei /host/textfile der hostPath in /var/run/node-exporter-textfile . (Informationen zum Textdateisammler im Node-Exporter finden Sie hier .)
  • Infolgedessen liest der Node-Exporter diese Dateien und Prometheus sammelt alle Daten vom Node-Exporter.

Was ist passiert?


Nun zum lang erwarteten Ergebnis. Wenn solche Metriken erstellt wurden, können wir sie betrachten und natürlich visuelle Grafiken zeichnen. So sieht es aus.

Erstens gibt es einen allgemeinen Block mit der Fähigkeit (unter Verwendung des Selektors), eine Liste von Knoten auszuwählen, von denen Ping ausgeführt wird und auf denen. Dies ist die Übersichtstabelle für den Ping zwischen den ausgewählten Knoten für den im Grafana-Dashboard angegebenen Zeitraum:



Und hier sind die Grafiken mit allgemeinen Informationen zu den ausgewählten Knoten :



Wir haben auch eine Liste von Linien, von denen jede ein Diagramm für einen separaten Knoten aus der Auswahl des Quellknotens ist:



Wenn Sie eine solche Zeile erweitern, sehen Sie Informationen zu Pings von einem bestimmten Knoten zu allen anderen , die in der Auswahl der Zielknoten ausgewählt wurden:



Diese Informationen sind in Grafiken dargestellt:



Wie sehen die geschätzten Grafiken schließlich mit schlechtem Ping zwischen den Knoten aus?





Wenn Sie dies in einer realen Umgebung beobachten, ist es Zeit, die Gründe herauszufinden.

PS


Lesen Sie auch in unserem Blog:

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


All Articles