Формат 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-схемы таблицы может быть узел со специальным именем
$remaining_row_bytes, он должен иметь типint32. - Среди детей корневого узла 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-части, то будет сгенерирована ошибка.
Для узла $remaining_row_bytes возвращается число байт, которые занимает оставшаяся часть строчки в skiff-формате.
Конфигурация формата
Описание skiff-формата, как и других форматов, делается с помощью атрибутов. В атрибутах должен присутствовать ключ table_skiff_schemas со списком описаний skiff-схем таблиц. Кроме того, может присутствовать ключ skiff_schema_registry. Если он присутствует, то должен содержать YSON map, хранящий отображение имени во вспомогательной skiff-схеме.
Skiff-схема в table_skiff_schemas, skiff_schema_registry может быть описана одним из двух способов:
- Строкой, начинающейся с
$. Тогда символ =$будет удален, а оставшаяся строка будет найдена в mapskiff_schema_registry. Схемой считается схема, описанная значением по соответствующему ключу. - Map с полями:
- (required)
wire_typeтип skiff узла в дереве; - (optional)
nameимя узла; - (optional)
childrenдети узла. Для составных типов это поле должно присутствовать. Ребёнок может либо ссылаться на описание вskiff_schema_registry, либо быть map.
- (required)
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"