Формат Skiff

В первой части раздела описана библиотека skiff serialization library — library/cpp/skiff, которая позволяет кодировать структурированные данные, в том числе не являющиеся таблицами в YTsaurus.
Во второй части описано, как эта библиотека используется для эффективной передачи табличных данных между job_proxy и пользовательским кодом.

Skiff (от schemaful format) — один из форматов передачи схематизированных данных между job_proxy и джобом.

Цели формата:

  • Оптимизировать время сериализации и десериализации данных для схематизированных данных.
  • Поддержать все типы, которые поддерживает YTsaurus.
  • Поддержать чтение даже нестрого типизированных таблиц.

Сравнение с другими форматами

protobuf

Protobuf обладает следующими недостатками:

  • Для некоторых клиентов (YQL) структура сообщений определяется в runtime. Парсить и писать protobuf для неизвестного типа неудобно.
  • Protobuf поддерживает обратную совместимость. Поэтому появляются теги, которые нужно поддерживать в актуальном состоянии.
  • Порядок полей в Protobuf не специфицирован. Поэтому появляются лишние branch по тегам на каждое поле.
  • Protobuf часто использует тип varint. В случае job_proxy, которая общается с джобом через pipe дешевле записать uint64-число целиком, чем сериализовать и десериализовать varint.
  • Из-за использования varint строки и вложенные сообщения префиксируются длиной, чтобы сериализовать сообщение, необходимо обойти структуру дважды.
  • Repeated поля в protobuf необязательно должны быть локализованы в одной части message, то есть начало list может быть в начале message, конец — в конце. Это осложняет написание корректных парсеров.
flatbuffers
  • Для некоторых клиентов (YQL) структура сообщений определяется в runtime. Парсить и писать flatbuffers для неизвестного типа неудобно из-за отсутствия reflection.

Skiff serialization library

Формат сериализации skiff является схематизированным. Чтобы прочитать и интерпретировать данные, необходимо знать схему этих данных.

Skiff Schema

Схема описывает структуру закодированного значения и способ, которым его закодировали.
Она имеет древовидную структуру. Дерево из одного узла может быть валидной схемой. Порядок детей узла в дереве важен. Каждому узлу соответствует тип кодирования — wire_type, листьям соответствуют простые типы, остальным узлам дерева соответствуют составные типы. Узлы skiff-схемы могут иметь имена, имена не влияют на кодирование данных и служат для установления соответствия skiff-схемы со схемой таблицы.

Простые типы

Nothing

nothing — отсутствие значения, специальный тип, который может быть использован только как дочерний составных типов Variant* / RepeatedVariant*.
Кодируется пустой строкой.

Boolean

boolean — булев тип.
Кодируется одним байтом: 001 для true, 000 для false.
Примеры: \x01, \x00.

Int64 / Uint64

uint64 / int64 — целые числа.
Кодируется 8-байтным числом, закодированным в little endian.
Примеры: \x2a\x00\x00\x00\x00\x00\x00\x00 (42), \x94\x88\x01\x00\x00\x00\x00\x00 (100500).

Double

double — 8-байтное представление double.
Примеры: \x9B\x91\x04\x8B\x0A\xBF\x05\x40 (2.718281828).

String32

string32 — длина строки, закодированная как 32-битный uint в little endian, затем сама строка.
Примеры:\x06\x00\x00\x00foobar

Yson32

yson32 — значение произвольного типа.
Кодируется длина закодированного YSON, закодированная как 32-битный uint, за которым следует сам закодированный YSON.
Примеры: \x09\x00\x00\x00{foo=bar}, \x07\x00\x00\x00100500u

Составные типы

Variant8 / variant16

variant8 / variant16 — алгебраические типы данных.

Кодируются 8-битным (для variant8) или 16-битным (для variant16) беззнаковым числом i — тегом, за которым следует значение, описанное типом дочернего узла под индексом i.

  • variant8 / variant16 может иметь одним из своих узлов тип nothing. Так как тип nothing кодируется пустой строкой, то, например, строка \x00 является валидным значением типа variant8.
  • Битности определяются соображениями практичности, тег варианта будет кодировать присутствие или отсутствие значения или индекс таблицы.
RepeatedVariant8 / RepeatedVariant16

repeated_variant8 / repeated_variant16 — список variant.
Кодируется как непрерывная последовательность вариантов 8-битным (для repeated_variant8) или 16-битным (для repeated_variant16) тегом, оканчивающаяся специальным тегом 0xFF (для repeated_variant8) или 0xFFFF (для для repeated_variant16), обозначающим конец последовательности.

  • Аналогично обычному variant8 может иметь одним из дочерних узлов тип nothing.
  • Битность типа определяется соображениями практичности для задачи формата передачи табличных данных (по сути, тег варианта будет использоваться как индекс sparse-колонки).
Tuple

tuple — кортеж значений фиксированного размера.
Кодируется последовательностью значений, первое значение имеет тип первого дочернего узла, второе значение — тип второго дочернего узла и так далее.

Использование skiff serialization library для передачи табличных данных

Skiff-схема таблицы

В атрибутах формата для каждой передаваемой таблицы, передается skiff-схема таблицы table_skiff_schema. Это skiff-схема, на которую накладывается ряд ограничений.

Поведение входного и выходного форматов отличается. Входной формат склеивает все таблицы и передаёт их через один pipe, а выходной формат читает каждую таблицу из своего pipe.

На вход джобу приходят последовательно значения с общей схемой. Общая схема имеет своим корнем узел Variant16, который в качестве детей имеет все skiff-схемы таблиц.
Из выходного потока для i-й выходной таблицы job_proxy пытается вычитать значения, которые описываются схемой с корневым узлом Variant16 с единственным ребёнком — skiff-схема i-й таблицы.

Ограничения на skiff-схему таблицы.

На схему для передачи табличных данных накладываются ограничения:

  • Корневым узлом skiff-схемы таблицы должен быть узел типа tuple.
  • Все дети корневого узла skiff-схемы таблицы должны иметь имена. По ним YTsaurus определяет, в какое поле поместить значение из той или иной колонки.
  • Среди имён детей корневого узла skiff-схемы таблицы могут встречаться специальные имена, начинающиеся с символа $. Те дети, у которых имена не начинаются с этого специального символа, образуют множество dense-узлов. Это имена колонок, значения из которых будут помещены в эти поля.
  • Среди детей корневого узла skiff-схемы таблицы может быть узел, помеченный именем $other_columns. Этот узел должен иметь тип yson32 и быть последним.
  • Среди детей корневого узла skiff-схемы таблицы может быть узел, помеченный именем $sparse_columns. Этот узел должен иметь тип RepeatedVariant16. Его дети образуют множество sparse-узлов и также должны иметь имена. Узел $sparse_columns должен быть предпоследним, если есть узел $other_columns, или последним, если такого узла нет.
  • Среди детей корневого узла skiff-схемы таблицы может быть узел со специальным именем $key_switch, он должен иметь тип boolean.
  • Среди детей корневого узла skiff-схемы таблицы могут быть узлы со специальными именами $row_index / $range_index. Они должны иметь тип variant8<nothing;int64>
  • Все dense-узлы должны иметь схему, которая:
    • либо описывает простой тип: int64, uint64, boolean, double, string32, yson32;
    • либо описывает тип variant8<nothing; TYPE;>, где TYPE — это простой тип: int64, uint64, boolean, double, string32, yson32
  • Все sparse-узлы должны иметь схему, которая описывает простой тип int64, uint64, boolean, double, string32, yson32.

Когда сериализуется строка таблицы, для каждого значения нужно найти dense или sparse-узел, имеющий то же имя, что и имя колонки.
Значения, которые описываются dense-узлами сериализуются всегда, как требует корневой узел skiff-схемы — tuple. Если значение для dense-узла отсутствует или имеет тип NULL, то соответствующий dense-узел skiff-схемы должен иметь тип variant8.
Значения, которые описываются sparse-частью схемы сериализуются, только если они присутствуют и имеют тип, отличный от NULL. Они попадают в поле $sparse_columns в виде списка.

Те значения строки таблицы, которые не попали в dense или sparse-часть, отправляются в колонку $other_columns. В этой колонке будет лежать yson map с такими значениями. Если колонки $other_columns нет, но при этом есть значение, не описанное ни в dense, ни в sparse-части, то будет сгенерирована ошибка.

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

Описание skiff-формата, как и других форматов, делается с помощью атрибутов. В атрибутах должен присутствовать ключ table_skiff_schemas со списком описаний skiff-схем таблиц. Кроме того, может присутствовать ключ skiff_schema_registry. Если он присутствует, то должен содержать YSON map, хранящий отображение имени во вспомогательной skiff-схеме.
Skiff-схема в table_skiff_schemas, skiff_schema_registry может быть описана одним из двух способов:

  • Строкой, начинающейся с $. Тогда символ =$ будет удален, а оставшаяся строка будет найдена в map skiff_schema_registry. Схемой считается схема, описанная значением по соответствующему ключу.
  • Map с полями:
    • (required) wire_type тип skiff узла в дереве;
    • (optional) name имя узла;
    • (optional) children дети узла. Для составных типов это поле должно присутствовать. Ребёнок может либо ссылаться на описание в skiff_schema_registry, либо быть map.

skiff_schema_registry нужен, чтобы позволить запускать операции с большим количеством входных таблиц с одинаковой схемой.

Пример:

<
    "table_skiff_schemas" = [
        "$table1"
    ];
    "skiff_schema_registry" = {
        "table1" = {
            "children" = [
                {
                    "name" = "uint64_column";
                    "wire_type" = "uint64"
                };
                {
                    "name" = "int64_column";
                    "wire_type" = "int64"
                };
                {
                    "name" = "boolean_column";
                    "wire_type" = "boolean"
                };
                {
                    "name" = "string32_column";
                    "wire_type" = "string32"
                };
                {
                    "name" = "yson32_column";
                    "wire_type" = "yson32"
                }
            ];
            "wire_type" = "tuple"
        }
    }
> "skiff"
Следующая