Хранение данных

Сборка мусора

Как хранилище узла избавляется от устаревших данных — три независимых фоновых процесса для архивов, блоков и состояний шардов, у каждого своя граница отсечения и свой момент срабатывания, ручной запуск через управляющий сокет и защита блоков и состояний, ещё нужных для сборки устойчивых состояний, от преждевременного удаления.

Хранилище узла постоянно растёт: с каждым мастер-блоком копятся архивы и данные блоков в blob-хранилище, в базе остаются дескрипторы блоков, в базе ячеек остаются состояния шардов. Эти запасы не могут расти бесконечно: сборка мусора (GC) следит за их отсечением. Это три независимых процесса, каждый для своего вида данных: архивов, блоков и состояний шардов. У устойчивых состояний на диске собственной сборки мусора нет — ротация, разобранная в статье об устойчивых состояниях, убирает старые поколения.

Общая для всех трёх идея — граница отсечения: место в истории, до которого данные ещё нужны, а после которого их можно стереть. У каждого вида данных эта граница считается по-своему.

Три независимых процесса

Каждый из трёх процессов включается собственной секцией конфигурации хранилища: archives_gc, blocks_gc, states_gc. Секция со значением null отключает соответствующий процесс целиком: задача для него не создаётся, а данные этого вида не чистятся вовсе. Секции независимы друг от друга — можно, например, чистить архивы и блоки, но хранить все состояния шардов без ограничения, или наоборот.

Между процессами нет ни общей блокировки, ни очерёдности: ничто не мешает чистке архивов, блоков и состояний идти одновременно. Сборка мусора живёт ровно столько же, сколько само хранилище узла — как только отпущена последняя ссылка на него, все три фоновые задачи останавливаются.

Тик сборщика мусора — общий сигнал, который запускает плановую работу сборщиков блоков и состояний. Он несёт seqno последнего применённого мастер-блока и seqno последнего известного ключевого блока: сборщик блоков считает от первого значения безопасную дистанцию, сборщик состояний берёт его как есть, без отступа. Сборщик архивов в плановой работе не читает эти сведения вовсе: он опирается на устойчивое состояние и системные часы (см. ниже).

Ручной запуск

Кроме плановой работы, каждый из трёх процессов можно запустить прямо сейчас — ручным триггером сборки мусора, отдельной командой на каждый вид данных: tycho node gc-archives, gc-blocks, gc-states. У команды один из двух взаимоисключающих флагов: --seqno задаёт точный seqno мастер-блока как границу, --distance задаёт отступ от последнего известного мастер-блока. Отступ, уводящий границу ниже нуля, просто даёт нулевую границу. Это безопасно: сборщики блоков и состояний явно пропускают нулевую цель, ничего не удаляя.

Команда идёт по управляющему сокету узла в персональный канал соответствующего процесса и не ждёт результата: ответ приходит сразу же, а сама чистка идёт фоном. Отсюда и оборотная сторона: триггер, который процесс не успел прочитать, теряется — второй ручной запуск, отправленный до того, как задача проснулась, просто перетирает первый. Общей команды «почистить всё» нет: у каждого вида данных своя команда и свой канал.

Ручной проход отличается от планового у каждого процесса по-своему: сборщик архивов идёт мимо ожидания нового устойчивого состояния, сборщик блоков — мимо своей паузы между запусками, сборщик состояний — мимо интервала между проходами. Дальше эти отличия разобраны подробно для каждого вида данных, начиная с архивов.

Сборка мусора архивов

Хранилище чистит архивы не по расписанию и не по высоте цепочки, а по появлению нового устойчивого состояния. Устойчивое состояние архивный сборщик мусора учитывает как пригодное для загрузки другими узлами через 12 часов после генерации блока — это отсрочка пригодности для загрузки, уже разобранная в статье об устойчивых состояниях. Для расчёта сборщика архивов эта величина всегда равна 12 часам: он берёт константу BOOT_OFFSET, а не настройку. Именно по истечении этой отсрочки состояние становится точкой старта — и с этого момента раздающая сторона обязана хранить архивы после него: без них узлу, стартующему с этого состояния, будет нечем догонять историю сети, а сама обязанность длится, пока состояние вообще может быть выбрано точкой старта.

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

Дождавшись срока, сборщик удаляет архивы, целиком лежащие до границы, заданной seqno этого устойчивого состояния. Архив, в который попадает граница, и всё, что после него, сохраняется. Ошибка удаления не останавливает круг — она только попадает в журнал, а сборщик возвращается к ожиданию следующего устойчивого состояния.

Ручной запуск для архивов срабатывает мимо этого ожидания: он не дожидается появления нового устойчивого состояния.

Сборка мусора блоков

Тип секции blocks_gc определяет один из трёх режимов сборки мусора блоков. По умолчанию действует BeforeSafeDistance: он держит отступ от вершины цепочки, который пересчитывается на каждом проходе, а не остаётся постоянным. Два других режима отступа не считают вовсе — они реагируют на появление нового ключевого блока и режут историю по самим ключевым блокам.

Безопасная дистанция

