From a2027302d9b4c671ba31f26249bd6f01216715f8 Mon Sep 17 00:00:00 2001 From: Josh Poole Date: Mon, 24 Aug 2026 08:52:59 +0100 Subject: [PATCH] 20260824 - Move tar1090's /run to tmpfs to stop ~7 GB/day of SD-card writes Every node writes ~7-8 GB/day to a consumer SD card for no functional benefit. readsb rewrites a 67-byte aircraft.json at 1 Hz forever - whether or not any aircraft exist - and collectd rewrites 48 RRD files every 60s. All of it landed on the container overlay, i.e. the card, because the tar1090 container declared tmpfs for /var/log only. On a normal bare-metal tar1090 install /run is already tmpfs and none of this ever touches storage; the container is the anomaly. The waste is not the data, it is where it lands. Measured across four nodes, readsb costs a flat 3.34-3.36 GB/day to produce ~8 KB/min of real JSON - about 300x write amplification, because each tiny file still dirties a full 4 KB ext4 block plus a journal entry. The figure being identical on nodes with zero aircraft is what proves the writes are cadence, not traffic: they will never improve on their own, on any node. Mount /run as tmpfs and enable GRAPHS1090_REDUCE_IO, which runs the RRD set from that tmpfs and writes it back as a single gzipped tarball instead of 48 scattered in-place updates per minute. REDUCE_IO uses /run/collectd as its working directory, so it is only effective *because* /run is now tmpfs - enabled on its own it would relocate the writes from one spot on the card to another. exec is required on the mount: s6-overlay execs its service scripts from /run. The ADS-B pathway is deliberately unchanged. readsb still writes aircraft.json at 1 Hz and the proxy still reads that file first on every request, so a node with a local receiver behaves exactly as before - only the backing store differs. Gating readsb off when no receiver is configured was considered and rejected: it saves ~5 MB of RAM, and it would put the local-receiver case on a separate rarely-exercised code path, which is how a hardcoded developer IP reached production here in the first place. Also blank the adsb_source default. It shipped as 192.168.8.183,30005,beast_in - a developer's LAN address that no deployed node can reach, and that every node in the fleet has been repeatedly opening TCP connections to on whatever customer network it sits on. Local-receiver support lives in the config key, not in the default value, so blanking it costs no capability: the merger still emits READSB_NET_CONNECTOR whenever adsb_source is set. Verified on owl-ded9 and jonathan-node-1 over a 46-hour soak: block layer 6.32 -> 0.79 GB/day (jonathan-node-1) block layer 4.39 -> 0.71 GB/day (owl-ded9) collectd 5.93 -> 0.00 GB/day readsb 3.36 -> 0.10 GB/day (residual is /var/globe_history, which stays on flash by design) RAM available was unchanged or improved on both - the tmpfs costs ~35-40 MB but frees more than that in overlay page cache. /run held steady at 36-40M of 64M across 46 hours, growing ~1 MB. 64M is sized for the RRD set, which varies by node (23M on owl, 30M on jonathan-node-1); 32M would not have been enough. Co-Authored-By: Claude Opus 5 --- config/default.yml | 2 +- docker-compose.yml | 9 +++++++++ staging/docker-compose.staging.yml | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config/default.yml b/config/default.yml index 1545deb..86816db 100644 --- a/config/default.yml +++ b/config/default.yml @@ -103,7 +103,7 @@ save: # tar1090-node configuration # Note: tar1090 uses location.rx for receiver position (no separate tar1090.location needed) tar1090: - adsb_source: "192.168.8.183,30005,beast_in" # External ADS-B feed (IP,PORT,PROTOCOL) + adsb_source: "" # Optional local ADS-B receiver (IP,PORT,PROTOCOL). Empty = rely on adsblol_fallback. adsblol_fallback: true adsblol_radius: 40 diff --git a/docker-compose.yml b/docker-compose.yml index 9240bb2..13201e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -124,7 +124,16 @@ services: - RECEIVER_LON=${RECEIVER_LON:-0} - ADSBLOL_RADIUS=${ADSBLOL_RADIUS:-40} - PROXY_PORT=3005 + # SD-card endurance: run the graphs1090 RRD set from the /run tmpfs below + # and write it back as a single gzipped tarball, instead of 48 scattered + # in-place RRD updates every 60s. Only effective because /run is tmpfs. + - GRAPHS1090_REDUCE_IO=true tmpfs: + # /run holds volatile runtime state (readsb JSON at 1 Hz, tar1090 chunks, + # graphs1090 PNGs, s6 service dirs) and is tmpfs on any normal install. + # On the container overlay it cost ~7 GB/day of SD-card writes for ~8 KB/min + # of real data. exec is required: s6-overlay execs its service scripts here. + - /run:exec,size=64M - /var/log:size=32M adsb2dd: diff --git a/staging/docker-compose.staging.yml b/staging/docker-compose.staging.yml index 28adf9a..42f97ad 100644 --- a/staging/docker-compose.staging.yml +++ b/staging/docker-compose.staging.yml @@ -111,7 +111,16 @@ services: - RECEIVER_LON=${RECEIVER_LON:--84.65191} - ADSBLOL_RADIUS=${ADSBLOL_RADIUS:-40} - PROXY_PORT=3005 + # SD-card endurance: run the graphs1090 RRD set from the /run tmpfs below + # and write it back as a single gzipped tarball, instead of 48 scattered + # in-place RRD updates every 60s. Only effective because /run is tmpfs. + - GRAPHS1090_REDUCE_IO=true tmpfs: + # /run holds volatile runtime state (readsb JSON at 1 Hz, tar1090 chunks, + # graphs1090 PNGs, s6 service dirs) and is tmpfs on any normal install. + # On the container overlay it cost ~7 GB/day of SD-card writes for ~8 KB/min + # of real data. exec is required: s6-overlay execs its service scripts here. + - /run:exec,size=64M - /var/log:size=32M adsb2dd: