rpi: вторая копия /mnt/backup на 1 ТБ диск в роутере (rclone SFTP) + мониторинг монтирования и заполнения
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3811483577
commit
464e12abdb
@@ -35,6 +35,17 @@ OneDrive на внешний HDD. Мониторится с fr1 через pushg
|
|||||||
- ⚠️ Архив без ротации: растёт вечно. Скрипт выходит с кодом 2 при заполнении >90 % (→ алерт BackupFailed), плюс HostDisk* если когда-то поставим node-exporter.
|
- ⚠️ Архив без ротации: растёт вечно. Скрипт выходит с кодом 2 при заполнении >90 % (→ алерт BackupFailed), плюс HostDisk* если когда-то поставим node-exporter.
|
||||||
- Первая полная выгрузка (368 ГБ) запущена 2026-10-11 ~13:13, ~27 МБ/с (упор в домашний канал).
|
- Первая полная выгрузка (368 ГБ) запущена 2026-10-11 ~13:13, ~27 МБ/с (упор в домашний канал).
|
||||||
|
|
||||||
|
## Вторая копия — 1 ТБ диск в роутере (с 2026-10-11)
|
||||||
|
- Диск воткнут в роутер Huawei (`192.168.10.254`), а не в малину — **не ест питание USB малины** (два HDD от неё не тянут, см. «Диски»). Доступ — SFTP роутера на порту **8022**, пользователь `root`, remote `router:` в `~rus/.config/rclone/rclone.conf` (пароль только там), host key — `~rus/.ssh/known_hosts_router`.
|
||||||
|
- ⚠️ Роутер не считает хэши (`shell_type = none`, `md5sum_command = none`) — rclone сравнивает по размеру+mtime, целостность проверяется перечиткой.
|
||||||
|
- `mnt-router.service` — `rclone mount router: /mnt/router` (user rus, `--vfs-cache-mode writes`, кэш в `/mnt/backup/.rclone-cache`). Для просмотра/ручной работы; бэкап пишет **напрямую в remote**, не через FUSE (коды ошибок, без кэша).
|
||||||
|
- Скорость (замер 2026-10-11, параллельно с выгрузкой OneDrive): запись ~8 МБ/с, чтение ~11 МБ/с — первичная копия ~370 ГБ ≈ 13 ч. Диск 932 ГБ.
|
||||||
|
- `router-backup` (`stacks/rpi-backup/`, таймер 06:00, `flock`): в `router:rpi-backup/` — `onedrive/{archive,manifests,logs}` только `copy` (там ничего не удаляется), затем зеркало `onedrive/current` с `--max-delete 1000`, затем зеркало зашифрованных `servers/` с `--max-delete 50`. Порядок важен: удалённое из current к этому моменту уже скопировано в archive. Логи — `/mnt/backup/router-logs/`.
|
||||||
|
- ⚠️ `--max-delete` — предохранитель: если локально «пропадёт» много файлов (сбой диска/монтирования), копия на роутере не зачистится, а прогон упадёт с ошибкой → алерт `BackupFailed`.
|
||||||
|
- По воскресеньям `verify`: сверка размеров деревьев + перечитать с роутера последний daily каждого сервера и сравнить sha256.
|
||||||
|
- Мониторинг: `router_mount_up` и заполнение `/mnt/router` (в `smart-metrics`, `df` под `timeout 20`), итог прогона — `backup_*{job="backup_router"}`; алерты `RouterDiskMountDown` (30 мин), `BackupDiskFull` (>90 %, любой диск бэкапа), `BackupStale` (>30 ч); ряд «Вторая копия» на дашборде rpi-disk.
|
||||||
|
- ⚠️ 1 ТБ меньше основного диска: сейчас влезает (~370 ГБ OneDrive + ~100 ГБ серверов), но архив версий растёт вечно — следить за `BackupDiskFull`.
|
||||||
|
|
||||||
## Бэкап серверов fr1/ru1 (с 2026-10-11, код — `stacks/server-backup/`)
|
## Бэкап серверов fr1/ru1 (с 2026-10-11, код — `stacks/server-backup/`)
|
||||||
- Малина — внешняя площадка для VPS: `servers-backup.timer` (02:15) забирает по SSH выгрузки, которые **сервер сам шифрует открытым ключом age** (`tar | zstd | age`). Закрытого ключа на малине нет — ни она, ни Astra, ни взломщик архивы не прочитают. Ключ у владельца: Mac `~/.config/age/servers-backup.key`.
|
- Малина — внешняя площадка для VPS: `servers-backup.timer` (02:15) забирает по SSH выгрузки, которые **сервер сам шифрует открытым ключом age** (`tar | zstd | age`). Закрытого ключа на малине нет — ни она, ни Astra, ни взломщик архивы не прочитают. Ключ у владельца: Mac `~/.config/age/servers-backup.key`.
|
||||||
- Хранилище `/mnt/backup/servers/<host>/{daily,weekly,monthly}` (root 700): 14 / 8 / 12, monthly — хардлинк первого daily месяца. fr1 daily ~2.1 ГБ (5.5 мин), ru1 ~17 МБ, fr1 weekly (вс) — тома ботов и imgbot.
|
- Хранилище `/mnt/backup/servers/<host>/{daily,weekly,monthly}` (root 700): 14 / 8 / 12, monthly — хардлинк первого daily месяца. fr1 daily ~2.1 ГБ (5.5 мин), ru1 ~17 МБ, fr1 weekly (вс) — тома ботов и imgbot.
|
||||||
|
|||||||
@@ -1096,6 +1096,199 @@
|
|||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "row",
|
||||||
|
"title": "Вторая копия — диск в роутере",
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 31,
|
||||||
|
"w": 24,
|
||||||
|
"h": 1
|
||||||
|
},
|
||||||
|
"id": 200,
|
||||||
|
"panels": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 201,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Диск в роутере (SFTP)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 32,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "router_mount_up{instance=\"$instance\"}",
|
||||||
|
"instant": true,
|
||||||
|
"range": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"1": {
|
||||||
|
"text": "доступен",
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
"0": {
|
||||||
|
"text": "НЕДОСТУПЕН",
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"description": "rclone-монтирование /mnt/router (Huawei SFTP :8022). Диск питается от роутера, не от малины.",
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "background",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "value",
|
||||||
|
"justifyMode": "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 202,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "С последней копии на роутер",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 32,
|
||||||
|
"w": 9,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "time() - backup_last_success_timestamp{instance=\"$instance\",job=\"backup_router\"}",
|
||||||
|
"instant": true,
|
||||||
|
"range": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "s",
|
||||||
|
"noValue": "ещё не было",
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"value": null,
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 93600,
|
||||||
|
"color": "orange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 108000,
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"description": "Ежедневно 06:00: OneDrive (архив — copy, current — зеркало с --max-delete) и зашифрованные бэкапы серверов.",
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "background",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "value",
|
||||||
|
"justifyMode": "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 203,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Заполнение диска в роутере",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 15,
|
||||||
|
"y": 32,
|
||||||
|
"w": 9,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "1 - backup_disk_avail_bytes{instance=\"$instance\",mountpoint=\"/mnt/router\"} / backup_disk_size_bytes{instance=\"$instance\",mountpoint=\"/mnt/router\"}",
|
||||||
|
"instant": true,
|
||||||
|
"range": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percentunit",
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"value": null,
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.8,
|
||||||
|
"color": "orange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.9,
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "background",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "value",
|
||||||
|
"justifyMode": "center"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -90,13 +90,29 @@ groups:
|
|||||||
annotations:
|
annotations:
|
||||||
summary: "{{ $labels.instance }}: {{ $labels.job }} завершился с кодом {{ $value }} — см. journalctl на хосте"
|
summary: "{{ $labels.instance }}: {{ $labels.job }} завершился с кодом {{ $value }} — см. journalctl на хосте"
|
||||||
|
|
||||||
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней; бэкап серверов ежедневно → 26ч
|
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней; бэкап серверов ежедневно → 26ч; копия на роутер ежедневно → 30ч (первичная ~13ч)
|
||||||
- alert: BackupStale
|
- alert: BackupStale
|
||||||
expr: |
|
expr: |
|
||||||
(time() - backup_last_success_timestamp{job="backup_onedrive"} > 12 * 3600)
|
(time() - backup_last_success_timestamp{job="backup_onedrive"} > 12 * 3600)
|
||||||
or (time() - backup_last_success_timestamp{job="backup_onedrive_verify"} > 8 * 86400)
|
or (time() - backup_last_success_timestamp{job="backup_onedrive_verify"} > 8 * 86400)
|
||||||
or (time() - backup_last_success_timestamp{job="backup_servers"} > 26 * 3600)
|
or (time() - backup_last_success_timestamp{job="backup_servers"} > 26 * 3600)
|
||||||
|
or (time() - backup_last_success_timestamp{job="backup_router"} > 30 * 3600)
|
||||||
for: 10m
|
for: 10m
|
||||||
labels: {severity: critical}
|
labels: {severity: critical}
|
||||||
annotations:
|
annotations:
|
||||||
summary: "{{ $labels.instance }}: {{ $labels.job }} не было успешного прогона {{ $value | humanizeDuration }}"
|
summary: "{{ $labels.instance }}: {{ $labels.job }} не было успешного прогона {{ $value | humanizeDuration }}"
|
||||||
|
|
||||||
|
# /mnt/backup (HDD 4 ТБ) и /mnt/router (1 ТБ диск в роутере) — заполнение
|
||||||
|
- alert: BackupDiskFull
|
||||||
|
expr: 1 - backup_disk_avail_bytes / backup_disk_size_bytes > 0.9
|
||||||
|
for: 15m
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: диск бэкапа {{ $labels.mountpoint }} заполнен на {{ $value | humanizePercentage }}"
|
||||||
|
|
||||||
|
- alert: RouterDiskMountDown
|
||||||
|
expr: router_mount_up == 0
|
||||||
|
for: 30m
|
||||||
|
labels: {severity: warning}
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }}: диск в роутере (/mnt/router, SFTP) недоступен 30+ мин — вторая копия бэкапа не пишется"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Вторая копия /mnt/backup на 1 ТБ диск в роутере Huawei (SFTP :8022, remote `router:` в
|
||||||
|
# ~rus/.config/rclone/rclone.conf). Диск питается от роутера — не нагружает USB малины.
|
||||||
|
#
|
||||||
|
# router-backup run — ежедневно (таймер 06:00)
|
||||||
|
# router-backup verify — по воскресеньям после run: размеры + перечитать свежие архивы серверов по sha256
|
||||||
|
#
|
||||||
|
# Пишем прямо в remote, а не через FUSE /mnt/router: так надёжнее и есть коды ошибок.
|
||||||
|
# Роутер не умеет хэши (shell_type=none) → сравнение по размеру+mtime, целостность — перечиткой.
|
||||||
|
set -uo pipefail
|
||||||
|
C=/home/rus/.config/rclone/rclone.conf
|
||||||
|
R=router:rpi-backup
|
||||||
|
SRC=/mnt/backup
|
||||||
|
TS=$(date +%F_%H%M)
|
||||||
|
LOGDIR=$SRC/router-logs; mkdir -p "$LOGDIR"
|
||||||
|
LOG="$LOGDIR/router-$TS.log"
|
||||||
|
rc() { rclone --config "$C" --log-file "$LOG" --log-level INFO --stats 10m --stats-log-level NOTICE \
|
||||||
|
--transfers 2 --checkers 4 --sftp-concurrency 8 --retries 5 --low-level-retries 20 "$@"; }
|
||||||
|
|
||||||
|
mountpoint -q "$SRC" || { echo "$SRC не смонтирован — пропуск"; exit 1; }
|
||||||
|
rclone --config "$C" lsd router: >/dev/null 2>&1 || { echo "роутер недоступен по SFTP"; exit 1; }
|
||||||
|
|
||||||
|
run() {
|
||||||
|
local r=0
|
||||||
|
# 1) то, что никогда не удаляется: архив версий, манифесты, логи — только copy
|
||||||
|
for d in archive manifests logs; do
|
||||||
|
[ -d "$SRC/onedrive/$d" ] && { rc copy "$SRC/onedrive/$d" "$R/onedrive/$d" || r=1; }
|
||||||
|
done
|
||||||
|
# 2) зеркало current. Удалённое из current локально уже лежит в archive (скопированном выше).
|
||||||
|
# --max-delete: если локально «пропало» много файлов (сбой диска/монтирования) — стоп, а не зачистка копии
|
||||||
|
rc sync "$SRC/onedrive/current" "$R/onedrive/current" --exclude '*.partial' --max-delete 1000 || r=1
|
||||||
|
# 3) зашифрованные бэкапы серверов — зеркало (ротация 14/8/12 → удаляется пара файлов в день)
|
||||||
|
rc sync "$SRC/servers" "$R/servers" --max-delete 50 || r=1
|
||||||
|
echo "run: $([ $r = 0 ] && echo OK || echo 'ЕСТЬ ОШИБКИ') — лог $LOG"
|
||||||
|
return $r
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
local r=0 s rel want got
|
||||||
|
# размеры всего дерева (только листинг, без чтения данных)
|
||||||
|
rc check "$SRC/onedrive/current" "$R/onedrive/current" --size-only --one-way --exclude '*.partial' || r=1
|
||||||
|
rc check "$SRC/servers" "$R/servers" --size-only --one-way || r=1
|
||||||
|
# перечитать с роутера последний daily каждого сервера и сверить с sha256
|
||||||
|
for s in $(ls -1t "$SRC"/servers/*/daily/*.age.sha256 2>/dev/null | awk -F/ '!seen[$(NF-2)]++'); do
|
||||||
|
rel=${s#"$SRC/servers/"}; rel=${rel%.sha256}
|
||||||
|
want=$(cut -d' ' -f1 "$s")
|
||||||
|
got=$(rclone --config "$C" cat "$R/servers/$rel" 2>>"$LOG" | sha256sum | cut -d' ' -f1)
|
||||||
|
if [ "$want" = "$got" ]; then echo "read-back OK $rel"; else echo "read-back BAD $rel"; r=1; fi
|
||||||
|
done
|
||||||
|
echo "verify: $([ $r = 0 ] && echo OK || echo 'ЕСТЬ РАСХОЖДЕНИЯ')"
|
||||||
|
return $r
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-run}" in
|
||||||
|
run) run; r=$?; [ "$(date +%u)" = 7 ] && { verify || r=1; }; exit $r ;;
|
||||||
|
verify) verify ;;
|
||||||
|
*) echo "usage: router-backup run|verify" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
@@ -39,4 +39,15 @@ if mountpoint -q /mnt/backup; then
|
|||||||
echo "backup_disk_size_bytes{mountpoint=\"/mnt/backup\"} $size" >> "$out"
|
echo "backup_disk_size_bytes{mountpoint=\"/mnt/backup\"} $size" >> "$out"
|
||||||
echo "backup_disk_avail_bytes{mountpoint=\"/mnt/backup\"} $avail" >> "$out"
|
echo "backup_disk_avail_bytes{mountpoint=\"/mnt/backup\"} $avail" >> "$out"
|
||||||
fi
|
fi
|
||||||
|
# диск в роутере (rclone SFTP-монтирование /mnt/router): жив ли и сколько занято.
|
||||||
|
# timeout — зависший FUSE не должен вешать сбор метрик
|
||||||
|
echo "# TYPE router_mount_up gauge" >> "$out"
|
||||||
|
if mountpoint -q /mnt/router && r=$(timeout 20 df -B1 --output=size,avail /mnt/router 2>/dev/null | tail -1) && [ -n "$r" ]; then
|
||||||
|
read -r size avail <<< "$r"
|
||||||
|
echo "router_mount_up 1" >> "$out"
|
||||||
|
echo "backup_disk_size_bytes{mountpoint=\"/mnt/router\"} $size" >> "$out"
|
||||||
|
echo "backup_disk_avail_bytes{mountpoint=\"/mnt/router\"} $avail" >> "$out"
|
||||||
|
else
|
||||||
|
echo "router_mount_up 0" >> "$out"
|
||||||
|
fi
|
||||||
metrics-push smart < "$out"
|
metrics-push smart < "$out"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Second copy of /mnt/backup to router USB disk (rclone SFTP)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
RequiresMountsFor=/mnt/backup
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/flock -n /run/lock/router-backup.lock /usr/local/bin/router-backup run
|
||||||
|
ExecStopPost=/usr/local/bin/backup-metrics backup_router
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
# первичная копия ~370 ГБ при ~8 МБ/с — около 13 ч
|
||||||
|
TimeoutStartSec=infinity
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily copy of /mnt/backup to router disk
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 06:00
|
||||||
|
RandomizedDelaySec=10m
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user