NoSQL
24 April 2011

Tarantool Данные и Протокол


Tarantool это замечательное высокопроизводительное no-Sql решение, разработка компании Mail.Ru. Исходники

Данное решение позволяет использовать как режим key/value, так и выборку множества записей в рекордсет по одному или нескольким критериям (полям поиска). Аналогов в рунете и не только, я пока не встречал. С натяжкой можно сравнить редис. Но в редисе — списковые данные и их нельзя выбирать по ключу. Судя до утверждениям разработчиков, скорость доступа по ключу превосходит memcache, при этом еще в бэдграунде осуществляется постоянное сохранение данных на диск. Но к сожалению, данная разработка имеет единственный perl клиент для доступа к данным, из-за чего не имеет такой популярности, как например у redis или memcache.

В doc/box-protocol источников есть описание Протокола, которое я в настоящее время переработал для написания клиента на Си и PHP. Изучив Протокол, вы можете реализоать нативный клиент на любимом Вам языке. Надеюсь, данная статья в этом Вам пригодится.


Все данные в этой базе разделены на namespace, скорее всего это аналог БД в MySQL. Нумерация всех неймспейсов — цифровая ( 0, 1, 2 и тд). На каждый namespace можно наложить определенный индекс. Нумерация Индексов — так же цифровая. Индексы накладываются на одно или несколько полей. Индекс может иметь тип HASH или TREE

Все Индесы и неймспейсы прописываются в Конфиге. Ниже показан пример, где прописано два индекса, цифровой и символьный. При том, второй индекс составной:namespace[0].enabled = 1
namespace[0].index[0].type = "HASH"
namespace[0].index[0].unique = 1
namespace[0].index[0].key_field[0].fieldno = 0
namespace[0].index[0].key_field[0].type = "NUM"
namespace[0].index[0].key_field[1].fieldno = 1
namespace[0].index[1].type = "TREE"
namespace[0].index[1].key_field[0].fieldno = 0
namespace[0].index[1].key_field[0].type = "STR"
namespace[0].index[1].key_field[1].fieldno = 1

Необходимо отметить про ключи. Ключи могут быть цифровыми (1,2,3… 6.2 *10^9 ) или символьными.

Все данные в неймспейсе хранятся в ввиде кортежей. Картеж представляет собойй набор полей. Поле может быть либо цифровым, либо символьным.

Обмен между клиентом и сервером осущестляется Сообщениями. Все Сообщения в tarantool Протокол делятся на Запрос Request и ответ Responce. Каждое Сообщение имеет обязательный Заголовок Header и так же может иметь Tело.

Заголовок Сообщения включает: тип Сообщения, длину тела и идентификатор запроса.

Структура Заголовка:
<blockquote>typedef  struct {
		uint32_t type;
		uint32_t len;
		uint32_t request_id;
} Header;
</blockquote>

Определены следующие типы Сообщений:
INSERT 0xd (13)
SELECT 0x11 (17)
UPDATE 0x13 (19)
DELETE 0x14 (29)
PING 0xff 0xff 0x0 0x0 (65280)

Идентификатор запроса устанавливается клиентом и может быть нулевой.

Общая структура запроса:
typedef	struct {
	Header header;
	union {
		InsertRequest insert;
		SelectRequest select;
		UpdateRequest insert;
		DeleteRequest insert;
	};
} Request;


Команда PING не имеет тела, по этому отсутствует PingRequest ;)

Тело команды INSERT состоит из номера namespace, над которым будет осуществляться операция, флага и кортежа.

namespace — Это пространство, в котором хранятся кортежи. Нумерация неймспейсов цифроая. В каждом неймспейсе могут быть определены индексы. Первичный индекс (PRIMARY) должен
присутствовать обязательно. Индексы определены в конфигурационном файле.

В настоящее время определен только единственный флаг BOX_RETURN_TUPLE (0x01) который показывает, вернуть ли данные в теле ответа.
Структура INSERT запроса:
typedef	struct {
	uint32_t namespaceNo;
	uint32_t flag;
	Tuple tuple;
} InsertRequest;


Все данные описываются с помощью кортежей Tuple. Кортеж состоит из поля cardinality, которое представляет собой размерность кортежа (кол-во полей) и массива полей. В общем виде это будет выглядеть так:
typedef	struct {
	uint32_t card;
	Field field[];
} Tuple;


