With logs off flash, the dominant remaining eMMC writer is zwave-js-ui: its value DB is fsync'd on every value change (jsonl-db; zwave-js-ui doesn't expose the driver throttle). On a busy network that fsync storm churned ~2.6 GiB/day for ~0.1 KiB/s of real data — near-pure write amplification (each fsync forces a block write + ext4 journal commit). Preload libeatmydata into the zwave-js container (musl .so extracted from the image's Alpine base, mounted ro, LD_PRELOAD) so fsync/fdatasync become no-ops and the kernel batches writes via normal writeback. The WS/MQTT value stream to HA and the on-disk cache are unchanged — full data resolution, no re-interview on restart. No ext4 mount option can do this (fsync is a durability contract; commit=/data=writeback/nobarrier don't batch it, and the only mount-level alternative — tmpfs — makes the whole store volatile). Also: - noatime -> noatime,lazytime on root: batches the inode-timestamp writeback (e.g. the zwave-js lockfile heartbeat) without weakening data durability. - edge-zwave-settings-backup.timer: hourly validated rolling backup of settings.json (Z-Wave network keys), since neutralized fsync means a power-loss mid settings-write could truncate it. Value-cache loss on crash is harmless (devices re-report; jsonl-db tolerates a partial trailing line). Verify the drop on the eMMC-write monitor; rollback = drop the LD_PRELOAD env + .so mount from the compose file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.4 KiB
Django/Jinja
57 lines
2.4 KiB
Django/Jinja
# Managed by ansible (roles/edge-host). Mirrors the former k8s manifests:
|
|
# kubernetes/apps/home-assistant-zwavejs/deployment.yaml and
|
|
# kubernetes/apps/home-assistant/sml2mqtt-deployment.yaml.
|
|
#
|
|
# No `privileged` needed here: docker's --device handles the device-cgroup
|
|
# grant that hostPath in k8s could not (that was the only reason the pods
|
|
# ran privileged).
|
|
# Both services use host networking: real host addresses (v4+v6), no
|
|
# bridge, no NAT. The broker LB is IPv6-first and docker bridge NAT66 is
|
|
# a tarpit (unmasqueraded SYNs die silently); the host stack just works.
|
|
services:
|
|
zwave-js:
|
|
image: {{ edge_zwavejs_image }}
|
|
container_name: zwave-js
|
|
restart: unless-stopped
|
|
stop_signal: SIGINT
|
|
tty: true
|
|
environment:
|
|
- TZ={{ edge_tz }}
|
|
# Batch the value-DB fsync storm to spare the eMMC (see role tasks /
|
|
# README "Logging & eMMC"). Neutralizes fsync only for this container;
|
|
# the WS/MQTT value stream to HA and cache persistence are unchanged.
|
|
- LD_PRELOAD=/usr/lib/libeatmydata.so
|
|
env_file:
|
|
- zwavejs.env
|
|
devices:
|
|
- "{{ edge_zwave_device }}:/dev/zwave"
|
|
volumes:
|
|
# musl libeatmydata.so extracted from the image's Alpine base (ro)
|
|
- ./zwave-js/libeatmydata.so:/usr/lib/libeatmydata.so:ro
|
|
# Persistent store: settings.json, device DB, network keys. Seeded
|
|
# from the k8s PVC `zwavejs-store` at cutover (see README). Since
|
|
# this host is stateful there is no init-container merge step —
|
|
# settings.json is edited via the UI and persists here; zwave.port
|
|
# stays /dev/zwave.
|
|
- ./zwave-js/store:/usr/src/app/store
|
|
# Host networking: binds 3000 (Z-Wave JS websocket for HA) and 8091
|
|
# (web UI) directly on the host.
|
|
network_mode: host
|
|
|
|
sml2mqtt:
|
|
image: {{ edge_sml2mqtt_image }}
|
|
container_name: sml2mqtt
|
|
restart: unless-stopped
|
|
# Outbound-only (serial in, MQTT out) — host networking sidesteps the
|
|
# docker NAT66 gap: the broker LB is IPv6-first and bridge-network v6
|
|
# SYNs leave unmasqueraded and die. The host stack reaches fd99::81
|
|
# fine (open3e proves it continuously).
|
|
network_mode: host
|
|
# Serial exclusivity: this is the ONLY reader of the meter port —
|
|
# HA's edl21 integration stays disabled (see sml2mqtt-config comment
|
|
# history in the k8s repo).
|
|
devices:
|
|
- "{{ edge_stromzaehler_device }}:/dev/stromzaehler"
|
|
volumes:
|
|
- ./sml2mqtt/config.yml:/sml2mqtt/config.yml:ro
|