Skip to content

Serve the cache from local disk and sync with the NFS archive - #5

Merged
Robbie1977 merged 2 commits into
mainfrom
feature/local-cache-warm-restore
Aug 26, 2026
Merged

Serve the cache from local disk and sync with the NFS archive#5
Robbie1977 merged 2 commits into
mainfrom
feature/local-cache-warm-restore

Conversation

@Robbie1977

Copy link
Copy Markdown
Contributor

What changed

/var/cache/nginx is now local node disk; /cache stays the shared NFS volume but as an archive rather than the live cache. Every instance restores from it in the background at start (newest entries first, nginx already serving) and backs up into it on a daily crond schedule (default 03:00 plus a deterministic per-container jitter of up to 120 min, mkdir lock on the archive so overlapping runs serialise). Both directions use rsync --update and never delete, so the archive is the union of every instance's cache with the newest copy of each entry winning. In-flight temp files (<hash>.<counter>) are filtered so a partial body can never be copied. /status gains an archive block with restore/backup progress; purge-cached-404s.sh --archive purges the archive too. New: cache-lib.sh, cache-restore.sh, cache-backup.sh, test/cache-sync-test.sh (run at image build).

Why

Serving proxy_cache from NFS costs an open/stat round trip per hit and a metadata walk over millions of entries for the cache manager, and nginx cannot share one cache directory between instances anyway (the index is per-process shared memory). This lets us run several instances and takes the read traffic off the NAS. A blocking restore was ruled out: ~1 TB / millions of files takes hours, and nginx serves cache files that appear on disk after startup (verified on 1.26), so warming in the background costs nothing but cold misses.

How to test

sh test/cache-sync-test.sh locally; the image build runs it under BusyBox ash. CI mounts a one-entry fake archive at /cache, checks the entry lands in the local cache, that the backup is scheduled, that /status carries the archive block and that a manual cache-backup.sh --full runs. To deploy: change the stack's volumes to /data/owl-cache:/var/cache/nginx and /cache:/cache, set CACHE_MAX_SIZE to the node disk, and watch curl /status | jq .archive.restore until state is done.

Follow-ups

Nodes and NAS must agree on time (NTP) for newest-wins to be meaningful. The X-Force-Refresh warm-up only warms the instance it hits: warm one, run cache-backup.sh, let the others pick it up on restore. If the 11 TB local disk turns out to persist across container restarts, the .restored marker already makes subsequent starts skip the restore. The CI-workflow commit is applied separately from the local checkout (PAT lacks workflow scope).

Serving nginx's proxy_cache from the NFS volume costs an open/stat round
trip per hit and a metadata walk over millions of entries for the cache
manager, and nginx cannot share one cache directory between instances
anyway. /var/cache/nginx is now local node disk; /cache stays the shared
volume but as an archive that every instance restores from in the
background at start (newest entries first, nginx already serving) and
backs up into on a daily crond schedule with per-container jitter and an
mkdir lock on the archive.

Both directions use rsync --update and never delete, so the archive is
the union of every instance's cache with the newest copy of each entry
winning. Cache entries are MD5-named immutable blobs, which is what makes
that merge rule sufficient. In-flight temp files (<hash>.<counter>) are
filtered out so a partial body can never be copied. The scripts drop to
the nginx user because the archive is root-squashed NFS.

nginx serves a cache file that appears on disk after startup (a lookup
that misses the in-memory index still opens and validates the file), so
startup does not wait for a ~1 TB copy; /status reports restore and
backup progress under "archive".
The startup test now mounts a one-entry fake archive at /cache and checks
that the entry lands in the local cache, that the backup is scheduled,
that /status carries the archive block and that a manual backup runs.
@Robbie1977
Robbie1977 merged commit b07e572 into main Aug 26, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant