Files
doc/ks4/restic-backup.md
T
Julien LutranandClaude Fable 5 fb410259c5 restic-backup: restic becomes THE S3 tool — full rewrite
Four buckets (data/nextcloud/seafile/instances), instances leg via
incus file mount (no plugin architecture in restic/rustic — mount the
source instead of forking the tool), nightly driver spec, parallel
plakar comparison week, plakar retirement plan (kloset kept for
PlakarKorp/plakar#2338 debugging), interim ks2 replica push until the
FTTH/nuc leg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 12:44:48 +02:00

248 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# restic backups on ks4 — the S3 leg
Status: **decided 2026-08-25 — restic replaces plakar as the S3
backup tool** (evaluation first: parallel run + comparison, see §8).
Final architecture: **two tools**`incus copy` for replication
(local sdb, nuc after FTTH, ks2 push as interim), **restic** for
everything S3.
Why: plakar's incremental cost scales with tree size, not churn
(~800 MiB parent metadata re-read from S3 + hours of CPU per night on
a 500k-file tree — measured, reported as
[PlakarKorp/plakar#2338](https://github.com/PlakarKorp/plakar/issues/2338)).
restic keeps its comparison state in a local cache: a night costs a
local stat-walk plus the churn — the rsync cost model. Neither restic
nor rustic has plakar's source-plugin architecture, but none is
needed: instances are reached via `incus file mount` (FUSE over the
same per-instance sftp API our plakar integration used) — zero
patches to maintain (see §6).
## 1. Repositories — one bucket per repo
| bucket = repo | contents | est. size | blobs (index RAM driver) |
|---|---|---|---|
| `restic-data` | `/backup/plakar-dumps` dump tree + the 8 small fs sources | ~50 G | small |
| `restic-nextcloud` | `/nextcloud` data (live rootfs path) | 737 G | ~1 M |
| `restic-seafile` | `/opt/seafile` (live rootfs path) | 933 G | ~24 M |
| `restic-instances` | the `backup`-project replicas, per-file via mount | ~16 small instances | medium |
Separate repos because restic loads the whole repo index into RAM per
operation and prune/check are per-repo; separate **buckets** (not
prefixes) for per-bucket OVH usage metrics, independent
versioning/object-lock decisions later, and bucket-native credential
scoping. One credential pair across all four to start.
Passphrase: `/root/.restic-passphrase` (mode 600), one for all repos,
**copy to the password manager immediately**. S3 creds + env in
`/root/.restic-env` (mode 600).
## 2. Option analysis (our numbers, not defaults-worship)
### `--pack-size` (default 16 MiB, max 128)
Packs are the S3 objects; blobs (~1 MiB avg for data) live inside
them. Pack size does **not** change index RAM (that scales with blob
count) — it changes object count, request count, and rewrite
amplification:
| pack-size | S3 objects for ~1.2 T stored | trade-off |
|---|---|---|
| 16 MiB | ~75 000 | slow `check`/listing, more requests; prune rewrites are fine-grained |
| **64 MiB (chosen)** | ~19 000 | 4× fewer objects/requests; assembly RAM ≈ pack×connections ≈ 64 M × 5 ≈ 320 MiB — fine |
| 128 MiB | ~9 500 | fewer still, but prune rewrite amplification doubles and upload buffers grow |
`--pack-size 64` on **every backup/prune invocation** (not a repo
property).
### Memory (the real constraint on this 23/31 GiB-used box)
- Index RAM ≈ 200300 B/blob → ~0.3 GiB (nextcloud) and
~0.61.2 GiB (seafile) per backup run; prune peaks 23×. Hence the
repo split (§1).
- `GOGC=20` trades CPU for a smaller Go heap — enable if the seafile
prune ever pressures the box.
### Cache location
`~/.cache/restic` would land on `/` (14 G free) and can reach a few
GiB at this scale → `--cache-dir /backup/restic-cache` (sdb pool;
cache reads are ARC-warm after first touch). tmpfs was considered and
rejected: the cache exists to avoid *network* metadata re-fetches;
tmpfs loses it at reboot and eats the RAM the ARC needs.
### Concurrency & compression
- `--read-concurrency 8` (default 2): more outstanding reads lets the
HDD elevator help on seek-bound trees (our 275-IOPS wall).
- `-o s3.connections=8` (default 5): mild upload parallelism bump.
- Compression: repo-v2 default `auto` is right.
### Excludes — the cheapest optimisation of all
Nextcloud's `appdata_*/preview` (and `dav-photocache`) are
regenerable caches holding a large fraction of the 500k files:
```
# /root/scripts/restic-nextcloud-exclude
/var/lib/incus/storage-pools/data/containers/nextcloud/rootfs/nextcloud/data/appdata_*/preview
/var/lib/incus/storage-pools/data/containers/nextcloud/rootfs/nextcloud/data/appdata_*/dav-photocache
```
Verify before the seed: `find .../data/appdata_* -path '*/preview/*' | wc -l`
for the win size, then `restic backup --dry-run -vv --exclude-file …`
prints every decision without uploading. Per-user `<user>/cache/`
dirs are a further candidate. Seafile: no excludes — the block store
*is* the data.
### Escape hatch: rustic
[rustic](https://github.com/rustic-rs/rustic) speaks the same
repository format — switching later is a binary swap on the same
repos (decided 2026-08-25: restic first, rustic if performance
issues rise; manual upgrades accepted). It would bring lock-free
prune and a lower RAM footprint. Don't run both concurrently on one
repo (rustic ignores restic's locks); a clean switchover is fine.
## 3. Install + init (root on ks4)
```sh
apt install restic sshfs # sshfs: needed by `incus file mount` (§6)
head -c 32 /dev/urandom | base64 > /root/.restic-passphrase && chmod 600 /root/.restic-passphrase
# ⚠️ password manager, NOW.
cat > /root/.restic-env <<'EOF'
export AWS_ACCESS_KEY_ID=<AK>
export AWS_SECRET_ACCESS_KEY=<SK>
export RESTIC_PASSWORD_FILE=/root/.restic-passphrase
export RESTIC_CACHE_DIR=/backup/restic-cache
EOF
chmod 600 /root/.restic-env && . /root/.restic-env && mkdir -p /backup/restic-cache
for r in restic-data restic-nextcloud restic-seafile restic-instances; do
restic -r s3:s3.sbg.io.cloud.ovh.net/$r init
done
```
## 4. Seed plan
1. **ARC floor first** (helps every walk on the box):
```sh
echo 8589934592 > /sys/module/zfs/parameters/zfs_arc_min
echo "options zfs zfs_arc_min=8589934592" > /etc/modprobe.d/zfs-arc.conf
update-initramfs -u
awk '/^c_min/ {printf "%.1f GiB\n", $3/1073741824}' /proc/spl/kstat/zfs/arcstats
```
⚠️ the floor is defended even under pressure — real headroom is
~31 G services 8 G; revisit before adding a fat service.
2. `restic-data` (~50 G — an hour).
3. `restic-nextcloud` (proven ~22 MiB/s from the live pool → ~8 h),
screen:
```sh
restic -r s3:s3.sbg.io.cloud.ovh.net/restic-nextcloud \
backup --pack-size 64 --read-concurrency 8 -o s3.connections=8 \
--exclude-file /root/scripts/restic-nextcloud-exclude \
/var/lib/incus/storage-pools/data/containers/nextcloud/rootfs/nextcloud
```
4. `restic-instances` (16 small replicas via §6 — a few hours).
5. `restic-seafile` — the one-time debt: 12 days, restartable at any
point (committed packs dedup on retry).
## 5. Nightly driver — `scripts/restic-backup.sh`
Same shape as the plakar drivers (flock, env, loud logging). Phases:
1. **dumps** — lifted verbatim from `plakar-backup.sh`: incus DB
dumps + auto-discovered MariaDB/PostgreSQL (native and docker)
dumps into `/backup/plakar-dumps` (path kept), then
`restic -r …/restic-data backup` of the dump tree **and** the 8
small source paths (one snapshot, paths from a list file).
2. **giants** — nextcloud (with excludes) and seafile into their
repos.
3. **instances** — §6 loop into `restic-instances`.
4. **retention** — per repo: `restic forget --keep-daily 14
--keep-weekly 8 --keep-monthly 6` (instant without `--prune`).
```cron
0 5 * * * /root/scripts/restic-backup.sh >> /var/log/restic-backup.log 2>&1
```
Weekly maintenance (Sunday; prune takes an **exclusive lock** — never
overlap the 05:00 run):
```cron
0 14 * * 0 /root/scripts/restic-maintenance.sh >> /var/log/restic-maintenance.log 2>&1
```
Per repo: `restic prune --max-unused 10% --max-repack-size 4G
--pack-size 64` — our dead-data rate is ~0.10.4 GiB/day against
~700 GiB repos (seafile append-mostly ≈ nothing dies), so 10 %
tolerated slack lets prune skip repacking for months, and the 4 G cap
bounds any single Sunday to ~1015 min even after a mass deletion.
Then `restic check` (structure, cheap) + `check
--read-data-subset=1/52` rotating — full data verification of every
byte once a year, ~25 GiB read per week.
## 6. Instances without a plugin — `incus file mount`
Neither restic nor rustic accepts source plugins, and forking the
backup tool is the wrong place to carry a patch. Instead, incus
exposes any instance (including **stopped replicas**) as a FUSE mount
over the same per-instance sftp API the plakar integration used:
```sh
mkdir -p /run/restic-incus
for inst in $(incus list --project backup -c n -f csv); do
case ",$EXCLUDE_INSTANCES," in *",$inst,"*) continue;; esac # nextcloud,seafile
incus config show "$inst" --project backup --expanded > "/backup/plakar-dumps/incus/$inst.yaml"
incus file mount "$inst/" /run/restic-incus --project backup & # foreground process; background + kill
MPID=$!; sleep 2
restic -r s3:…/restic-instances backup --pack-size 64 --tag "$inst" /run/restic-incus
kill $MPID; wait $MPID 2>/dev/null; umount /run/restic-incus 2>/dev/null
done
```
(Exact mount lifecycle to be hardened in the script — `incus file
mount` runs in the foreground and needs `sshfs` installed.) All
instances by default, opt-out via a variable — the no-manifest-drift
rule survives the tool change. Chained after the 01:00 incus-copy in
its cron entry once validated, same freshness reasoning as before.
Bonus over plakar: one shared repo dedups the Ubuntu base across all
16 replicas. Restore granularity: per-file; instance definitions ride
the dump tree (`incus/<inst>.yaml` + `incus-global-db.sql`).
## 7. Restore test (gate)
Per repo, one restore + diff (never to `/tmp` — 16 GiB tmpfs):
```sh
restic -r s3:…/restic-nextcloud restore latest --target /backup/restore-test --include '<subtree>'
diff -r … && rm -rf /backup/restore-test
```
## 8. Evaluation protocol + plakar retirement
- **Parallel week**: plakar 04:30 cron keeps running untouched;
restic runs at 05:00 on the same data. Same nights, same churn →
direct comparison (wall, bytes read from store, bytes written, peak
RSS via `/usr/bin/time -v`) against plakar's measured table in
[plakar-s3-data.md](plakar-s3-data.md). Restore-speed compared on
the same 10 GiB dumps tree (plakar measured: 39 min).
- **Cutover** (after the comparison confirms + restore test passes):
drop the plakar 04:30 + Sunday-check crons; 01:00 becomes
`incus-copy ; restic instances phase` (or stays in the 05:00 run).
Update plan.md legs table.
- **plakar retirement, one exception**: keep plakar installed and
keep the `plakar-data` kloset until
[#2338](https://github.com/PlakarKorp/plakar/issues/2338) concludes
— the issue offers debug runs against that repository. Delete the
`plakar-incus` bucket immediately; `plakar-data` a few weeks later.
The `integration-incus` repo retires from production; optionally
still publish to PlakarKorp/hub as a community contribution.
- **Interim ks2 push** (until the nuc leg seeds after FTTH, expected
before end of September): re-enable the replica push so instances
keep an off-site copy meanwhile:
```cron
0 2 * * * /root/scripts/incus-copy.sh -d ks2 -m push >> /var/log/incus-copy.log 2>&1
```