Акция Панель управления ispmanager для VDS — первый месяц бесплатно
до 31.07.2026 Подробнее
Выберите продукт

SQLite на VDS: WAL, busy_timeout, бэкапы и практика для PHP и Node.js

Как выжать максимум из SQLite на VDS: когда включать WAL, какое значение выбрать для busy_timeout, как строить короткие транзакции и чекпоинты, какие PRAGMA действительно полезны, как делать горячие бэкапы (включая Litestream) и проверять восстановление. Примеры для PHP и Node.js.
SQLite на VDS: WAL, busy_timeout, бэкапы и практика для PHP и Node.js

SQLite нередко недооценивают на продакшене, хотя для множества задач он идеален: встроенные CMS/панели, кэши, журналы событий, очереди, аналитика на один узел, edge‑функции. На VDS SQLite даёт стабильную предсказуемую производительность при минимальном операционном оверхеде — если правильно включить WAL, выставить busy_timeout, спроектировать транзакции и продумать бэкапы. Ниже — концентрат практики, проверенный в бою.

Когда SQLite на VDS — это хорошая идея

SQLite отлично подходит там, где:

  • записи короткие, а транзакции быстрые (миллисекунды);
  • большинство операций — чтение, а запись — всплесками;
  • нет требования к горизонтальному шардингу/репликации на уровне СУБД;
  • важна простота эксплуатации: один файл данных, нулевой DevOps‑сложности.

Но важно помнить, что одновременно писать может только один процесс (или одна транзакция) — это архитектурный предел SQLite. Режим WAL снимает конфликт между многими читателями и одним писателем, однако несколько писателей по‑прежнему конфликтуют.

Если вам не нужен root‑доступ и нагрузка невысокая, SQLite отлично живёт и на качественном виртуальном хостинге.

WAL: что это и почему он почти всегда обязателен

В режимах журналирования по умолчанию (ROLLBACK‑journal) читатели и писатели мешают друг другу. WAL (Write‑Ahead Logging) меняет порядок: запись уходит в WAL‑файл, читатели продолжают читать старые страницы без ожидания, а чекпоинт переносит изменения обратно в основной файл.

Плюсы WAL на VDS:

  • чтения не блокируют запись и наоборот (при условии коротких транзакций);
  • стабильнее латентность, меньше «ступенек» под нагрузкой;
  • улучшенная устойчивость к сбоям при корректной настройке synchronous.

Базовые команды и проверки:

sqlite3 app.db "PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=1000; PRAGMA synchronous=NORMAL;"

Комментарии к настройкам:

  • journal_mode=WAL — включает WAL‑журналирование;
  • wal_autocheckpoint=1000 — инициировать чекпоинт примерно каждые 1000 страниц WAL (подбирайте экспериментально: если WAL растёт слишком быстро — уменьшайте, если чекпоинты мешают — увеличивайте);
  • synchronous=NORMAL — хороший баланс производительности и надёжности для большинства приложений на VDS. FULL даёт максимум прочности, но дороже по I/O.

busy_timeout: спасение от «database is locked»

busy_timeout — это окно ожидания (в миллисекундах), в течение которого SQLite будет повторять попытку взять блокировку вместо мгновенной ошибки database is locked. По умолчанию 0 (не ждать).

Практические рекомендации:

  • Для веб‑приложений ставьте 500–3000 мс: достаточно, чтобы переждать чужую короткую запись.
  • Комбинируйте с транзакциями BEGIN IMMEDIATE для записей, чтобы сразу занять write‑lock и избежать поздних сюрпризов.
  • Следите за метриками: если часто упираетесь в timeout, значит транзакции слишком длинные или писателей слишком много.

PHP (PDO SQLite) — включаем WAL и busy_timeout

