132 lines
6.4 KiB
Markdown
132 lines
6.4 KiB
Markdown
# бекапирование sqlite в minio
|
||
|
||
```yaml
|
||
services:
|
||
sqlite_backup:
|
||
image: git.realmanual.ru/pub/sqlite-backup-s3
|
||
container_name: sqlite_backup
|
||
restart: always
|
||
volumes:
|
||
- ./var/lib/data/:/data
|
||
environment:
|
||
- DB_FILE
|
||
- CRONTAB
|
||
- DELETE_AFTER
|
||
- KEEP_LAST
|
||
- TZ
|
||
- SQLITE_TIMEOUT
|
||
- BACKUP_RETRIES
|
||
- BACKUP_RETRY_DELAY
|
||
- MINIO_PATH
|
||
- MINIO_ACCOUNT_ID
|
||
- MINIO_APPLICATION_KEY
|
||
- MINIO_ENDPOINT
|
||
- MINIO_LOCATION
|
||
- HEALTHCHECK_URL
|
||
- HEALTHCHECK_UUID
|
||
```
|
||
|
||
пример .env
|
||
|
||
```bash
|
||
DB_FILE=/data/db.sqlite
|
||
CRONTAB=00 06 * * *
|
||
DELETE_AFTER=10
|
||
KEEP_LAST=10
|
||
TZ=Asia/Novosibirsk
|
||
SQLITE_TIMEOUT=60000
|
||
BACKUP_RETRIES=5
|
||
BACKUP_RETRY_DELAY=30
|
||
MINIO_PATH=myminio://sqlite/master/
|
||
MINIO_ACCOUNT_ID=account
|
||
MINIO_APPLICATION_KEY=key
|
||
MINIO_ENDPOINT=https://s3.domain.ru
|
||
MINIO_LOCATION=ru-nsk
|
||
|
||
# необязательно: пинг в healthchecks.io (в т.ч. self-hosted)
|
||
HEALTHCHECK_URL=https://hc.domain.ru/ping
|
||
HEALTHCHECK_UUID=00000000-0000-0000-0000-000000000000
|
||
```
|
||
|
||
1. myminio - системно! не менять
|
||
2. проверьте настройки локации, не должно быть пусто
|
||
3. DELETE_AFTER - в днях
|
||
4. KEEP_LAST - минимальное число копий, которые сохраняются всегда
|
||
|
||
## Ротация
|
||
|
||
Удаление старых бекапов работает по двум условиям одновременно:
|
||
|
||
1. бекап старше `DELETE_AFTER` дней
|
||
2. и при этом не входит в `KEEP_LAST` самых свежих копий
|
||
|
||
То есть последние `KEEP_LAST` бекапов не удаляются никогда, даже если они старше `DELETE_AFTER` дней. Это защищает от потери всех копий, если контейнер долго стоял и новые бекапы не создавались.
|
||
|
||
`KEEP_LAST` по умолчанию `10`, если переменная не задана.
|
||
|
||
## Заблокированная база
|
||
|
||
Если базу пишет живой процесс (grafana, или не добитый старый под), `sqlite3 .backup` падает с `Error: database is locked`.
|
||
|
||
Бекап это переживает: соединение открывается с `.timeout SQLITE_TIMEOUT` (мс) — sqlite ждёт освобождения блокировки вместо мгновенной ошибки. Если за это время писатель не отпустил базу, попытка повторяется `BACKUP_RETRIES` раз с паузой `BACKUP_RETRY_DELAY` секунд.
|
||
|
||
| Переменная | Default | Что делает |
|
||
|---|---|---|
|
||
| `SQLITE_TIMEOUT` | `60000` | сколько мс ждать блокировку в рамках одной попытки |
|
||
| `BACKUP_RETRIES` | `5` | сколько попыток снять дамп |
|
||
| `BACKUP_RETRY_DELAY` | `30` | пауза между попытками, сек |
|
||
|
||
Максимальное время ожидания с дефолтами: `60s * 5 + 30s * 4 = 7 минут`. После этого — `exit 1` и пинг `/fail`.
|
||
|
||
Если база залочена *постоянно* — это не про бекап, а про то, что старый под не умер. Ретраи только прикрывают короткие окна.
|
||
|
||
## Healthchecks
|
||
|
||
Если `HEALTHCHECK_URL` пуст — пинги не отправляются, бекап работает как обычно.
|
||
|
||
Итоговый адрес: `${HEALTHCHECK_URL}/${HEALTHCHECK_UUID}`
|
||
|
||
| Момент | Запрос |
|
||
|---|---|
|
||
| старт бекапа | `.../${HEALTHCHECK_UUID}/start` |
|
||
| дамп снят, проверен `PRAGMA integrity_check`, запакован, залит в s3, размер в s3 сверен с локальным, старое удалено | `.../${HEALTHCHECK_UUID}` |
|
||
| любая ошибка на любом шаге | `.../${HEALTHCHECK_UUID}/fail` |
|
||
|
||
Для self-hosted инстанса `HEALTHCHECK_URL` — это адрес его ping-эндпоинта, например `https://hc.domain.ru/ping`.
|
||
|
||
## Планировщик
|
||
|
||
`crond` из busybox не используется: под non-root uid (обычная ситуация в
|
||
kubernetes с `securityContext.runAsUser`) он не загружает ни одного crontab и
|
||
не сообщает об этом — в логе бесконечно идёт `wakeup dt=60`, а бекапов нет.
|
||
Вместо него `entrypoint.sh` содержит свой планировщик на sh.
|
||
|
||
Что это даёт:
|
||
|
||
* работает под любым uid;
|
||
* окружение наследуется напрямую, без выгрузки env в файл;
|
||
* бекапы никогда не накладываются друг на друга — следующий запуск возможен
|
||
только после завершения предыдущего;
|
||
* некорректный `CRONTAB` роняет контейнер на старте с внятным сообщением
|
||
(`[scheduler] FATAL: minute: '60' is outside 0-59`), а не приводит к тихому
|
||
простою.
|
||
|
||
Синтаксис `CRONTAB` — стандартные пять полей: `*`, `5`, `1,15`, `2-6`, `*/15`,
|
||
`8-18/2`, `30/5`. В месяцах и днях недели понимаются имена (`jan`, `mon`).
|
||
Воскресенье — и `0`, и `7`. Если ограничены одновременно день месяца и день
|
||
недели, запуск происходит при совпадении любого из них — как в `crontab(5)`.
|
||
|
||
При старте контейнера бекап делается сразу, эта минута считается отработанной.
|
||
|
||
Расписание видно в логе при запуске:
|
||
|
||
```
|
||
[scheduler] schedule: 30 07 * * * (TZ=Asia/Novosibirsk, now 2026-09-02 16:45:21 +0700)
|
||
[scheduler] expanded: min=[30] hour=[7] dom=[1 2 3 ...] mon=[1 2 ...] dow=[0 1 ...] day-match=and
|
||
```
|
||
|
||
Ручной запуск: `docker exec ... /scripts/backup.sh` — использует то окружение,
|
||
с которым его вызвали.
|
||
|
||
Период опроса часов — 20 секунд, меняется через `POLL_INTERVAL` (секунды).
|