В режиме BeforeSafeDistance граница — это seqno последнего мастер-блока минус безопасная дистанция. Сама дистанция считается как максимум сразу из трёх величин: настройки safe_distance, которую задаёт оператор, жёсткого минимума в 100 блоков и длины хвоста дифов очереди (плюс один), взятой из кэша длин хвостов дифов для текущего мастер-блока. Жёсткий минимум страхует от того, что сборщик сотрёт блоки, ещё нужные незакрытому архиву, а хвост дифов — от того, что сотрутся блоки, чьи дифы очереди нужны для сборки устойчивого состояния очереди. Уменьшение safe_distance в конфигурации ниже жёсткого минимума поэтому ни на что не влияет.

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

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

Реакция на новые ключевые блоки

Два других режима, BeforePreviousKeyBlock и BeforePreviousPersistentState, устроены иначе: у них нет ни дистанции, ни паузы между запусками, ни плановых проходов между ключевыми блоками. Они просто ждут появления нового ключевого блока и без него круг вообще не выполняют.

Дождавшись, оба ищут границу от seqno последнего ключевого блока: BeforePreviousKeyBlock берёт предыдущий ключевой блок, BeforePreviousPersistentState — предыдущий ключевой блок, для которого уже сохранено устойчивое состояние. Если такого блока в индексе ключевых блоков нет, сборщик пишет предупреждение и пропускает круг.

Оба режима держат на диске заведомо больше данных, чем безопасная дистанция. Режим по устойчивому состоянию гарантирует, что узел хранит блоки как минимум от предпоследнего устойчивого ключевого блока и может раздавать их догоняющим узлам. Плата за это — рваная нагрузка: удаление происходит редко и сразу большими порциями, ровно на ключевых блоках.

Защита от преждевременного удаления

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

Кроме того, пока узел собирает и сохраняет набор устойчивых состояний, он временно защищает от удаления сами эти блоки. Топ-блоки шардов и мастерчейна защищены от собственной сборки мусора состояний. Те же блоки вместе с цепочкой предыдущих блоков, чьи дифы очереди войдут в устойчивое состояние очереди, защищены и от сборки мусора блоков. Устройство самой защиты, пары флагов «начата» и «завершена», разобрано в статье о дескрипторах блоков: она снимается только после того, как сохранение подтверждённо завершилось.

Сборка мусора состояний

Сборщик чистит состояния шардов агрессивнее всего: никакого запаса по высоте у него нет вовсе. Целью планового прохода служит seqno самого свежего мастер-блока — не минус что-либо. Само хранилище состояний уже решает, насколько ниже этой цели в итоге пройдёт удаление: оно опускает границу до минимального мастер-блока, на который ссылается целевой, а пропускает блоки, чьё состояние защищено флагом от собственной сборки мусора.

Единственный тормоз здесь — параметр interval между запусками, и он ограничивает только частоту стартов, а не сам факт удаления. Первый проход после старта узла может быть дополнительно отложен на случайную величину в пределах интервала, если оператор включил эту настройку отдельно, и только один раз. Ручной запуск полностью игнорирует интервал и выполняется немедленно.

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

Конфигурация

Сборка мусора настраивается тремя независимыми опциональными секциями конфигурации хранилища узла: archives_gc, blocks_gc, states_gc. Секция со значением null отключает соответствующий процесс целиком, а поля, не заданные внутри секции, берутся по умолчанию.

Значения по умолчанию секции archives_gc:

ПараметрПо умолчаниюЧто задаёт
persistent_state_offset"300s"добавочная выдержка сверх отсрочки пригодности для загрузки, прежде чем удалять архивы до устойчивого состояния

Значения по умолчанию секции blocks_gc:

ПараметрПо умолчаниюЧто задаёт
typeBeforeSafeDistanceрежим выбора границы: BeforeSafeDistance, BeforePreviousKeyBlock или BeforePreviousPersistentState
safe_distance1000(только при type: BeforeSafeDistance) отступ от seqno последнего мастер-блока
min_interval"60s"(только при type: BeforeSafeDistance) минимальный промежуток между плановыми запусками
enable_for_synctrueвключать ли сборку мусора блоков во время синхронизации узла: в версии 0.3.11 это значение нигде не читается
max_blocks_per_batch100000предел числа записей в одном пакете удаления, null — без ограничения

Значения по умолчанию секции states_gc:

ПараметрПо умолчаниюЧто задаёт
interval"1s"минимальный промежуток между плановыми запусками
random_offsetfalseдобавлять ли случайную задержку перед самым первым проходом после старта узла

Состояние узла

NodeStateStorage — шесть фиксированных значений в одной колонке базы CoreDb: последний и начальный мастер-блок, идентификатор и доказательство нулевого состояния, идентификатор экземпляра хранилища, отметка о незавершённом сохранении устойчивого состояния. Решение о холодном или тёплом старте, состояние синхронизации узла и наблюдение за последним мастер-блоком через канал уведомлений.

Объектное хранилище

Клиент чтения S3-совместимого объектного хранилища (feature `s3`) — дополнительный источник архивов блоков и устойчивых состояний рядом с сетью, без единого вызова записи: бакет наполняет процесс, внешний по отношению к узлу. Адресация объектов повторяет локальные имена файлов, скачивание идёт чанками с проверкой целостности и повторами, а сам клиент, создаваемый один раз, разделяется между холодным стартом, догоняющей синхронизацией архивами и раздачей данных по сети: в первых двух через гибридного клиента, который гоняет источники наперегонки, в третьем как простой запасной вариант.