Read in:
Русский

Сеть trip2g на прерываемых нодах

Короткий итог. Сеть знаний trip2g (markdown-вольты на SQLite) можно держать на прерываемых (preemptible) нодах: они в 2-3 раза дешевле и могут исчезнуть в любой момент. В синтетическом тесте сеть это переживает: нода умирает, поднимается на другой машине и восстанавливает свои данные за ~35 секунд, теряя не больше 1 секунды записей. Держит данные не локальный диск, а копия в объектном хранилище (Litestream). Ниже по деталям: что проверял, как это работает и где границы.

Проверял на расходном двухнодовом кластере, настоящий power-off через Hetzner API. Тест синтетический, не прод: он показывает, что механизм держит, а не что потерь не будет.

Что такое прерываемые ноды

Preemptible (spot) ноды провайдер может забрать за секунды, в обмен на цену сильно ниже. Забирает грубо: гипервизор отбирает машину, у тебя SIGTERM и около 30 секунд. Вежливого «дай доделать» нет. Для сети знаний такой хаос допустим при одном условии: каждая нода должна подниматься со своими данными на любой машине.

Сколько это экономит (цифры провайдеров): AWS Spot обычно на 70-90% дешевле on-demand, Google Cloud Spot на 60-91%, Yandex Cloud прерываемые ВМ фиксированно на 50% (и живут не дольше 24 часов). То есть выигрыш от 2x до ~10x в зависимости от провайдера и типа машины. Оговорка: то, что ниже, это симуляция (power-off через API на расходных ВМ); повторить можно хоть на домашнем компе. Реальную экономию даёт именно облачный spot, а тест лишь показывает, что сеть переживёт его поведение.

Тёплый кеш переезжает, данные нет

У Nomad есть ephemeral_disk { sticky = true, migrate = true }. Документация точна: при переезде аллокации тёплый кеш действительно мигрирует на новую ноду. Ловушка в слове «переезд»: это про вежливый случай, а на споте вежливого случая нет.

  • Грейсфул-дренаж (nomad node drain): данные мигрируют. Маркер с ноды 2 появился целым на ноде 1 за 27-34 секунды.
  • Жёсткий power-off (а преемпт это именно он): аллокация уезжает на свежую ноду и поднимается с пустой базой. migrate копирует со старой ноды, а старая нода мертва. Копировать неоткуда.

Почему migrate тут структурно не спасает: чтобы он сработал, шедулеру нужно вежливо сдренить аллокацию, а это минуты. У спота есть SIGTERM и 30 секунд. Не сходится.

Что реально переживает: копия вне ноды

Переживает только копия вне ноды. Litestream стримит SQLite-WAL в объектное хранилище (здесь MinIO) раз в секунду, и на каждом старте аллокация восстанавливается из хранилища до запуска приложения. Тот же жёсткий power-off:

ephemeral_disk{migrate} Litestream в MinIO
Грейсфул-дренаж переживает, ~26 с переживает, 27-34 с
Жёсткий power-off пустой диск, потеря свежая нода, 33-35 с
Окно потерь вся БД, если копии вне ноды нет (с периодическим бэкапом — интервал бэкапа) не больше интервала синка (1 с); в прогоне ~0

База вернулась на ноду, которая её никогда не держала. Приём, который делает это надёжным: восстанавливаться на каждом старте, а не только на свежей ноде. Тогда объектное хранилище становится единственным источником правды при любом размещении.

Конфиг

# prestart: объектное хранилище это источник правды на каждом старте
task "restore" {
  lifecycle { hook = "prestart" }
  # rm -f /data/db.sqlite3* && litestream restore -if-replica-exists -o /data/db.sqlite3 s3://bucket/db
}
# poststart sidecar: стримим WAL раз в секунду
task "replicate" {
  lifecycle { hook = "poststart", sidecar = true }
  # litestream replicate /data/db.sqlite3 s3://bucket/db   (sync-interval: 1s)
}
# job: пусть Nomad бесконечно пересоздаёт и быстро признаёт ноду потерянной
reschedule { unlimited = true }
disconnect { lost_after = "30s", replace = true }

Плюс docker volumes { enabled = true } для host-bind; control-plane и само хранилище держим на стабильной ноде, не на споте.

Где теряет

Граница одна: Litestream реплицирует раз в секунду. Запись, прилетевшая меньше чем за секунду до power-off, теряется; если Litestream упал в том же окне, что и нода, эти WAL-страницы уходят с ней. В прогоне намерил около нуля, но это везение по таймингу, а не гарантия нуля. Нужно теснее: снижай интервал и плати за больше PUT-ов в хранилище.

Между двумя крайностями (migrate без копии = полная потеря, Litestream = не больше 1 секунды) есть середина: периодический off-node бэкап раз в N. Потеря тогда ограничена N (минута, если бэкапить раз в минуту), а не «всё с момента размещения». Минус наивной ротации: хранишь только последние несколько точек и можешь откатиться лишь на ~5 минут назад. Решение — экспоненциальное прореживание (точка минуту назад, час, день, 7 дней) с постоянной ротацией: тугой RPO и глубокая история одновременно. Это отдельная доработка.

Итог

  • Durability на споте живёт вне ноды: WAL в объектное хранилище, restore на каждом старте.
  • ephemeral_disk migrate это тёплый кеш для вежливого переезда, а не durability. На споте вежливого переезда нет.
  • Смерть ноды превращается в 35-секундную пересборку, а не в потерю данных.
  • Окно потерь равно интервалу синка (у нас не больше 1 секунды). Это песочница и экономия в 2-3 раза, не zero-loss-гарантия для прода.