CJSON: внутренний формат Reindexer для обработки данных

В Reindexer используется внутренний формат представления данных CJSON (Compact JSON). С его помощью решаются задачи:

  1. Хранение - -tuple (используется в ядре Reindexer совместно с со структурой PayloadValue).
  2. Передача данных по сети - «транспортный» CJSON.

Структура CJSON для обоих случаев схожая, однако между -tuple и «транспортным» CJSON есть различия в способах хранения значений полей документа.

Структура CJSON

Каждое поле в СJSON представлено в виде ctag, который кодирует тип, имя и двоичное представление данных поля в формате, зависящем от типа.

Формат ctag представлен в таблице:

Поле ctag Размер в битах Описание
TypeTag0 3 Тип поля. Зависит от типа данных в поле. Записывается в виде TAG_XXX. Возможные значения представлены ниже
NameIndex 12 Индекс имени поля в словаре имён. 0 означает пустое имя
FieldIndex 10 Индекс поля данных в PayloadValue. 0 - нет ссылки, значение следует сразу за ctag
Reserved 4 Зарезервировано для дальнейшенго использования
TypeTag1 3 Дополнительные старшие биты для Типа поля. Совместно с TypeTag0 определяют фактический тип данных

Возможные значения поля TypeTag:

Тип поля (TypeTag) Значение Описание
TAG_VARINT 0 Данные в целочисленном формате
TAG_DOUBLE 1 Числа с плавающей точкой
TAG_STRING 2 Данные в формате строки
TAG_ARRAY 3 Данные — массив элементов
TAG_BOOL 4 Данные в формате boolean
TAG_NULL 5 Null
TAG_OBJECT 6 Данные — объект
TAG_END 7 Конец CJSON-объекта
TAG_UUID 8 Данные в формате UUID. Старший бит хранится в TypeTag1

Хранение массивов

Массивы могут храниться двумя различными способами:

  • однородный массив из элементов одного типа,
  • смешанный массив из элементов разных типов.

Оба типа массивов в дополнение к ctag используют atag.

atag — формат тега массива

atag — тег (4 байта, int), с помощью которого кодируется количество и тип элементов массива:

Поле тега Размер в битах Описание
Count 24 Количество элементов в массиве
TypeTag 6 Тип элементов массива. Если TypeTag массива равен — TAG_OBJECT, массив является смешанным, и каждый его элемент содержит свой ctag. TypeTag здесь может принимать те же значения, что и в ctag

Формат пакета CJSON

# Поле Описание
1 Offset to names dict Смещение словаря имен (номер байта в пакете, с которого начинается словарь). Если в пакете нет нового словаря имен, TAG_END в начале отсутствует
2 Records Дерево записей. Начинается с TAG_OBJECT и заканчивается TAG_END
3 Names dictionary Словарь имен полей
names_dictionary := 
  <varint(count)>
  [[<varint(string length)>, <name char array>],
   [<varint(string length)>, <name char array>],
   ...

Offset to names dict не всегда присутствует в пакете. Если пакет начинается с TAG_END, за ним размещается Offset to names dict. Если же пакет начинается не с TAG_END, то в нем вообще не содержится словарь имен.

Формат записи:

record := 
  ctag := (TAG_OBJECT,name)
    [field := 
      ctag := (TAG_VARINT,name) data := <varint> |
      ctag := (TAG_DOUBLE,name) data := <8 byte double> |
      ctag := (TAG_BOOL,name) data := <1 byte: 0 - False, 1 - True> |
      ctag := (TAG_STRING,name) data := <varint(string length)>, <char array> |
      ctag := (TAG_NULL,name) |
      ctag := (TAG_ARRAY,name) 
        data := atag := (TAG_OBJECT|TAG_VARINT|TAG_DOUBLE|TAG_BOOL, count) 
        array := [ctag := TAG_XXX] <data>,[[ctag := TAG_XXX>]<data>]] ... |
      ctag (TAG_OBJECT,name)> 
        [subfield := field]
      ...
      ctag := (TAG_END)
    ]
    ...
  ctag := (TAG_END)

Пример объекта CJSON

{
  "name": "Hello",
  "year": 2010,
  "articles": [
    1,
    2,
    3,
    4,
    5
  ],
  "info": {
    "name": "Info"
  }
}
(TAG_OBJECT)                                    06 
  (TAG_STRING,1) 5,"Hello"                      0A 05 48 65 6C 6C 6F
  (TAG_VARINT,2) 2010                           10 B4 1F
  (TAG_ARRAY,3) (TAG_VARINT,5) 1 2 3 4 5        1B 05 00 00 00 01 02 03 04 05
  (TAG_OBJECT,4)                                26
     (TAG_STRING,1) 4,"Info"                    0A 04 49 6E 66 6F
  (TAG_END)                                     07
(TAG_END)                                       07
1 - "name"
2 - "year"
3 - "articles"
4 - "info"

-tuple и «транспортный» CJSON

-tuple

-tuple в формате CJSON используется для обработки документов ядром Reindexer. Его применение позволяет решить проблему с отсутствием унифицированной структуры записей в документоориентированных БД, когда каждая запись может иметь уникальные поля и нужно где-то хранить информацию по ее неиндексным полям.

-tuple - всегда самый первый (с индексом 0) элемент записи (PayloadValue -> Item). В нем хранятся либо ссылки на индексные поля записи, либо сами значения — в случае если поле неиндексное.

