From 456fce2386d39e058d549be756095a18180a531c Mon Sep 17 00:00:00 2001 From: Robert Rapp Date: Mon, 12 Oct 2026 20:49:49 +0200 Subject: [PATCH] infra-stack: Schutz gegen veralteten ZFS-Seiten-Cache nach HA-Umzug, Betriebsabschnitt 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bei der HA-Probe startete dockerd auf pve48 nicht (bbolt „invalid freelist page“): Nach zfs receive liefert der Zielknoten für früher gemappte Dateien alten Inhalt aus dem Seiten-Cache. Ein oneshot-Dienst im LXC verwirft vor containerd/docker den Cache aller Dateien (posix_fadvise DONTNEED). BETRIEB.md Abschnitt 7: URLs, Geheimnisse (ohne Werte), file_sd auffrischen, Wiederherstellung. Co-Authored-By: Claude Opus 5.5 --- docs/BETRIEB.md | 48 ++++++++++++ .../files/knaster-drop-stale-cache.py | 75 +++++++++++++++++++ .../files/knaster-drop-stale-cache.service | 15 ++++ .../ansible/roles/infra-stack/tasks/main.yml | 21 ++++++ 4 files changed, 159 insertions(+) create mode 100644 platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.py create mode 100644 platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.service diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 796189e..f7b0a10 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -93,3 +93,51 @@ Stand 12.10.2026, getestet mit dem Wartungsmodus auf pve44. Gilt für pve44, pve **GPU-VMs aktualisieren** - Pakete per `apt` in der VM, die NVIDIA-Pakete sind auf `hold` (R580). Vor Treiberwechseln einen Snapshot anlegen: `qm snapshot vor-treiber`. - Nach einem VM-Neustart immer `refresh-vm-access.sh`, solange es keine DHCP-Reservierungen gibt (D5). + +## 7. Infra-Stack und Monitoring (LXC `infra`) + +Stand 12.10.2026 (Task 5.2). Der LXC `infra` (vmid 101, HA, Heimat pve44, Replikation nach pve48/pve52) führt einen Compose-Stack `knaster-infra`: Router (Dry-Run), TTS (Piper), Postgres, Prometheus, Alertmanager, Grafana, pve-exporter, node-exporter, autoheal. Kein eigener LiteLLM (D4). + +**Adressen** (DHCP, die IP kann sich nach einem Umzug ändern; aktuelle IP: `ansible infra -m debug -a var=ansible_host` auf ops oder `refresh-vm-access.sh`) + +| Dienst | URL | Anmeldung | +|---|---|---| +| Grafana | `http://:3000` | `admin` bzw. `kiosk` (Viewer) | +| Prometheus | `http://:9090` | ohne (nur LAN) | +| Alertmanager | `http://:9093` | ohne (nur LAN) | +| Router | `http://:8080` | Bearer-Token | + +**Wo liegt was** +- Konfiguration im Repo: `deploy/compose/infra`, `deploy/prometheus`, `deploy/alertmanager`, `deploy/grafana` (Datasource, Dashboards). Im LXC als Kopie unter `/opt/knaster/repo` (wird bei jedem Ansible-Lauf abgeglichen, dort nichts ändern). +- Geheimnisse nur im LXC, nie im Repo: `/opt/knaster/infra/.env` (0600) mit `POSTGRES_PASSWORD`, `KNASTER_API_TOKEN`, `GRAFANA_ADMIN_PASSWORD`, `GRAFANA_KIOSK_PASSWORD` (beim ersten Lauf mit `openssl rand` erzeugt, danach nie überschrieben). Lesen: `ssh root@ grep ^GRAFANA_KIOSK_PASSWORD= /opt/knaster/infra/.env` (für den Kiosk, Task 5.3). +- `/opt/knaster/infra/secrets/router_token` (Kopie von `KNASTER_API_TOKEN` für den Alertmanager) und `secrets/pve-exporter.env` (Kopie von ops `/root/.config/knaster/pve-exporter.env`, Token `monitoring@pve!prom`, PVEAuditor). +- Einstellungen ohne Geheimnis (Dry-Run, `LITELLM_BASE_URL` …) stehen in `platform/proxmox/ansible/roles/infra-stack/defaults/main.yml` und werden bei jedem Lauf in die `.env` geschrieben. + +**Ausrollen** (auf ops, idempotent): `cd /root/cluster-knaster && git pull`, Adressen auffrischen (Abschnitt 6, Schritt 6), dann in `platform/proxmox/ansible`: +- `ansible-playbook playbooks/monitoring.yml` – Exporter in den GPU-VMs (ohne Treiber-Rolle, kein Neustart) und Stack im LXC. +- Ein Image-Update heißt: Tag im Compose-File ändern, committen, Playbook laufen lassen. + +**Ziele der GPU-VMs (file_sd) auffrischen** +Die GPU-VMs haben DHCP-Adressen (D5). `platform/proxmox/scripts/monitoring-targets.py` schreibt aus `tofu output -json gpu_vms` und der Registry `node.json` und `gpu.json` (Labels `instance` = VM-Name, `pve_node`, `vmid`, `slot`, `slots`). Nach jedem VM-Neustart: +```bash +cd /root/cluster-knaster/platform/proxmox/tofu && . /root/.config/knaster/tofu.env && bash ../scripts/refresh-vm-access.sh +cd ../ansible && ansible-playbook playbooks/monitoring-targets.yml +``` +Prometheus liest die Dateien in `/opt/knaster/infra/file_sd` selbst neu, ein Neustart ist nicht nötig. + +**Exporter in den GPU-VMs:** `prometheus-node-exporter` (Port 9100, Ubuntu-Paket, liest `/var/lib/node_exporter/textfile` mit den Xid-Zählern) und `nvidia_gpu_exporter` (Port 9835, nvidia-smi; DCGM unterstützt Maxwell nicht). ufw-Regeln mit Kommentar `knaster-monitoring` erlauben beide Ports aus `192.168.100.0/24`; ufw selbst wird von der Rolle nicht ein- oder ausgeschaltet. + +**Alerts:** Alertmanager meldet an den Router (`/v1/healer/alertmanager`, Healer im Dry-Run). Der Receiver für n8n `knaster-notify` ist in `deploy/alertmanager/alertmanager.yml` nur vorbereitet (auskommentiert). Test: `docker exec knaster-infra-alertmanager-1 amtool --alertmanager.url=http://localhost:9093 alert add test severity=warning` und `… alert query`. + +**Dashboards** (Ordner `knaster`, Startseite „knaster – Übersicht“): Proxmox via Prometheus (grafana.com 10347), Node Exporter Full (1860), Nvidia GPU Metrics (14574), knaster – Übersicht (eigenes). + +**Achtung HA-Umzug und ZFS-Seiten-Cache (gefunden 12.10.2026 bei der HA-Probe)** +Nach `zfs receive` (Replikation) liefert ein Knoten für Dateien, die dort früher per mmap geöffnet waren, weiter den alten Inhalt aus seinem Seiten-Cache (ZFS 2.4.4, Kernel 7.0.14; Beleg auf pve44: `zfs diff ` leer, `written` 0, md5 der Datei trotzdem ungleich der im Snapshot). Folge beim ersten Umzug nach pve48: dockerd las veraltete bbolt-Datenbanken, brach mit `panic: invalid freelist page` ab, danach fehlten Netzwerk und Volume-Labels. Betroffen ist jeder replizierte LXC mit mmap-Nutzern (Docker, Prometheus-TSDB, später `grp-test`), sobald er auf einen Knoten zurückkehrt, auf dem er schon lief. +- Schutz im Container: Dienst `knaster-drop-stale-cache.service` (Rolle `infra-stack`) verwirft vor containerd/docker per `posix_fadvise(DONTNEED)` den Cache aller Dateien des Containers. Protokoll: `journalctl -u knaster-drop-stale-cache -b` (zeigt geänderte Datenbank-Prüfsummen). +- Eine Lösung auf den Knoten (OpenZFS-Fehler melden bzw. Update, Hookscript) entscheidet der Nutzer. + +**Wiederherstellung, falls Docker nach einem Umzug nicht startet** (im LXC, nichts löschen): +1. `systemctl stop docker containerd`; `journalctl -u docker -b | grep -A12 panic` zeigt die betroffene Datenbank (z. B. `buildkit/cache.db`). +2. Die defekte Datenbank bzw. das Verzeichnis beiseiteschieben, z. B. `mv /var/lib/docker/buildkit /var/lib/docker/buildkit.defekt-`. +3. `systemctl start docker`, dann `docker compose -p knaster-infra --env-file /opt/knaster/infra/.env -f /opt/knaster/repo/deploy/compose/infra/docker-compose.yml up -d --force-recreate` (Volumes bleiben, kein `down -v`). +4. Danach auf ops `ansible-playbook playbooks/monitoring.yml --limit infra`. diff --git a/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.py b/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.py new file mode 100644 index 0000000..9da7282 --- /dev/null +++ b/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Veralteten Seiten-Cache des Wirts für alle Dateien dieses Containers verwerfen (vor Docker-Start). + +Hintergrund (12.10.2026, Task 5.2, HA-Probe): Nach `zfs receive` (Proxmox-Replikation) liefert der Wirt +für Dateien, die auf ihm früher per mmap geöffnet waren, weiter den ALTEN Inhalt aus dem Seiten-Cache +(pve44: `zfs diff` leer, `written` 0, Datei-md5 trotzdem ungleich Snapshot; ZFS 2.4.4, Kernel 7.0.14). +Zieht der Container per HA auf so einen Knoten, liest dockerd veraltete bbolt-Datenbanken +(Panic „invalid freelist page“, Netzwerke/Volumes „not found“); Prometheus-TSDB wäre ebenso betroffen. + +POSIX_FADV_DONTNEED wirft saubere, nicht gemappte Seiten aus dem Cache; danach liest der Kernel wieder +von ZFS. Läuft vor containerd/docker, scheitert nie (Exit 0), protokolliert geänderte *.db-Prüfsummen. +""" + +import hashlib +import os +import stat +import sys + +SKIP = {"/proc", "/sys", "/dev", "/run", "/tmp"} +WATCH = ("/var/lib/docker/", "/var/lib/containerd/") + + +def md5(path: str) -> str: + h = hashlib.md5() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def main() -> int: + root_dev = os.stat("/").st_dev + files = dropped = errors = 0 + changed = [] + for dirpath, dirnames, filenames in os.walk("/"): + dirnames[:] = [d for d in dirnames if os.path.join(dirpath, d) not in SKIP] + try: + if os.lstat(dirpath).st_dev != root_dev: + dirnames[:] = [] + continue + except OSError: + continue + for name in filenames: + path = os.path.join(dirpath, name) + try: + st = os.lstat(path) + if not stat.S_ISREG(st.st_mode) or st.st_dev != root_dev or st.st_size == 0: + continue + files += 1 + watch = path.endswith(".db") and path.startswith(WATCH) + before = md5(path) if watch else None + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) + dropped += 1 + finally: + os.close(fd) + if watch: + after = md5(path) + if after != before: + changed.append(f"{path}: {before[:8]} -> {after[:8]}") + except OSError: + errors += 1 + for line in changed: + print(f"veralteter Cache verworfen: {line}") + print(f"{files} Dateien, Cache verworfen für {dropped}, Fehler {errors}, geänderte Datenbanken {len(changed)}") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: # nie den Docker-Start blockieren + print(f"Fehler: {exc}", file=sys.stderr) + sys.exit(0) diff --git a/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.service b/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.service new file mode 100644 index 0000000..3e85f3d --- /dev/null +++ b/platform/proxmox/ansible/roles/infra-stack/files/knaster-drop-stale-cache.service @@ -0,0 +1,15 @@ +[Unit] +Description=Veralteten Wirts-Seiten-Cache nach ZFS-Replikation verwerfen (vor Docker) +Documentation=file:///usr/local/sbin/knaster-drop-stale-cache +DefaultDependencies=no +After=local-fs.target +Before=containerd.service docker.socket docker.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /usr/local/sbin/knaster-drop-stale-cache +TimeoutStartSec=300 +RemainAfterExit=yes + +[Install] +WantedBy=containerd.service docker.service diff --git a/platform/proxmox/ansible/roles/infra-stack/tasks/main.yml b/platform/proxmox/ansible/roles/infra-stack/tasks/main.yml index 91227dd..c815eca 100644 --- a/platform/proxmox/ansible/roles/infra-stack/tasks/main.yml +++ b/platform/proxmox/ansible/roles/infra-stack/tasks/main.yml @@ -90,6 +90,27 @@ - name: file_sd-Ziele ansible.builtin.include_tasks: targets.yml +# HA-Umzug: Nach zfs receive liefert der Zielknoten für früher gemappte Dateien veralteten Seiten-Cache +# (12.10.2026 belegt). Vor jedem Docker-Start den Cache dieses Containers verwerfen. BETRIEB.md Abschnitt 7. +- name: Schutz gegen veralteten Seiten-Cache – Skript + ansible.builtin.copy: + src: knaster-drop-stale-cache.py + dest: /usr/local/sbin/knaster-drop-stale-cache + mode: "0755" + +- name: Schutz gegen veralteten Seiten-Cache – systemd-Dienst vor containerd/docker + ansible.builtin.copy: + src: knaster-drop-stale-cache.service + dest: /etc/systemd/system/knaster-drop-stale-cache.service + mode: "0644" + register: dropcache_unit + +- name: Schutz gegen veralteten Seiten-Cache – aktivieren + ansible.builtin.systemd: + name: knaster-drop-stale-cache.service + enabled: true + daemon_reload: "{{ dropcache_unit.changed }}" + - name: Stack bauen und starten ansible.builtin.command: > docker compose -p {{ infra_compose_project }} --env-file {{ infra_state_dir }}/.env