0074. Хранилище принимает готовый файл вместо копии
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-11.
Контекст
Аудит производительности (2026-10-11, этап 2) нашёл лишние полные копии больших объектов на пути записи. LocalBlobStore.put пишет объект один раз: во временный файл с хешем, затем жёсткая ссылка в blobs/. Но несколько путей сначала кладут байты в свою временную область на том же томе, а потом читают их заново в put, который снова пишет и хеширует:
- raw-файлы без контрольной суммы (ADR 0064) и публикация npm: временный файл уже хеширован, затем ещё одно чтение, запись и хеш;
- слой OCI: куски
PATCHдописываются во временный файл, приPUTон целиком копируется в хранилище; на время копии объект занимает место дважды; - зеркала: каждая часть хешируется во временный файл, затем копируется в часть загрузки с повторным хешем.
Решение
- Временная область отдаёт свой файл как поток с меткой
StagedFile: путь, а для raw-файлов и частей зеркал — размер и SHA-256, измеренные при записи. Слой приложения меток не видит и путей не получает — для него это обычный поток. LocalBlobStoreпринимает такой файл как объект (или часть) второй ссылкой на те же байты, без копии:- только если файл лежит на том же томе, что и хранилище; иначе и при отсутствии файла — прежнее копирование;
- размер файла и измеренные размер и SHA-256 должны совпасть с заявленными; без измерения (OCI) файл один раз хешируется на месте;
- перед тем как объект станет видимым, файл синхронизируется (
fsync), затем ссылка и синхронизация каталога — гарантия долговечности та же, что у записи потоком.
- Внутренние чтения (сборка multipart, проверки хеша) идут блоками 1 МиБ; ответы клиентам — прежние 64 КиБ, на них настроен ограничитель полосы.
Почему измерению можно доверять
Размер и хеш считает тот же код, который пишет файл, с правами 0600 в каталоге данных; никто другой этот файл не открывает. Перед переносом хранилище переименовывает временный файл в собственное имя и убирает его после ссылки, поэтому через временное имя до опубликованных байтов уже не добраться. Это важно для OCI: временная область дописывает в существующий файл (r+), и повтор запроса после сбоя между переносом и удалением загрузки иначе писал бы в опубликованный объект. Теперь такой повтор не находит файла и получает BLOB_UPLOAD_INVALID. Если перенос не удался, файл возвращается под временное имя, если его не занял новый файл. Зеркало создаёт часть заново (wx), а не усекает её.
Что не меняется
- Завершение multipart по-прежнему читает все части и пишет объект: SHA-256 всего объекта нельзя получить из хешей частей. Большие объекты завершаются в worker, не в HTTP-запросе.
- npm по-прежнему читает tarball для разбора
package.jsonи хешей integrity; убрана только третья копия в хранилище.
Последствия
- Для raw, npm и зеркал — на одно полное чтение, запись и хеш меньше; для OCI — на одну запись меньше и без двойного места на время финализации.
- Неопубликованный объект, если его файл после переноса всё-таки изменился бы, при повторе проверяется хешем и отвергается; опубликованные объекты неизменяемы.
- Перенос не резервирует место: он не занимает новых байтов. Проверка свободного места остаётся для записи потоком.
- Тесты: перенос без копии (тот же inode), после переноса временное имя свободно и запись через него не меняет объект, отказ при несовпадении размера или хеша возвращает файл, прежняя запись при отсутствии файла, перенос при заполненном томе.