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.
    Возможно что-то я недопонял, так что буду рад делововой критики от авторов этого замечательного проекта.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 40

    • НЛО прилетело и опубликовало эту надпись здесь
        –2
        спасибо
          –7
          «в бэдграунде» чето новое :)
            –7
            исправьте: нативный клиент на любымом Вам языке
              0
              поправил. спасибо
              –7
              длинна => длина ещё
                0
                ок — поправил
            • НЛО прилетело и опубликовало эту надпись здесь
                +3
                а вам важен фикс или благодарность?
                  0
                  Тут не все так однозначно, публичность может не благодарность иметь своей целью: иногда пишу авторам в личку замечания по орфографии, пунктуации, переводу — исправляют не все, даже когда ошибки очевидные. Всем, кто исправляет — спасибо.
                  • НЛО прилетело и опубликовало эту надпись здесь
                +5
                Одна из причин популярности Redis — простота использования
                Тут, судя по вашему описанию, до простоты API Redis очень далеко
                  –1
                  если Вы поклонник Перла, то клиент Перла позволяет работать с пcевдо SQL, типа писать запросы:
                  SELECT * FROM t0 WHERE k0=124
                    0
                    А почему псевдо? Вы, наверное, имеете в виду, что это — не полностью реализованный SQL.
                      +1
                      это SQL реализованный не по стандарту SQL'92, а адаптироанный применительно к key/value хранилищам, чтоб программистам было легче создавать свое творчество.
                      Примеры:
                      update t0 set k1 = 2, k2 = 3 where k0=1
                      insert into t0 values (4294967295)
                      insert into t0 values (1, 'I am a new tuple')

                      select * from t0 where k0=4294967295
                      Здесь нет обращения к конкретной таблице, так как нет понятия таблиц в принципе. Запрос идет в конкретный неймспейс t0 (в данном случае =0 ), В данном случае важна цифра 0, и с таким же успехом можно написать
                      select * from n0 where k0=4294967295 — результат будет одним и тем же.
                        0
                        то же самое относится и к ключам, важен номер а не имя:
                        select * from n0 where k0=4294967295
                        select * from n0 where m0=4294967295
                        дадут один и тот же результат.
                          0
                          и еще нельзя написатьконкретные поля: SELECT field1,field3 FROM t0 WHERE k0 = 0
                          можно выбрать все поля звездочкой.
                          +1
                          Ох плохая это идея.

                          Скажу как разработчик в свое время (2000-2002) довольно перспективной и очень необычной базы данных, основанной на связных множествах.

                          И одна из задач менеджмента была именно реализация под-множества возможностей SQL99. Типа чтоб кастомерам было легче переходить.

                          В результате это была имхо наиболее ошибочное решение, которое и привело к фейлу. Ошибка в том, что кастомеры искусившись SQL-языком стали писать почти обычные запросы, не воспользовавшись всей мощью entety-relations системы.

                          В общем мне кажется что если бд действительно необычна и дает какие-то существенные приемущества по сравнению с обычной SQL-бд, то не стоит давать возможность писать SQL-like запросы.
                            0
                            не смог найти клиент Перла, пожалуйста, покажите его.
                        +11
                        Мне кажется, чтобы заинтересовать кого-то, нужно хотя бы привести сравнение производительности с существующими системами. А также показать примеры красивого использования, опять же, проводя какую-то параллель с тем же Redis.
                        А когда люди заинтересуются движком, то ли с точки зрения производительности, то ли с точки зрения возможностей, — тогда уже описывать протокол.
                          –4
                          сравнение производительности memcache & rdis можно найти в интернете
                          Я делапл замеры на 100 000 последовательных get/set memcache/redis результат, 16/19 сек
                          с tarantool замеры не делал, но маилрушники его сравнивали с мемкешом и у меня нет причин их замерам не верить.
                            0
                            А что с Tokyocabinet/Kyotocabinet?
                              0
                              я работаю над этим вопросом ;)
                              есть положительные результаты
                              и вот в процессе сравнения с аналогами рождаются такие вот посты
                                0
                                Я делапл замеры для treeDb основанной на Tokyocabinet
                                результаты см здесь www.slideshare.net/akalend/treedb-keyvalue-nosql-database
                                  0
                                  как видно из замеров — чуть-чуть обгоняем мемкеш
                            +6
                            5 копеек: абзац про редис это, извините, не имеет отношение к реальности. не хочется холиварить но и видов данных там достаточно — там не только списки, но и аналогичные вашей структуре ассоциативные массивы и много другого полезного, не говоря уж о базовых вариациях «ключ-значение». клиенты под него есть на всем что шевелится :) и многое другое
                            по сравнению с тарантулом фокус немного другой, так что каждому инструменту своя область, но чем тарантул лучше редиса если честно не видно (производительность думается сравнимая — нового уж давно никто ничего не выдвигал...).
                            имхо!
                              –1
                              и я про тоже но другими словами, что у редиса есть мгного клиентов, по этому он и популярен. У Тарантула их нет, по этому популярность этой БД в тени.
                                0
                                Тарантул — имеет копию данных на диске (уже очень давно), и она не ломается, если сервер или машина падает.
                                Если Вы цените Ваши данные — думаю это важно.
                                То же самое с репликацией — в Tarantool она уже довольно давно.
                                Протокол в Tarantool бинарный, в Редисе текстовый — соотв. бинарный быстрее.
                                Хранимые процедуры в Tarantool пишутся один раз, компилируются один раз, выполняются много раз, в Redis'е только недавно появился eval, то есть каждый раз компилируется.
                                  0
                                  редис тоже имеет копию данных, репликация есть, протокол там *фактически* бинарный — оверхед порядка десятков байт (несеръезный), никакого ескейпинга данных не нужно. эвал в редисе компилируется не каждый раз (скомпиленные куски хранятся в кеше и дергать их можно по хешу не посылая весь скрипт — вообще эта часть активно развивается)
                                +1
                                На wiki проекта нашлись примеры на Ruby.
                                +1
                                Хотелось бы bind-инг к популярным языкам программирования и ссылку на sourceforge с исходниками и собранными бинариками для популярных платформ. Тогда проще будет использовать.
                                  0
                                  согласен,
                                  ссылкой выше клиент на руби.
                                  Больше портов на другие языки нет из-за сравнительной молодости проекта.

                                  Сишный клиент, входящий в состав дистрибутива как таковым не является. Маилрушники обещают его выпустить, но про сроки ничего не известно. стал разбираться с протоколом, чтоб сделать bind-инг к РНР — написал эту статью…
                                    0
                                    Клиенты в процессе, скачать можно с tarantool.org или github.com/mailru/tarantool.

                                    Советую использовать ветку 'master-stable'
                                    0
                                    Я почитал код, и у меня возник вопрос:
                                    Почему у вас LEB128 реализован в обратную сторону?
                                      0
                                      а какой код прочитали? уже столько воды утекло
                                        0
                                        Актуальный мастер на гитхабе
                                        void
                                        write_varint32(struct tbuf *b, u32 value)
                                        {
                                            tbuf_ensure(b, 5);
                                            if (value >= (1 << 7)) {
                                                if (value >= (1 << 14)) {
                                                    if (value >= (1 << 21)) {
                                                        if (value >= (1 << 28))
                                                            append_byte(b, (u8)(value >> 28) | 0x80);
                                                        append_byte(b, (u8)(value >> 21) | 0x80);
                                                    }
                                                    append_byte(b, (u8)((value >> 14) | 0x80));
                                                }
                                                append_byte(b, (u8)((value >> 7) | 0x80));
                                            }
                                            append_byte(b, (u8)((value) & 0x7F));
                                        }
                                         


                                        В обратную сторону:
                                        u32
                                        read_varint32(struct tbuf *buf)
                                        {
                                            u8 *= buf->data;
                                            int size = buf->size;
                                         
                                            if (size < 1) {
                                                tnt_raise(IllegalParams, :"packet too short (expected 1 byte)");
                                            }
                                            if (!(b[0] & 0x80)) {
                                                buf->data += 1;
                                                buf->capacity -= 1;
                                                buf->size -= 1;
                                                return (b[0] & 0x7f);
                                            }
                                         
                                            if (size < 2)
                                                tnt_raise(IllegalParams, :"packet too short (expected 2 bytes)");
                                            if (!(b[1] & 0x80)) {
                                                buf->data += 2;
                                                buf->capacity -= 2;
                                                buf->size -= 2;
                                                return (b[0] & 0x7f) << 7 | (b[1] & 0x7f);
                                            }
                                            if (size < 3)
                                                tnt_raise(IllegalParams, :"packet too short (expected 3 bytes)");
                                            if (!(b[2] & 0x80)) {
                                                buf->data += 3;
                                                buf->capacity -= 3;
                                                buf->size -= 3;
                                                return (b[0] & 0x7f) << 14 | (b[1] & 0x7f) << 7 | (b[2] & 0x7f);
                                            }
                                         
                                            if (size < 4)
                                                tnt_raise(IllegalParams, :"packet too short (expected 4 bytes)");
                                            if (!(b[3] & 0x80)) {
                                                buf->data += 4;
                                                buf->capacity -= 4;
                                                buf->size -= 4;
                                                return (b[0] & 0x7f) << 21 | (b[1] & 0x7f) << 14 |
                                                    (b[2] & 0x7f) << 7 | (b[3] & 0x7f);
                                            }
                                         
                                            if (size < 5)
                                                tnt_raise(IllegalParams, :"packet too short (expected 5 bytes)");
                                            if (!(b[4] & 0x80)) {
                                                buf->data += 5;
                                                buf->capacity -= 5;
                                                buf->size -= 5;
                                                return (b[0] & 0x7f) << 28 | (b[1] & 0x7f) << 21 |
                                                    (b[2] & 0x7f) << 14 | (b[3] & 0x7f) << 7 | (b[4] & 0x7f);
                                            }
                                         
                                            tnt_raise(IllegalParams, :"incorrect BER format");
                                            return 0;
                                        }
                                         
                                          0
                                          Не смог уснуть, не разобравшись.
                                          Да, кажется, что наличие слов «Little Endian» в названии протокола указывает на то, что первым в поток лезет старший фрагмент. Но если вчитаться в спецификацию DWARF, то на страницах 70 и 71 можно найти такой текст:
                                          It is ‘‘little endian’’ only in the sense that it avoids using space to represent the
                                          ‘‘big’’ end of an unsigned integer, when the big end is all zeroes or sign extension bits).

                                          DW_FORM_udata (unsigned LEB128) numbers are encoded as follows: start at the low order
                                          end of an unsigned integer and chop it into 7-bit chunks. Place each chunk into the low order 7
                                          bits of a byte. Typically, several of the high order bytes will be zero; discard them. Emit the
                                          remaining bytes in a stream, starting with the low order byte; set the high order bit on each byte
                                          except the last emitted byte. The high bit of zero on the last byte indicates to the decoder that it
                                          has encountered the last byte.


                                          Отсюда следует, что в статье Википедии, на которую вы ссылаетесь в документации для ознакомления с LEB128, примеры кода правильные, а вот у вас порядок октетов в бинарном потоке не тот.
                                            0
                                            спасибо — буду проверять

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое