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

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

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

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

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

Клиент под флагом сборки

Весь код объектного хранилища собирается только с feature s3, которой нет в сборке узла по умолчанию. Без неё в бинаре нет ни модуля клиента, ни поля конфигурации s3_client, ни связанных с ним реализаций холодного старта, провайдера архивов и S3-прокси blockchain RPC.

Поэтому секция s3_client, оказавшаяся в файле конфигурации узла, собранного без этой feature, не вызывает ошибки разбора — она просто молча игнорируется, и узел стартует без объектного хранилища вовсе. Диагностика в этом случае только косвенная: в логах не появляется ничего, что указывало бы на S3.

Клиент только для чтения

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

В обход этих четырёх операций клиент открывает прямой доступ к нижележащему объектному хранилищу для диапазонных чтений.

Ни один из них не пишет в объектное хранилище.

Адресация объектов

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

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

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

Проверка наличия

Перед тем как качать объект, клиент спрашивает о нём напрямую — один запрос на размер, без предварительного листинга бакета. У ответа три исхода:

  • объект найден, и его размер больше нуля — можно скачивать, известен точный размер;
  • объекта нет либо он найден с размером ровно ноль — оба случая клиент трактует одинаково, как отсутствие объекта;
  • любая другая ошибка (сеть, доступ, неверная подпись) уходит вызывающему как есть.

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

Скачивание чанками

Скачивание начинается с того же запроса на размер, что и справка. Дальше объект качается диапазонными запросами по размеру чанка, заданному конфигурацией: до 10 запросов одновременно. Но в обработку чанки уходят строго по порядку смещений: иначе их нельзя было бы распаковывать потоком. Распаковка идёт zstd на отдельном блокирующем потоке, а не в асинхронном коде. Чанк приходит сжатым, а дальше уже распакованные байты передаются получателю, которому клиент не навязывает ни файл, ни буфер в памяти: для провайдера архивов это буфер в памяти либо временный файл в зависимости от memory_threshold, а для устойчивого состояния — временный файл с последующим переименованием.

В версии 0.3.11 число одновременных запросов остаётся константой кода, а не отдельной настройкой рядом с размером чанка и числом повторов.

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

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

Повторы при неудаче

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

Три точки подключения

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

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

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

Раздача наружу. Если в секции RPC-сервиса отдельно задано поле s3_proxy, тот же клиент подставляется провайдером данных RPC рядом с локальным хранилищем узла: тогда узел способен отвечать на запросы архивов и устойчивых состояний по сети, даже если нужных данных нет у него самого на диске. Здесь гонки между источниками нет: объектное хранилище — простой запасной вариант позади локального хранилища, к нему обращаются, только если локальное хранилище не смогло ответить.

Гонка источников

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

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

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

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

ПараметрПо умолчаниюЧто задаёт
region— (обязательное)регион адреса объектного хранилища
endpoint— (обязательное)адрес объектного хранилища вместе со схемой
bucket— (обязательное)имя бакета
archive_key_prefix""префикс перед идентификатором архива в ключе объекта
state_key_prefix""префикс перед именем файла устойчивого состояния в ключе объекта
credentialsnullучётные данные доступа к объектному хранилищу
chunk_size"10 MiB"размер чанка при скачивании
download_retries10число попыток скачать один чанк

Значение chunk_size дополнительно ограничено при создании клиента: узел уже на старте отвергает размер меньше 1 КиБ или больше максимума 32-битного целого числа (чуть меньше 4 ГиБ — 4 294 967 295 байт, ровно 4 ГиБ уже не проходит). Ограничение здесь жёсткое: узел с такой конфигурацией не поднимается вовсе, а не продолжает работу со значением по умолчанию.

Если объектное хранилище требует авторизации, внутри s3_client задаётся вложенная секция credentials:

ПараметрПо умолчаниюЧто задаёт
access_key— (обязательное)идентификатор ключа доступа
secret_key— (обязательное)секретный ключ доступа
tokennullвременный токен сессии

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