Русский
Сеть 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-гарантия для прода.