Устойчивые состояния
Предыдущие статьи раздела показывали, как узел хранит и обновляет текущее состояние рабочей цепочки: дерево ячеек, непрерывно меняющееся вслед за новыми блоками. Рядом с ним узел ведёт устойчивые состояния: замороженные снимки сети на отдельных ключевых блоках. Нужны они не самому узлу, а тому, кто проходит холодный старт — впервые запускается в сети или начинает заново после потери локального состояния. Такому узлу незачем проигрывать всю историю от нулевого состояния: он стартует с уже готового снимка и доигрывает лишь то, что случилось после него.
За устойчивые состояния отвечает PersistentStateStorage — компонент общего хранилища узла, наравне с хранилищами блоков, дескрипторов, связей, текущих состояний шардов и состояния узла. Он единственный среди них, чьи данные целиком живут в файловой системе, а не в RocksDB. Собственных колонок у него нет вовсе. Базу ячеек он получает только на чтение и по ней же собирает файл состояния шарда.
Каждое устойчивое состояние — отдельный файл на диске.
Два вида и адресация
Состояние шарда — дерево ячеек в формате BOC, файл с расширением .boc. Рядом хранится устойчивое состояние очереди: снимок исходящей очереди внутренних сообщений того же блока, файл .queue. У каждого вида собственные писатель и читатель формата, но оба раздаются и хранятся одинаково: как непрозрачные сжатые файлы, адресуемые парой «идентификатор блока + вид».
Имя файла целиком строится из идентификатора блока: <идентификатор блока>.boc или <идентификатор блока>.queue. По этому же расширению хранилище узнаёт вид файла, сканируя диск.
Файлы разложены по каталогам-поколениям: states/<mc_seqno>, где mc_seqno — номер мастер-блока, ради которого собран весь набор. В одном каталоге лежат файлы всех шардов сразу: состояние мастерчейна и состояния верхних блоков каждого шарда, по два файла на блок.
Номер поколения при этом не входит в адрес самого состояния: оно по-прежнему адресуется идентификатором блока и видом, а каталог — лишь метка того, ради какого мастер-блока набор собирался. Поэтому один и тот же файл состояния может физически лежать сразу в нескольких каталогах-поколениях одновременно.
Критерий устойчивости
Устойчивым становится не каждый ключевой блок. Признак простой: время генерации блока делится на интервалы длиной около 36 часов, и устойчивым признаётся первый ключевой блок, попавший в новый такой интервал. У такого блока сдвинутое на 17 бит время генерации отличается от того же значения у предыдущего ключевого блока.
Устойчивый ключевой блок обнаруживает и запускает его сохранение применитель состояний — тот же компонент модуля продвижения по блокам, что применяет к состоянию каждого шарда очередной новый блок. На устойчивом ключевом блоке он поднимает одну фоновую задачу сразу на все шарды.
Состояния шардов и состояния их очередей собираются двумя параллельными потоками работы. Внутри каждого потока порядок фиксирован: сначала все шарды и только потом мастерчейн. Так файлы мастерчейна не появляются в каталоге поколения раньше, чем готовы одноимённые файлы всех шардов. Такой порядок гарантирован только для набора, записанного применителем состояний в штатной работе узла: при холодном старте тот же каталог наполняется в обратном порядке — сначала файлы мастерчейна, затем шардов. В штатном случае появление .boc-файла мастерчейна сигнализирует, что состояния всех шардов набора готовы. С .queue-файлом та же логика: он появляется, когда готовы их очереди. По-настоящему полным такой набор становится только тогда, когда в каталоге есть оба файла мастерчейна. Одновременно выполняется не больше одной такой задачи: следующая дожидается завершения предыдущей.
Сбор устойчивых состояний можно отключить: конструктор without_persistent_states переводит применитель в режим, в котором он не запускает их сохранение при обычной работе узла. Признак задаётся при конструировании компонента, а не через таблицу конфигурации ниже, поэтому в этой таблице его искать не стоит. Он гасит только новые сохранения: если отметка о начатом сохранении осталась незавершённой, узел всё равно доведёт его до конца при следующем запуске — независимо от этого признака. От раздачи он не зависит: она отключается отдельно, флагом serve_persistent_states (см. раздел «Раздача файлов»).
Сохранение файла
Файл состояния шарда собирается из самой базы ячеек. Писатели обоих видов упаковывают собранные данные и на лету сжимают их потоком zstd с уровнем сжатия 9. Так они и лежат на диске: принимающая сторона распаковывает их уже при скачивании.
Второй способ применяется к уже готовому файлу — тому, что узел получил при собственном холодном старте, скачав его у другого узла. Такой файл приходит уже распакованным: его лишь сжимают и переименовывают в целевое имя. Разбор содержимого и обращение к базе здесь не нужны: файл считается уже проверенным.
Перед тем как запускать любой из этих путей, хранилище проверяет, не сохранено ли состояние этого же блока уже в каком-нибудь поколении. Если да, файл не пересобирается. Когда поколение уже закешированной копии не старше запрошенного, работа на этом заканчивается вовсе: нужное состояние уже доступно и копировать ничего не нужно, даже если сама копия физически лежит не в запрошенном, а в более новом поколении. Если же запрошенное поколение новее, уже имеющаяся копия дублируется в каталог нового поколения. Экономия существенная: пересборка состояния шарда — это полный обход дерева ячеек в базе, и повторять его для одного и того же блока на каждом следующем устойчивом мастер-блоке не нужно.
Обратная сторона такого переиспользования: одни и те же байты состояния физически хранятся на диске в каждом живом поколении. Только последующая ротация освобождает место.
Отсрочка пригодности для загрузки
Свежесохранённое устойчивое состояние ещё не пригодно для выбора при старте нового узла. При конфигурации по умолчанию для этого должно пройти не меньше 12 часов с момента генерации блока, а сам порог настраивается отдельно на стороне стартующего узла. Раздачу файла это не ограничивает: раздающая сторона отдаёт его чанки без проверки возраста. Эта отсрочка пригодности для загрузки гарантирует, что новый узел вместе со снимком состояния скачает ещё и некоторый объём истории после него, а не останется с одной лишь неподвижной точкой.
Поэтому при штатном шаге между устойчивыми состояниями на диске узла, который уже некоторое время сам сохраняет состояния, живут три поколения. Самое новое — только что сохранённое, но ещё не пригодное для чужой загрузки. Следующее по свежести уже пригодно для загрузки. А граница проходит ещё на один устойчивый ключевой блок старше: дальше неё ротация всё удаляет целыми каталогами. Если несколько устойчивых ключевых блоков подряд оказались ближе 12 часов друг к другу, поколений на диске остаётся больше. Это не значит, что самое свежее поколение на диске непременно непригодно в любой момент времени. Устойчивые ключевые блоки случаются примерно раз в 36 часов, а при редких ключевых блоках промежуток между ними растягивается ещё сильнее. При таком типичном шаге новейшее сохранённое состояние непригодно лишь примерно треть этого периода. Всё это время узел, стартующий с нуля, получает предыдущее поколение: отставание такого снимка доходит до шага между устойчивыми состояниями плюс 12 часов, то есть почти до 48 часов 25 минут. Остальные две трети времени узел уже получает самый свежий снимок с диска, но это не самый свежий снимок сети: при конфигурации по умолчанию отставание составляет от 12 часов почти до 36 часов 25 минут (одного шага между устойчивыми состояниями). В обоих случаях узел доберёт разницу блоками и архивами.
Та же отсрочка учитывается и при удалении архивов блоков: сборщик мусора не станет удалять архивы, предшествующие устойчивому состоянию, раньше, чем она пройдёт.
Ротация
Как только сохранение нового набора устойчивых состояний завершено, хранилище ищет самое новое поколение, уже пригодное для загрузки, и берёт ещё на один устойчивый ключевой блок назад — эта точка и становится границей отсечения. Всё, что старше границы, удаляется целыми каталогами: ротация не разбирает удаляемые поколения на отдельные файлы. Она никогда не трогает каталог поколения нулевого состояния.
Раздача файлов
Наружу узел раздаёт устойчивые состояния чанками по 1 МиБ. Запрос со смещением, не кратным размеру чанка, отбраковывается сразу, а число параллельных чтений не превышает 20. Запрос сведений о состоянии отвечает всего двумя числами: размером файла, сжатого, как он лежит на диске, и размером чанка. В этом ответе нет ни хеша, ни версии, ни времени создания — целостность скачанного состояния проверяется отдельно, по root hash блока, который скачивающая сторона и так знает из его идентификатора.
Оба вида запроса, сведения о состоянии и чанк, доступны по сети как часть blockchain RPC — отдельной парой методов для состояния шарда и для устойчивого состояния очереди. Раздачу можно выключить на уровне сервиса одним флагом конфигурации: тогда сведения всегда отвечают, что состояния нет, а запрос чанка отклоняется как некорректный.
Между сервисом и хранилищем стоит провайдер данных RPC. Помимо реализации поверх локального хранилища у устойчивых состояний есть и реализация поверх S3, и гибридная, с основным источником и запасным.
Скачивание другим узлом
Скачивающая сторона выбирает источник состояния так: запрос «есть ли оно у вас» уходит сразу десяти надёжным соседям, и первый ответивший «да» становится единственным источником всех последующих чанков. Смешивать чанки от разных соседей незачем: состояние в любом случае проверяется целиком по root hash, и при неудаче проще перекачать его заново у другого соседа. Чанки качаются у выбранного соседа параллельными пачками запросов с повторами при сетевых неудачах, а полученный поток распаковывается zstd уже на стороне скачивающего.
При холодном старте скачанный файл используется дважды. Сначала он идёт в дело: состояние шарда потоково загружается в базу ячеек с проверкой root hash, а состояние очереди передаётся обработчику импорта очереди. Затем тот же файл сохраняется как собственное устойчивое состояние узла, минуя пересборку из базы. Поэтому узел, только что поднявшийся с устойчивого состояния, немедленно способен раздавать это состояние дальше — ещё до того, как соберёт из базы хоть одно состояние сам.
Конфигурация
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
serve_persistent_states | true | раздаёт ли сервис blockchain RPC устойчивые состояния по сети |
Конфигурация блокчейна
Единый onchain-конфиг сети (`BlockchainConfig`) — устройство словаря параметров и типизированный/сырой доступ к нему, физическое место конфига внутри состояния мастерчейна и краткий обзор остальных номеров словаря.
Состояние узла
NodeStateStorage — шесть фиксированных значений в одной колонке базы CoreDb: последний и начальный мастер-блок, идентификатор и доказательство нулевого состояния, идентификатор экземпляра хранилища, отметка о незавершённом сохранении устойчивого состояния. Решение о холодном или тёплом старте, состояние синхронизации узла и наблюдение за последним мастер-блоком через канал уведомлений.