Каждое поле представлено массивом байт. Поле может иметь: int32, int64 или потоком байт.
В настоящее время я пока определил так:
typedef Field u_char * data;

Все данные полей упакованы с помощью LED128 en.wikipedia.org/wiki/LEB128

тело SELECT запроса включает: номер namespace, номер индекса, по которому происходит выборка, смещение offset и размер вывода limit и кол-во кортежей и сами кортежи. Параметры offset и limit аналогичны выборке: SELECT * FROM… LIMIT

typedef	struct {
	uint32_t namespaceNo;
	uint32_t indexNo;
	uint32_t offset;
	uint32_t limit;
	uint32_t count;
	Tuple tuples[];
} SelectRequest;


Если мы указываем SELECT * FROM t0 WHERE k0=1, то кол-во кортежей=1 и значение Tuple должно соответствовать 1. Если определен вторичный составной индекс k1 (цифровое поле и символьное), то на запрос
SELECT * FROM t0 WHERE k1 = (21, 'USSR')
кол-во кортежей=2 и должно быть представлено два значения Tuple. Необходимо сделать пояснение, что представленный sql схематичный, и не соответствует стандарту SQL'92. Дело в том, что данные в tarantool/box представлены кортежами, а не таблицами (столбцы и строки). На кортеж может содержать любое кол-во полей. Все кортежи хранятся в немспейсе. Однако на мемспейс можно налажить HASH или rbTREE индекс по которому будет осуществлен поиск.

Тело UPDATE запроса включает: номер неймспейса, флаг, кортеж, кол-во операций и сами операции. Поля флаг и кортеж аналогичны операции UPDATE. Кол-во операций может быть равно нолю. Структура будет выглядеть следующим образом:

typedef	struct {
	uint32_t namespaceNo;
	uint32_t flag;
	Tuple tuple;
	int32_t count;
	Operation operation[];
} UpdateRequest;


Каждая Операция представляет собой структуру, содержащей номер поля по которому будет проведена операция, код операции и аргумент.

Используются коды операций:
0 — присвоение аргумента данному полю.
Если аргументом является тип int32, то так же возможны следующие действия:
1 — добавить аргумент к существующему полю
2 — выполнить AND с существующем полем
3 — выполнить XOR с существующем полем
4 — выполнить OR с существующем полем
typedef	struct {
	int32_t fieldNo;
	int8_t  opcode;
	Field   arg;
} Operation;


Операция DELETE всегда выполняется по первичному ключу и содержит номер неймспейса и кортеж. Структура операции DELETE представлена ниже:
typedef	struct {
	uint32_t namespaceNo;
	Tuple tuple;
} SelectRequest;


Каждый ответ сервера содержит: Заголовок Header, код ответа и при необходимости тело ответа. Заголовок ответа — аналогичен заголовку запроса. Код возрата 0 — успех, либо смотрим ошибки в include/iproto.h

В общем виде получается следующая структура ответа:
typedef	struct {				
		Header header;
		int32_t code;
		union {
			SelectResponce selectBody;
			insertResponce insertBody;
			uint8_t * data;
			int32_t count;
		};			
} Responce;


Тело ответа на запрос SELECT стостоит из поля содержащего общее кол-во кортежей и самого множества возвращаемых кортежей. Если результат запроса пустой, то кортежи не возвращаются и поле количества содержит ноль.
typedef	struct {
	int32_t count;
	FqTuple tuples[];
} SelectResponce;


Каждый возвращаемый кортеж (FqTuple) содержит размер кортежа, некий идентификаро cardinality, который выступает как разделитель (boundaries) и самого кортежа.
typedef	struct {
	int32_t size;
	uint32_t card;
	Tuple tuple;
} FqTuple;


Если в запросе InsertRequest установлен флаг BOX_RETURN_TUPLE, то ответ может содержать тело:
typedef	struct {
	int32_t count;
	FqTuple tuple;
} InsertResponce;


Аналогичным является и ответ на запрос UPDATE.

Запрос Delete возвращает количество удаленый записей. Так как при удалении используется только первичный индекс, то мы можем удалить только одну запись, соответственно вернется 0 или 1. Это поле count структуры Responce. Еще в структуре выделен массив байт data для анализа данных.

Возможны в тексте есть некоторые неточности, где-то можно использоать int32_t вместо uint32_t.
Возможно что-то я недопонял, так что буду рад делововой критики от авторов этого замечательного проекта.

+34
4.9k 56
Comments 40