Формат 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
может быть описана одним из двух способов:
- Строкой, начинающейся с
$
. Тогда символ =$
будет удален, а оставшаяся строка будет найдена в 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"