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

Хранилище

Карта раздела Storage — из каких частей состоит хранилище узла Tycho и как они опираются друг на друга.

Узел Tycho должен сохранить и потом отдать обратно всё, из чего состоит цепочка: блоки и собранные из них архивы для синхронизации других узлов, дерево ячеек текущего состояния каждого шарда, снятые на ключевых блоках устойчивые состояния для холодного старта и собственную позицию в цепочке. Всё это не может расти бесконечно, поэтому сборка мусора вычищает устаревшие данные, а рядом с сетью как источником данных узел может читать те же архивы и устойчивые состояния из внешнего объектного хранилища. Хранилище узла отвечает за это.

Хранилище устроено в два слоя. Внизу — общая инфраструктура: контекст хранилища, который управляет окружением RocksDB, реестром открытых баз через обёртку weedb и каталогами файловой системы. У этого слоя нет собственной статьи: он не несёт предметной логики, а только даёт доменным хранилищам общее окружение: настроенные инстансы RocksDB, кэши и работу с файлами.

Сверху расположены доменные хранилища, каждое отвечает за свой вид данных цепочки. Главное из них — CoreStorage: фасад, который открывает поверх контекста хранилища две базы RocksDB, применяет к ним миграции и связывает все входящие в него подсистемы. Рядом с ним работает S3-клиент: клиент чтения внешнего объектного хранилища. Над тем же контекстом хранилища строятся и другие домены узла: внутренняя очередь коллатора и RPC-состояние узла, но это отдельные темы вне этого раздела. Здесь описаны только CoreStorage и S3-клиент.

Устройство на диске

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

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

Устойчивые состояния — третий вид данных на диске: файлы <mc_seqno>/<идентификатор блока>.<вид> в отдельном каталоге, вне RocksDB и вне Cassadilia.

Cassadilia и RocksDB как два движка хранения

У хранилища узла для данных два движка: не всё лежит в RocksDB. Тела блоков и готовые архивы: крупные неизменяемые блобы, доступ к которым преимущественно на чтение. Для них узел использует Cassadilia — собственный CAS-движок Broxus с нулевым усилением записи: блоб пишется один раз и дальше только читается, тогда как LSM-дерево вроде RocksDB переписывает данные при каждой компакции. RocksDB в тракте блоков и архивов остаётся только под метаданные (дескрипторы блоков, журнал событий архивов, временный список блоков собираемого архива): сами байты компонентов туда не попадают.

Обратный случай: ячейки состояний шардов, мелкие значения с произвольным доступом по хешу и изменяемым счётчиком ссылок. Для них выбрана обычная LSM-база RocksDB, отдельный инстанс под базу ячеек, который независим от основной базы. Соседние статьи раздела посвящены устройству самого движка Cassadilia и хранилища ячеек.

Хранилище блоков, архивы и их метаданные

Каждый блок, который узел получает из сети или собирает сам, проходит через хранилище блоков — фасад, который принимает и выдаёт три отдельно хранимых компонента блока: данные, доказательство и диф очереди. Сами байты фасад не хранит: он делегирует их в blob-хранилище над Cassadilia, а собственных RocksDB-таблиц у него нет. Модуль продвижения по блокам, коллатор, blockchain RPC, RPC-сервис узла и control-сервер работают с этими данными через методы хранилища блоков: это общая точка входа.

Синхронизирующемуся узлу невыгодно скачивать историю блок за блоком, поэтому узел заранее упаковывает подряд идущие блоки в архивы: тоже данные в Cassadilia, но в собственном CAS-сторе archives, с отдельным жизненным циклом сборки, коммита и восстановления после рестарта.

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

Хранилище ячеек и обновление состояния шарда

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

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

Конфигурация блокчейна

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

Устойчивые состояния

