Объектное хранилище
Другие статьи раздела уже показывали узел, который берёт архивы блоков и устойчивые состояния у соседей по сети. Рядом с этим путём при сборке с 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 | "" | префикс перед именем файла устойчивого состояния в ключе объекта |
credentials | null | учётные данные доступа к объектному хранилищу |
chunk_size | "10 MiB" | размер чанка при скачивании |
download_retries | 10 | число попыток скачать один чанк |
Значение chunk_size дополнительно ограничено при создании клиента: узел уже на старте отвергает размер меньше 1 КиБ или больше максимума 32-битного целого числа (чуть меньше 4 ГиБ — 4 294 967 295 байт, ровно 4 ГиБ уже не проходит). Ограничение здесь жёсткое: узел с такой конфигурацией не поднимается вовсе, а не продолжает работу со значением по умолчанию.
Если объектное хранилище требует авторизации, внутри s3_client задаётся вложенная секция credentials:
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
access_key | — (обязательное) | идентификатор ключа доступа |
secret_key | — (обязательное) | секретный ключ доступа |
token | null | временный токен сессии |
Без секции credentials клиент не задаёт статических ключей доступа вовсе и обращается за ними к сервису метаданных экземпляра — такой путь предполагает, что узел работает в облачной среде, которая сама раздаёт временные учётные данные.
Сборка мусора
Как хранилище узла избавляется от устаревших данных — три независимых фоновых процесса для архивов, блоков и состояний шардов, у каждого своя граница отсечения и свой момент срабатывания, ручной запуск через управляющий сокет и защита блоков и состояний, ещё нужных для сборки устойчивых состояний, от преждевременного удаления.
Проверка подписи в TON и Tycho