Перейти к содержимому
Dataset-Grid
en

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).

shell
export DG_ENDPOINT="https://<your-workspace-host>"   # см. Настройки → API
export DG_TOKEN="dg_live_…"                           # не коммитьте это значение

Модель хранения

Шарды режутся на блоки фиксированного размера 8 МиБ (8 388 608 байт); последний блок шарда обычно короче. Блок идентифицируется SHA-256 своих байтов в шестнадцатеричной записи в нижнем регистре.

manifest.json (фрагмент)GET /api/v1/datasets/{dataset}/manifest
{
  "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 хэшей
    }
  ]
}

Загрузка

  1. Разделите каждый шард на блоки по 8 МиБ и вычислите SHA-256 каждого блока.
  2. POST /api/v1/datasets/{dataset}/uploads с путём шарда, размером и упорядоченным списком хэшей. В ответе — upload_id и подмножество хэшей, которых на сервере ещё нет (missing).
  3. Для каждого отсутствующего хэша выполните PUT /storage/shards/{sha256} с сырыми байтами блока и заголовком X-DG-Upload-Id. Сервер хэширует тело и отклоняет его с 400 block_digest_mismatch, если хэш не совпадает с URL. Запросы независимы и могут выполняться параллельно (до 16 на токен).
  4. POST /api/v1/datasets/{dataset}/uploads/{upload_id}/commit. Сервер проверяет наличие всех блоков и создаёт новую ревизию. Сессия загрузки истекает после 24 часов бездействия; загруженные блоки хранятся 7 дней и переиспользуются следующей сессией.

PUT для уже существующего блока возвращает 200 OK, не дочитывая тело до конца, поэтому повтор всей загрузки «вслепую» обходится дёшево.

Скачивание

  1. Получите манифест ветки, тега или хэша ревизии: GET /api/v1/datasets/{dataset}/manifest?revision=main.
  2. Заранее выделите целевой шард размером size байт.
  3. Для каждого блока i выполните GET /storage/shards/{sha256} и запишите тело по смещению i × block_size. Ответы содержат ETag: "<sha256>" и Cache-Control: immutable и поддерживают Range для докачки частично полученного блока (206 Partial Content).
  4. Проверяйте каждый блок по хэшу перед записью, а в конце — весь шард по sha256.

Большие ответы могут отдаваться через 307 Temporary Redirect на ближайший узел хранения с подписанным URL, действующим 15 минут. Клиент обязан следовать редиректам и не должен передавать заголовок Authorization на адрес редиректа.

Индекс и стриминг без полной выгрузки

Для выборочного чтения у каждой ревизии есть индекс /grid/index/segment.arrow — таблица Arrow, где каждой строке датасета сопоставлены шард, смещение строковой группы и её длина. Загрузив только индекс (десятки МиБ), обучающий загрузчик обращается к нужным строковым группам Range-запросами к /storage/shards, не скачивая шард целиком. Метаданные Parquet (схема, статистика строковых групп) лежат в футере файла, поэтому схему 4-гигабайтного шарда можно прочитать двумя Range-запросами — последних 8 байт (длина футера) и самого футера:

bash
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 — в нём есть повторы с экспоненциальной задержкой, индикатор прогресса и локальный кэш блоков.

dg_transfer.py
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 раздела Загрузка.

bash
# 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 ч бездействия
Срок действия подписанного URL15 мин
Частота запросов к API600 запросов/мин на токен (передача блоков не учитывается)

Ошибки

Ошибки возвращаются в JSON: {"error": "block_not_found", "message": "…", "request_id": "…"}. При обращении в поддержку указывайте request_id.

СтатусКодЗначение
400block_digest_mismatchХэш тела не совпадает с хэшем в URL. Перечитайте блок с диска и повторите.
401invalid_tokenТокен отсутствует, истёк или отозван.
403insufficient_scopeТокен read использован для записи или датасет вне области действия токена.
404block_not_foundБлок не хранится (или не виден этому пространству).
409upload_expiredСессия загрузки истекла. Начните новую — уже загруженные блоки будут переиспользованы.
413block_too_largeТело больше 8 МиБ.
416range_not_satisfiableЗапрошенный диапазон выходит за пределы блока.
429rate_limitedСлишком много параллельных запросов. Подождите Retry-After секунд.
507quota_exceededДостигнута квота хранилища пространства. См. тарифы и дополнительные услуги.

Что-то непонятно или не хватает информации?

Написать в поддержку