Рядом с непрерывно меняющимся текущим состоянием узел ведёт устойчивые состояния — замороженные снимки сети на отдельных ключевых блоках, нужные не самому узлу, а тому, кто проходит холодный старт. Эта подсистема не хранит в RocksDB ничего: ни одной собственной колонки, только читает базу ячеек при подготовке снимка. Файлы разложены по каталогам-поколениям <mc_seqno> и раздаются по сети чанками, а ротация выметает устаревшие поколения, а не сборка мусора.

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

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

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

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

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

Узел, собранный с feature s3, умеет читать архивы и устойчивые состояния не только из сети, но и из внешнего объектного хранилища: клиента только для чтения, без единого вызова записи. Он подключается в трёх точках: холодном старте, догоняющей синхронизации архивами и раздаче данных другим узлам по сети. Для первых двух гибридный клиент опрашивает сеть и объектное хранилище наперегонки, но только пока не сложится предпочтение одного из источников. Как только источник выигрывает гонку, дальше опрашивают только его — пока он отвечает успешно, а при первой же его неудаче гонка возобновляется. Для раздачи наружу объектное хранилище остаётся лишь запасным вариантом позади локального хранилища, гонки там нет вовсе.

Граф зависимостей

Зависимости между частями хранилища идут не по порядку статей, а по слоям, снизу вверх:

  • контекст хранилища — основание: RocksDB через weedb и файловая система, без зависимостей внутри раздела;
  • хранилище дескрипторов блоков опирается только на контекст хранилища и дальше служит метаданными для всего остального;
  • хранилище связей блоков опирается на контекст хранилища и хранилище дескрипторов блоков: запись каждой связи проверяет и правит флаг дескриптора, которым помечено, что эта связь уже сохранена;
  • Cassadilia (blob-хранилище) опирается на хранилище дескрипторов блоков, а не наоборот: сам движок ничего не знает ни о блоках, ни об архивах;
  • хранилище блоков опирается на Cassadilia, архивы, хранилище дескрипторов блоков и хранилище связей блоков: байты оно делегирует вниз, архивную часть своего фасада — в архивы, а метаданные берёт у соседей;
  • архивы опираются на Cassadilia и хранилище дескрипторов блоков, но не на хранилище связей блоков: байты уходят в Cassadilia, а статус блока при коммите архива проверяется по дескриптору;
  • хранилище ячеек опирается на хранилище дескрипторов блоков, хранилище блоков и хранилище связей блоков: эпоха ячейки и раунд её рождения приходят из дескриптора блока, а меркл-обновление для следующего состояния — из данных блока;
  • конфигурация блокчейна физически лежит внутри хранилища ячеек и зависит только от него;
  • устойчивые состояния опираются на хранилище ячеек, хранилище блоков, хранилище дескрипторов блоков и хранилище состояния узла: им нужны текущее дерево ячеек, хвост дифов очереди, индекс ключевых блоков, а от состояния узла — seqno нулевого состояния для ротации и отметка о незавершённом сохранении;
  • хранилище состояния узла зависит только от контекста хранилища — наравне с хранилищем дескрипторов блоков, это один из самых нижних доменных узлов. Хранилище связей блоков стоит уже слоем выше, поверх дескрипторов;
  • сборка мусора опирается почти на всё перечисленное сразу: архивы, хранилище блоков, Cassadilia, хранилище дескрипторов блоков, хранилище ячеек, устойчивые состояния и хранилище состояния узла — она читает границы отсечения у каждого вида данных, а не диктует их;
  • объектное хранилище опирается на устойчивые состояния и архивы: оно лишь читает те же данные вторым путём;
  • CoreStorage — фасад поверх контекста хранилища, хранилища блоков, хранилища дескрипторов блоков, хранилища связей блоков, хранилища ячеек, устойчивых состояний, хранилища состояния узла и сборки мусора: он открывает обе базы RocksDB, применяет миграции и связывает эти подсистемы в одну точку входа. Cassadilia и архивы напрямую в этот список не входят: CoreStorage получает их только транзитивно, через хранилище блоков. С S3-клиентом CoreStorage не связан вовсе: клиент работает рядом с ним (см. выше).

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