<?php
$dsn = 'sqlite:' . __DIR__ . '/app.db';
$db = new PDO($dsn, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// Базовые PRAGMA на каждый коннект
$db->exec('PRAGMA journal_mode=WAL');
$db->exec('PRAGMA synchronous=NORMAL');
$db->exec('PRAGMA wal_autocheckpoint=1000');
$db->exec('PRAGMA busy_timeout=2000'); // 2 секунды

// Запись с немедленной блокировкой на запись
$db->beginTransaction();
$db->exec('BEGIN IMMEDIATE');
$stmt = $db->prepare('INSERT INTO events(ts, payload) VALUES(strftime("%s","now"), :p)');
$stmt->execute([':p' => json_encode(['ok' => true])]);
$db->commit();

Node.js — better-sqlite3 и sqlite3

Для Node.js чаще всего используют better-sqlite3 (синхронный, быстрый для коротких запросов) или классический sqlite3 (асинхронный). В обоих случаях включаем WAL и busy_timeout.

// better-sqlite3
const Database = require('better-sqlite3');
const db = new Database('app.db', { fileMustExist: false });

// Настройка PRAGMA на старте процесса
['PRAGMA journal_mode=WAL',
 'PRAGMA synchronous=NORMAL',
 'PRAGMA wal_autocheckpoint=1000',
 'PRAGMA busy_timeout=2000']
.forEach(sql => db.pragma(sql.replace('PRAGMA ', '')));

// Паттерн короткой записи
const insert = db.prepare('INSERT INTO events(ts, payload) VALUES(strftime("%s","now"), ?)');
const write = (payload) => {
  const tx = db.transaction((p) => {
    insert.run(JSON.stringify(p));
  });
  tx(payload);
};

Если используется sqlite3 (asynchronous), задайте busyTimeout на уровне подключения:

const sqlite3 = require('sqlite3');
const { open } = require('sqlite');

(async () => {
  const db = await open({
    filename: 'app.db',
    driver: sqlite3.Database
  });
  await db.exec('PRAGMA journal_mode=WAL');
  await db.exec('PRAGMA synchronous=NORMAL');
  await db.exec('PRAGMA wal_autocheckpoint=1000');
  await db.exec('PRAGMA busy_timeout=2000');
})();

Схема WAL и чекпоинта в SQLite

Транзакционные паттерны: один писатель, много читателей

SQLite допускает множество конкурентных читателей, но только одна активная транзакция записи. Поэтому главный принцип — удерживать write‑lock как можно меньше:

  • Готовьте данные заранее (в памяти), пишите быстро, атомарно;
  • Используйте BEGIN IMMEDIATE, чтобы сразу занять write‑lock и не тратить время посреди транзакции;
  • Избегайте долгих SELECT перед записью в той же транзакции — разделяйте чтение и запись;
  • Делайте батчи: лучше одна короткая транзакция с 100 вставками, чем 100 отдельных транзакций.

Если у вас несколько воркеров, пишущих одновременно, добавьте «гейт» (канал) на уровне приложения: очередь на один писатель. Для Node.js это может быть простой in‑process очередь; для PHP — отдельный процесс‑воркер, принимающий задания через очередь/IPC. Да, это доп.слой, но он окупается стабильностью.

Файловая система и VDS: что важно для производительности и durability

На VDS в 2025 году чаще встречаются ext4 и XFS. Оба подходят для SQLite, но есть нюансы:

  • ext4: по умолчанию data=ordered, хорошие гарантии порядка записи. Отличный выбор для баз небольшого и среднего размера.
  • XFS: стабилен под параллельной нагрузкой и на больших файлах; отлично работает на NVMe‑хранилище.

Рекомендации:

  • Не храните базу на сетевых FS (NFS/SMB) — риск неожиданных блокировок и потери гарантий fsync. Держите базу на локальном блочном устройстве VDS.
  • Проверьте правильность времени и часового пояса (chrony/systemd‑timesyncd), чтобы не сбить расписания бэкапов и TTL WAL‑логики.
  • Увеличьте лимит дескрипторов для процессов, активно работающих с SQLite (и бэкап‑агентов).
# временно в shell
ulimit -n 65536

# постоянный лимит через systemd для вашего сервиса
# /etc/systemd/system/app.service
[Service]
LimitNOFILE=65536

Если подбираете ресурсы сервера под SQLite, пригодится материал про выбор CPU и RAM для VDS под нагрузку. Для повышения надёжности юнитов посмотрите и усиление сервисов systemd на VDS.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Контроль роста WAL и чекпоинты

Если WAL растёт без сдерживания, значит чекпоинты происходят редко или долго. Проверьте автопорог и инициируйте ручной чекпоинт в кроне или системном таймере.

sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"

TRUNCATE после чекпоинта обрезает WAL‑файл (если это допустимо). Для интенсивной записи вместо агрессивного TRUNCATE оставьте PASSIVE или RESTART и увеличьте автопорог, чтобы реже дергать диск.

Производительность: ключевые PRAGMA

  • journal_mode=WAL — базис.
  • synchronous=NORMAL — компромисс между IOPS и гарантией.
  • wal_autocheckpoint — подберите экспериментально под ваш профиль.
  • temp_store=MEMORY — меньше временных файлов, если хватает RAM.
  • mmap_size — может ускорить чтение больших таблиц, но тестируйте.
  • cache_size — отрицательное значение задаёт размер в КБ; полезно ограничить чтобы не выедать память.
sqlite3 app.db "PRAGMA temp_store=MEMORY; PRAGMA mmap_size=268435456; PRAGMA cache_size=-131072;"

Не переусердствуйте с кэшами: SQLite умеет эффективно работать и на умеренных значениях, а борьба за RAM на VDS иногда дороже выигрыша в латентности.

Durability: что реально сохраняется при сбоях

synchronous=FULL максимально жёстко синхронизирует журнал и базовый файл; NORMAL упрощает часть fsync и почти всегда безопасен при использовании WAL на корректных дисках. Реальная устойчивость зависит от стека: файловая система, драйвер диска, контроллер, виртуализация. Проверьте поведение на стенде с искусственными сбоями (отключение питания VM, kill -9 процесса) и убедитесь, что вы не теряете подтверждённые транзакции.

Если у вас SLA уровня критичных финансовых систем — рассмотрите внешнюю СУБД с репликацией. SQLite создан как встраиваемая БД и не стремится заменить кластерные решения.

Онлайн-бэкапы: .backup, VACUUM INTO, pg‑style «горячее копирование»

Для горячего бэкапа SQLite предусмотрен собственный API, доступный через CLI:

# Горячая копия без остановки сервиса
sqlite3 app.db ".backup 'backup/app-$(date +%F).db'"

# Начиная с 3.27: VACUUM INTO создаёт компактную копию
sqlite3 app.db "VACUUM INTO 'backup/app-compact.db'"

Это безопасно даже в WAL‑режиме: механизм использует внутренний backup‑API и не ломает активные транзакции. Имейте в виду, что VACUUM INTO сильнее нагружает диск, зато дефрагментирует файл.

Потоковая репликация WAL: Litestream

Litestream — агент, который отслеживает WAL и отправляет сегменты в удалённое хранилище (обычно S3‑совместимое). Это даёт точку восстановления во времени (PITR) и быструю перезаливку баз на новый сервер VDS при миграции или аварии.

Минимальный конфиг Litestream (пример):

# /etc/litestream.yml
dbs:
  - path: /srv/app/app.db
    replicas:
      - url: s3://my-bucket/sqlite/app
        endpoint: s3.example.internal
        access-key-id: MYKEY
        secret-access-key: MYSECRET
        retention: 30d

Сервис systemd:

# /etc/systemd/system/litestream.service
[Unit]
Description=Litestream WAL shipping
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/litestream replicate
Restart=always
RestartSec=2s
User=litestream
Group=litestream
UMask=007
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Стратегия восстановления:

  1. Остановить приложение, убедиться, что локальный процесс не держит файл.
  2. Восстановить базу из последнего snapshot и WAL‑журнала на нужную точку времени.
  3. Запустить приложение, проверить целостность (PRAGMA integrity_check).
# Пример проверки целостности
sqlite3 app.db "PRAGMA integrity_check;"

При использовании Litestream обязательно резервируйте и сам файл базы, и снапшоты, и WAL‑сегменты. Проверьте восстановление на отдельной ВМ: «бэкап считается сделанным только после тестового restore».

Потоковая репликация WAL через Litestream

Обслуживание: VACUUM, ANALYZE, индексы

Регулярные операции ускоряют запросы и держат размер базы под контролем:

  • ANALYZE — обновляет статистику планировщика;
  • VACUUM — дефрагментация и переупаковка файла (дорогая операция);
  • PRAGMA optimize — мягкая оптимизация на базе статистики.
sqlite3 app.db "ANALYZE; PRAGMA optimize;"

Если у вас много удалений, подумайте о PRAGMA incremental_vacuum и рациональной структуре индексов. Число индексов напрямую влияет на стоимость записи: каждая вставка обновляет все соответствующие индексы.

Наблюдаемость: что мерить и как понимать симптомы

Признаки перегрузки писателя:

  • рост времени ответа на INSERT/UPDATE;
  • участившиеся таймауты busy_timeout;
  • заметный рост WAL с редкими чекпоинтами.

Команды диагностики:

sqlite3 app.db ".dbinfo"
sqlite3 app.db "PRAGMA wal_checkpoint(PASSIVE);"
sqlite3 app.db "PRAGMA page_size; PRAGMA page_count;"

Собирайте метрики на уровне приложения: число попыток записи, доля таймаутов/ретраев, размер базы, размер WAL. Журналы с замером времени транзакций резко упрощают поиск «долгих» мест.

Безопасность и права

Права на файлы базы и WAL должны позволять запись только сервисному пользователю приложения. Хорошая практика — выделенный системный пользователь/группа и umask 007 для исключения лишнего чтения. Бэкапы храните в отдельном каталоге с ограниченными правами. Если нужна защита «на покое», используйте шифрование на уровне диска/тома средствами ОС.

Частые ошибки и как их избежать

  • Размещение базы на NFS/SMB: нестабильные блокировки и потеря гарантии flush — переносите на локальный диск.
  • Длинные транзакции записи: дробите на батчи, поднимите busy_timeout, используйте BEGIN IMMEDIATE.
  • Отсутствие регулярных чекпоинтов: следите за ростом WAL и инициируйте wal_checkpoint.
  • Отсутствие восстановимых бэкапов: внедрите .backup/VACUUM INTO и потоковую репликацию WAL, делайте тестовые restore.
  • Грубые PRAGMA «на глаз»: каждую настройку проверяйте бенчмарком под своей нагрузкой.

Мини‑чек‑лист для продакшена на VDS

  • Включить journal_mode=WAL, выставить synchronous=NORMAL или FULL под SLA.
  • Настроить busy_timeout 500–3000 мс, писать через BEGIN IMMEDIATE, батчировать.
  • Подобрать wal_autocheckpoint, поставить периодический чекпоинт.
  • Вести горячие бэкапы (.backup или VACUUM INTO) и/или Litestream; регулярно проверять восстановление.
  • Держать базу на локальном ext4/XFS, поднять LimitNOFILE у сервиса.
  • Мониторить латентность транзакций, размер WAL и частоту таймаутов.

Пример «с нуля»: инициализация базы и миграции

# Создать базу, включить WAL и базовые настройки
sqlite3 app.db "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA wal_autocheckpoint=1000;"

# Создать таблицы
sqlite3 app.db "CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY, ts INTEGER, payload TEXT);"

# Проверка
sqlite3 app.db ".tables"

# Тестовая вставка
sqlite3 app.db "INSERT INTO events(ts, payload) VALUES(strftime('%s','now'), '{\"hello\":true}');"

# Чтение
sqlite3 app.db "SELECT count(*), min(ts), max(ts) FROM events;"

Дальше подключайте приложение (PHP/Node.js), не забывая выполнять PRAGMA на соединении. Для миграций удобно хранить версионированные SQL‑файлы и вести таблицу migrations с применёнными версиями; миграции выполняйте в одной транзакции с BEGIN IMMEDIATE.

Итого

SQLite на VDS — это надёжно и быстро, когда спроектированы короткие записи, включён WAL, выставлен разумный busy_timeout, а бэкапы и чекпоинты идут по расписанию. Добавьте дисциплину транзакций, мониторинг и регулярное тестирование восстановления — и получите лёгкую в эксплуатации БД с очень низкой стоимостью владения. Для PHP и Node.js настройки просты, а выигрыш в предсказуемости — ощутим с первого дня.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...