В CJSON каждое поле записи (документа, строки неймспейса) описывается с помощью ctagcarraytag). В ctag хранится:

  • тип поля (TypeTag),
  • целочисленные номер имени поля (NameIndex),
  • индекс поля в PayloadValue (FieldTag) - если поле индексное .

Если поле индексное, в FieldTag хранится его индекс в PayloadType и IndexesStorage (этот индекс также позволяет вычислить его смещение в PayloadValue). При этом в -tuple сериализуется только ctag, а само значение находится в PayloadValue (доступ к нему можно получить через метод PayloadIface::Get(ctag.field)).

Если поле неиндексное, значение ctag.field = -1. Значение поля сериализуется сразу за ctag.

Все документы неймспейса хранятся в NamespaceImpl (массив items_) в виде PayloadValue. У каждого такого документа в первом поле находится значение -tuple.

«Транспортный» CJSON

«Транспортный» CJSON используется для отправки данных по сети. С ним работают cproto-клиенты, grpc-клиенты, rpc/grpc-серверы. Так же он используется при проксировании в синхронном кластере и шардировании.

В «транспортным» CJSON (в отличие от -tuple, используемого ядром) каждое поле, независимо от того, индексное оно или неиндексное, кодируется без ссылок на его значение в PayloadValue. То есть значение каждого поля хранится непосредственно внутри объекта CJSON. Ссылки на индексные поля в данном случае использоваться не могут, поскольку клиент может не иметь данных PayloadType.

Пример обхода структуры CJSON (код из skipCjsonTag):

void skipCjsonTag(ctag tag, Serializer &rdser) {
	const bool embeddedField = (tag.Field() < 0);
	switch (tag.Type()) {
		case TAG_ARRAY: {
			if (embeddedField) {
				carraytag atag = rdser.GetUInt32();
				for (int i = 0; i < atag.Count(); i++) {
					ctag t = atag.Tag() != TAG_OBJECT ? atag.Tag() : rdser.GetVarUint();
					skipCjsonTag(t, rdser);
				}
			} else {
				rdser.GetVarUint();
			}
		} break;

		case TAG_OBJECT:
			for (ctag otag = rdser.GetVarUint(); otag.Type() != TAG_END; otag = rdser.GetVarUint()) {
				skipCjsonTag(otag, rdser);
			}
			break;
		default:
			if (embeddedField) rdser.GetRawVariant(KeyValueType(tag.Type()));
	}
}

Основные модули работы с CJSON

В Reindexer для работы с CJSON используются следующие основные модули:

Модуль Описание Ссылка
CJsonModifier Класс модификации существующего CJSON. Используется для обновления и удаления существующих, добавления новых (как индексных, так и неиндексных) полей. Поиск поля для модификации осуществляется по его TagsPath (json path с тэгом на каждое вложенное поле) через рекурсивный обход json-подобной структуры CJSON. Обновление поля происходит через построение нового CJSON, либо с обновлением текущих полей (или добавлением новых), либо с пропуском (при удалении) некоторых из них. Если при обходе всего CJSON соответствующее поле не было найдено, осуществляется вставка нового поля из корня cjson в соответствие с заданным json path https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsonmodifier.cc
ItemModifier Класс модификации записи БД (Item) при выполнении Update запроса. Модифицирует PayloadValue существующей записи на основе UpdateEntry (из объекта Query). Обновление полей-объектов происходит через модификацию CJSON (построение нового CJSON) c использованием класса CJsonModifier и последующим обновлением всех индексных полей (включая композитные индексы). В случае обновления скалярных полей (полей не объектов) происходит либо обновление индексов, либо обновление CJSON через CJsonModifier https://github.com/Restream/reindexer/blob/master/cpp_src/core/itemmodifier.cc
BaseEncoder Класс построения выходных форматов данных (json/msgpack/cjson/protobuf) на основании -tuple существующей записи. Через BaseEncoder происходит конвертация представления данных Item в JSON или транспортный CJSON. Алгоритм построения выходных форматов основан на рекурсивном обходе json-подобной структуры -tuple с записью данных в выходной WrSerializer поток с использованием Builder (JSONBuilder/CJSONBuilder/MsgPackBuilder/ProtobufBuilder/CsvBuilder) объекта, переданного пользователем как аргумент в метод Encode https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/baseencoder.cc
CJsonDecoder Класс, декодирующий tuple/cjson с последующей записью выходных данных в WrSerializer поток в формате -tuple. Алгоритм работы осуществляется на основе рекурсивного обхода json-подобной структуры CJSON https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsondecoder.cc
CJsonBuilder Класс построения полей CJSON. CJsonBuilder используется BaseEncoder для построения транспортабельного CJSON. Каждое поле кодируется полностью: ctag (поле field = -1) + значение, следующее за ним https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsonbuilder.cc
FieldsExtractor Класс извлечения значений неиндексных полей из CJSON объекта. Поиск и получение значений полей основаны на работе класса BaseEncoder, который использует FieldsExtractor как объект Builder при рекурсивном обходе -tuple. Поле считается найденным, когда значение внутренней переменной expectedPathDepth_ (начальное значение равно длине json path) становится равным нулю (означает правильный уровень вложенности поля) https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/fieldextractor.h
CJson tools Набор функций для работы с CJSON: генерация -tuple на основании PayloadValue, копирование значения поля в CJSON, создание поля-ссылки (индексное поле в -tuple) и неиндексного поля (как неиндексное поле в -tuple, так и поле в «транспортном» CJSON) и т.д.