infra-stack: Schutz gegen veralteten ZFS-Seiten-Cache nach HA-Umzug, Betriebsabschnitt 7
ci / test (push) Successful in 28s
ci / test (push) Successful in 28s
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ddd7ad033d
commit
456fce2386
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user