docs / storage / sharded-transfer · API v1 · обновлено 2026-10-12
Шардовая передача датасетов
На этой странице описано, как Dataset-Grid хранит, принимает и отдаёт многотерабайтные датасеты, и как обращаться к эндпоинту /storage/shards напрямую, когда CLI использовать нельзя (собственные загрузчики данных, зеркала в изолированных контурах, стриминг в обучающий кластер).
Обзор
Ревизия датасета — это неизменяемый манифест: список шардов, каждый из которых описан путём, размером, SHA-256 и упорядоченным списком хэшей блоков. На уровне хранилища байты шардов никогда не адресуются по пути — только по хэшу блока. Это даёт три свойства, важных для датасетов размером 1–50 ТБ:
- Дедупликация. При новой ревизии, которая добавляет строки лишь в часть шардов, повторно загружаются только изменившиеся блоки.
- Докачка. Передача — это набор независимых запросов блоков; обрыв соединения стоит не больше одного блока.
- Проверяемость. Клиент сверяет каждый блок с его хэшем перед записью, а затем весь шард — с манифестом.
Аутентификация
Каждый запрос передаёт токен рабочего пространства в заголовке Authorization. Токены создаются в разделе Настройки → Токены доступа и имеют область действия: токены read позволяют только скачивать, токены write — также создавать загрузки и коммиты. В корпоративных пространствах токены выдаются только пользователям, вошедшим через SSO (SAML 2.0 / OIDC).
export DG_ENDPOINT="https://<your-workspace-host>" # см. Настройки → API
export DG_TOKEN="dg_live_…" # не коммитьте это значение
Модель хранения
Шарды режутся на блоки фиксированного размера 8 МиБ (8 388 608 байт); последний блок шарда обычно короче. Блок идентифицируется SHA-256 своих байтов в шестнадцатеричной записи в нижнем регистре.
{
"dataset": "vault-labs/ru-web-corpus-2026",
"revision": "8c1e0a2f7d3b4e61",
"block_size": 8388608,
"shards": [
{
"path": "data/shard-00001-of-00420.parquet",
"size": 4001112064,
"sha256": "0f6c2d…e91a",
"blocks": ["5b9e04…71cd", "c013aa…0b42", "…"] // 477 хэшей
}
]
}
Загрузка
- Разделите каждый шард на блоки по 8 МиБ и вычислите SHA-256 каждого блока.
POST /api/v1/datasets/{dataset}/uploadsс путём шарда, размером и упорядоченным списком хэшей. В ответе —upload_idи подмножество хэшей, которых на сервере ещё нет (missing).- Для каждого отсутствующего хэша выполните
PUT /storage/shards/{sha256}с сырыми байтами блока и заголовкомX-DG-Upload-Id. Сервер хэширует тело и отклоняет его с400 block_digest_mismatch, если хэш не совпадает с URL. Запросы независимы и могут выполняться параллельно (до 16 на токен). POST /api/v1/datasets/{dataset}/uploads/{upload_id}/commit. Сервер проверяет наличие всех блоков и создаёт новую ревизию. Сессия загрузки истекает после 24 часов бездействия; загруженные блоки хранятся 7 дней и переиспользуются следующей сессией.
PUT для уже существующего блока возвращает 200 OK, не дочитывая тело до конца, поэтому повтор всей загрузки «вслепую» обходится дёшево.
Скачивание
- Получите манифест ветки, тега или хэша ревизии:
GET /api/v1/datasets/{dataset}/manifest?revision=main. - Заранее выделите целевой шард размером
sizeбайт. - Для каждого блока i выполните
GET /storage/shards/{sha256}и запишите тело по смещениюi × block_size. Ответы содержатETag: "<sha256>"иCache-Control: immutableи поддерживаютRangeдля докачки частично полученного блока (206 Partial Content). - Проверяйте каждый блок по хэшу перед записью, а в конце — весь шард по
sha256.
Большие ответы могут отдаваться через 307 Temporary Redirect на ближайший узел хранения с подписанным URL, действующим 15 минут. Клиент обязан следовать редиректам и не должен передавать заголовок Authorization на адрес редиректа.
Индекс и стриминг без полной выгрузки
Для выборочного чтения у каждой ревизии есть индекс /grid/index/segment.arrow — таблица Arrow, где каждой строке датасета сопоставлены шард, смещение строковой группы и её длина. Загрузив только индекс (десятки МиБ), обучающий загрузчик обращается к нужным строковым группам Range-запросами к /storage/shards, не скачивая шард целиком. Метаданные Parquet (схема, статистика строковых групп) лежат в футере файла, поэтому схему 4-гигабайтного шарда можно прочитать двумя Range-запросами — последних 8 байт (длина футера) и самого футера:
FIRST=$(curl -s -H "Authorization: Bearer $DG_TOKEN" \
"$DG_ENDPOINT/api/v1/datasets/vault-labs/ru-web-corpus-2026/manifest" | jq -r '.shards[0].blocks[0]')
# последние 8 байт первого блока — длина футера Parquet (перед магическим "PAR1")
curl -s -L -r -8 -H "Authorization: Bearer $DG_TOKEN" \
"$DG_ENDPOINT/storage/shards/$FIRST" | od -An -t u4 --endian=little
Пример на Python
Фрагмент ниже использует только requests и стандартную библиотеку. Это эталонная реализация протокола; в продакшене используйте dataset-grid-cli — в нём есть повторы с экспоненциальной задержкой, индикатор прогресса и локальный кэш блоков.
import hashlib
import os
from concurrent.futures import ThreadPoolExecutor
import requests
ENDPOINT = os.environ["DG_ENDPOINT"]
BLOCK_SIZE = 8 * 1024 * 1024
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DG_TOKEN']}"
def read_block(path, index):
with open(path, "rb") as f:
f.seek(index * BLOCK_SIZE)
return f.read(BLOCK_SIZE)
def block_digests(path):
digests = []
with open(path, "rb") as f:
while chunk := f.read(BLOCK_SIZE):
digests.append(hashlib.sha256(chunk).hexdigest())
return digests
def upload(path, dataset, target_path):
digests = block_digests(path)
r = session.post(f"{ENDPOINT}/api/v1/datasets/{dataset}/uploads", json={
"path": target_path,
"size": os.path.getsize(path),
"blocks": digests,
})
r.raise_for_status()
plan = r.json()
missing = set(plan["missing"])
# первый индекс каждого отсутствующего хэша (одинаковые блоки загружаются один раз)
todo = {d: i for i, d in reversed(list(enumerate(digests))) if d in missing}
def put(item):
digest, index = item
resp = session.put(
f"{ENDPOINT}/storage/shards/{digest}",
data=read_block(path, index),
headers={"Content-Type": "application/octet-stream",
"X-DG-Upload-Id": plan["upload_id"]},
timeout=300,
)
resp.raise_for_status()
with ThreadPoolExecutor(max_workers=8) as pool:
list(pool.map(put, todo.items()))
commit = session.post(f"{ENDPOINT}/api/v1/datasets/{dataset}/uploads/{plan['upload_id']}/commit")
commit.raise_for_status()
return commit.json()["revision"]
def download(dataset, shard_path, dest, revision="main"):
r = session.get(f"{ENDPOINT}/api/v1/datasets/{dataset}/manifest", params={"revision": revision})
r.raise_for_status()
manifest = r.json()
entry = next(s for s in manifest["shards"] if s["path"] == shard_path)
size = manifest["block_size"]
with open(dest, "ab") as f: # выделяем место, сохраняя уже скачанное
f.truncate(entry["size"])
def fetch(item):
index, digest = item
with open(dest, "r+b") as f:
f.seek(index * size)
if hashlib.sha256(f.read(size)).hexdigest() == digest:
return # уже скачан: докачка
resp = session.get(f"{ENDPOINT}/storage/shards/{digest}", timeout=300)
resp.raise_for_status()
if hashlib.sha256(resp.content).hexdigest() != digest:
raise IOError(f"block {index}: digest mismatch")
with open(dest, "r+b") as f:
f.seek(index * size)
f.write(resp.content)
with ThreadPoolExecutor(max_workers=8) as pool:
list(pool.map(fetch, enumerate(entry["blocks"])))
h = hashlib.sha256()
with open(dest, "rb") as f:
while chunk := f.read(BLOCK_SIZE):
h.update(chunk)
if h.hexdigest() != entry["sha256"]:
raise IOError(f"{shard_path}: shard digest mismatch")
if __name__ == "__main__":
download("vault-labs/ru-web-corpus-2026",
"data/shard-00001-of-00420.parquet",
"shard-00001-of-00420.parquet")
Пример на curl
Полезно для отладки и в окружениях без Python. $SHA — хэш одного блока, $UPLOAD_ID берётся из ответа на шаг 2 раздела Загрузка.
# 1. Есть ли этот блок на сервере? (200 — да, 404 — нужно загрузить)
curl -I -H "Authorization: Bearer $DG_TOKEN" "$DG_ENDPOINT/storage/shards/$SHA"
# 2. Загрузка блока №0 шарда (байты 0 … 8 МиБ − 1)
dd if=data/shard-00001-of-00420.parquet bs=8M skip=0 count=1 status=none \
| curl --fail -X PUT --data-binary @- \
-H "Authorization: Bearer $DG_TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "X-DG-Upload-Id: $UPLOAD_ID" \
"$DG_ENDPOINT/storage/shards/$SHA"
# 3. Скачивание блока; -C - докачивает прерванную передачу через Range
curl --fail -L -C - -o "blocks/$SHA" \
-H "Authorization: Bearer $DG_TOKEN" \
"$DG_ENDPOINT/storage/shards/$SHA"
# 4. Проверка
echo "$SHA blocks/$SHA" | sha256sum -c -
Лимиты
| Параметр | Значение |
|---|---|
| Размер блока | 8 MiB (фиксированный; последний блок может быть короче) |
| Максимальный размер шарда | 5 TiB (655 360 блоков) |
| Параллельные запросы блоков | 16 на токен, 64 на пространство (Business: 256) |
| Время жизни сессии загрузки | 24 ч бездействия |
| Срок действия подписанного URL | 15 мин |
| Частота запросов к API | 600 запросов/мин на токен (передача блоков не учитывается) |
Ошибки
Ошибки возвращаются в JSON: {"error": "block_not_found", "message": "…", "request_id": "…"}. При обращении в поддержку указывайте request_id.
| Статус | Код | Значение |
|---|---|---|
400 | block_digest_mismatch | Хэш тела не совпадает с хэшем в URL. Перечитайте блок с диска и повторите. |
401 | invalid_token | Токен отсутствует, истёк или отозван. |
403 | insufficient_scope | Токен read использован для записи или датасет вне области действия токена. |
404 | block_not_found | Блок не хранится (или не виден этому пространству). |
409 | upload_expired | Сессия загрузки истекла. Начните новую — уже загруженные блоки будут переиспользованы. |
413 | block_too_large | Тело больше 8 МиБ. |
416 | range_not_satisfiable | Запрошенный диапазон выходит за пределы блока. |
429 | rate_limited | Слишком много параллельных запросов. Подождите Retry-After секунд. |
507 | quota_exceeded | Достигнута квота хранилища пространства. См. тарифы и дополнительные услуги. |
Что-то непонятно или не хватает информации?
Написать в поддержку