Наверх ▲

12 примеров использования Redis - в Tarantool

Константин Осипов Константин Осипов Разработчик и архитектор СУБД Tarantool. Александр Календарев Александр Календарев Активный пользователь Tarantool/Box, автор PHP драйвера.

Константин Осипов: Доброе утро! Меня зовут Константин Осипов. Я хотел бы понять, насколько аудитория знакома с Tarantool, который я разрабатываю. Мы собираемся сравнивать его с Redis. В принципе, архитектура похожа, но не совсем. Моего коллегу по докладу зовут Александр Календарев. Александр использует Tarantool в одном из своих проектов. 

Прежде чем мы начнем, я немного расскажу про Redis и Tarantool в сравнении. Архитектуры во многом похожи, но мы уделим внимание каким-то определенным моментам. Redis считается сервером структур данных (англ. Data Structure Server). Он хранит в памяти структуры данных и предоставляет доступ к ним через сеть. При этом можно с помощью снимка (англ. snapshot) сохранить данные на диск. У Redis есть такая возможность, как "Append Only File" (то, что в Tarantool называется Write-Ahead Log, в этих двух названиях заложена семантическая разница), которую можно включить, и тогда все изменения будут также попадать в некоторый файл. Ваши изменения будут сохраняться примерно с той же скоростью, с какой они происходят.

Кооперативная многозадачность

Tarantool и Redis также используют кооперативную многозадачность. Это относительно современный подход. Хотя даже MySQL в 2000-м году использовала "Meet on Threads", там тоже была кооперативная многозадачность. Современный подход к многозадачности таков: используется максимум один процессор. При этом параллельные потоки в программе передают управление кооперативно (то есть друг другу).

Каковы преимущества этого подхода?

Это подход без блокировок. Не нужны ни мьютексы, ни точки синхронизации, потому что данные общие, и в один момент времени с ними работает один процессор. Соответственно, все издержки таких баз данных, как PostgreSQL, MySQL, вообще любых многопоточных и многопроцессных баз данных здесь исчезают. Эта модель похожа на модель Erlang, в которой узлы обмениваются между собой сообщениями вместо того, чтобы использовать общую память. Это другая модель использования данных.

У кого есть вопросы по тому, что я сказал? Нет? Ничего непонятно? 

Вопрос из зала: Правильно ли я понимаю, что один экземпляр Tarantool может использовать только одно ядро?

Константин Осипов: Правильно ли вы поняли, что один экземпляр Tarantool использует одно ядро? Совершенно верно. Типичная конфигурация – это то, что на одной машине работает два-четыре экземпляра. Просто за счет того, что вы все равно шардите ваши данные, вы разбиваете их между несколькими экземплярами на одном компьютере.

Вопрос из зала: Не совсем понятно по поводу мьютексов. Вы говорите, что их не нужно использовать вообще, получается?

Константин Осипов: Да, Tarantool и Redis не используют мьютексы вообще. Синхронизация как таковая не нужна. Все, что нужно выполнить атомарно, выполняется атомарно. Управление другой сопрограмме либо другому "зеленому потоку" не отдается до тех пор, пока эта критическая секция не завершена. В мьютексах нет необходимости.

Александр Календарев: Я хотел просто добавить. Вчера был очень похожий доклад, где рассказывали про фреймворки, HTTP-сервер. Там как раз была похожая технология. Используется библиотека libcore. Там на одном потоке используются как раз эти параллельные вычисления. 

Вопрос из зала: Почему название такое ядовитое?

Константин Осипов: К сожалению, этот вопрос преследует меня, как чума. Название придумали (я не буду говорить «воспаленные») усталые умы программистов, поэтому не судите строго. В принципе, это инструмент, "tool". Если вам не нравится слово "taran", можете говорить, что это "tnttool". Это просто некий инструмент. 

Название отражает модульную структуру сервера. Есть некий фреймворк для работы с сетью, для работы с диском, и есть некий модуль (бокс), который отвечает за хранение данных. 

Теперь я перейду к докладу. Рассказывать мы будем вдвоем. Александр написал для нас драйвер на PHP. Он будет рассказывать про вещи, связанные с PHP. Я буду рассказывать про разные сценарии, про Lua, про хранимые на Lua процедуры и отвечать на вопросы с позиции разработчика.

Александр Календарев: Секундочку. Мне просто хотелось бы знать, как много здесь PHP-разработчиков? Больше половины зала – это уже хорошо. Сегмент рынка PHP – где-то 70 %.

Константин Осипов: О чем наш доклад? Мы немного поговорим о модели данных. Она у нас тоже своя. Сейчас все изобретают модели данных. 

Мы поговорим о производительности. Поговорим о доступе из PHP. Хотя доступ есть из других языков, это совершенно точно.

Расскажем о паттернах. Заранее скажу, что паттернов у нас не 12. 12 точно бы не влезли. Но практически все, что вы можете сделать с Redis, вы можете сделать и с Tarantool. Тут нужно, конечно, смотреть по использованию памяти. Возможно, где-то одно решение будет выигрывать, где-то другое. 

Рассмотрим масштабирование. Пока это не самая сильная наша сторона, но мы можем это делать. Просто этого требует труда.

Также мы поговорим о наших планах.

Модель данных Tarantool

Я перейду к модели данных. Модель данных Tarantool близка, скорее, к реляционной СУБД, но не совсем. Если в реляционной СУБД есть таблицы, у нас это называется пространствами. В пространстве хранятся кортежи. 

Кортеж не имеет ограничений по длине. У кортежа может быть 10, 20, 200 полей – сколько угодно. В течение жизненного цикла размерность (англ. dimension) кортежа может меняться. Вы можете добавлять, удалять поля. Кортеж – это ассоциация между ключом и списком полей. Ключ – это всегда первое поле кортежа. 

Что я пытался изобразить на слайде? По коллекции кортежей, которая из себя представляет пространство, можно строить индексы. Всегда есть первичный индекс, первичный ключ. Он по первому полю. 

Можно строить вторичные ключи. Их можно строить по 2-му, 3-му, 50-му полю и так далее, как вам удобно. Соответственно, если у вас есть индекс по какому-то из полей кортежа, то это поле обязательно должно присутствовать в каждом кортеже, который есть в этом пространстве.

Почему используется такая модель? В отличие от реляционных систем, мы не хотим заставлять вас сразу планировать вашу схему. Модель структур данных – это нечто более сложное. Нет изобретения модели как таковой. Это, я бы сказал, облегченная, слабореляционная модель (англ. relaxed relational model). Нет ограничения реляционной модели, но при этом модель близка к реляционной. 

Типы данных и индексов

Что еще хотелось бы упомянуть? Типы данных, которые у нас есть: числовой 32-битный, 64-битный и строковый. Вообще о типах данных мы не задумываемся до тех пор, пока нам не нужно строить индексы. 

Зачем нужны типы данных Tarantool? Просто для того, чтобы правильно их сортировать и хранить в индексах. Если у вас нет индексов, то вы можете в каждом поле хранить какое угодно значение. Мы об этом ничего не знаем. Вы можете создать кортеж, в котором одни сплошные "int", потом в тот же самый кортеж записать строки. Если нет индексов, то нет никаких проблем.

Типы индексов – это хеши и "деревья". Зачем нужны "деревья"? "Деревья" позволяют получать доступ к данным в отсортированном виде. Если у вас есть "деревья", вы можете получить максимальный элемент, минимальный элемент, проитерировать от одного ключа до +10 ключей, которые больше него в этой коллекции. "Дерево" – это отсортированная коллекция. 

Помимо этого, индексы могут быть составными (индекс может покрывать несколько полей, точно так же, как в реляционной БД). Мы поддерживаем 4 CRUD-операции: INSERT/UPDATE/DELETE/SELECT. Операции выполняются по первичному ключу. Вторичные индексы используются для операций "select". Можно выполнять операцию "select" по вторичному индексу. 

У нас есть SQL, но не для того, чтобы сказать, что мы SQL СУБД. Мы относимся к NoSQL или Not only SQL. Опять же – мы не изобретаем новый язык программирования для вас. Если вы знакомы с синтаксисом MySQL, то это его подмножество (INSERT/UPDATE/DELETE), пожалуйста, можете использовать его для доступа к данным.

Александр Календарев: Хочу добавить несколько слов. Я как пользователь использую SQL из административной консоли, чтобы посмотреть, как у меня "легли" данные. В основном, для отладки.

Теперь хочу немного рассказать про наш PHP-интерфейс. PHP-интерфейс выполнен в виде одного класса. Ничего нового я тут не придумал. У класса есть конструктор, в котором имеются параметры соединения. Это "host", "port" и еще есть так называемый административный порт. Для чего он нужен? Там хранится служебная информация. Мы можем выполнять не только действия с данными, но и с помощью определенных команд посмотреть, какие используются индексы, параметры среды и тому подобное. 

Теперь о соединении. Соединение здесь сделано отложенным.

У нас происходит соединение при первом обращении. Все данные там были по умолчанию (я забыл это упомянуть). 

Далее хочу сказать немного о стиле программирования. Так как у нас Tarantool использует только цифры (это сделано для более быстрого доступа), то желательно в какой-то секции сделать весь define. У меня define – пространство 0, пространство 1, индекс такой-то. Так проще и понятнее для программиста.

Константин Осипов: Можно, я тебя прерву? Что значит "мы используем цифры"? Идентификаторы – это цифры. Это что-то вроде объектных идентификаторов. Все пространства пронумерованы, все индексы в пространстве пронумерованы. Нет имен. Если для SQL-таблиц можно задать свое имя, у нас просто номер.

Соответственно, протокол бинарный. В этот протокол пакуется номер – опять же для простого доступа, для простого создания всяких прокси и так далее, мультиплексоров именно на уровне протокола для Tarantool.

Александр Календарев: На слайде представлено 3 оператора изменения данных – INSERT, DELETE и UPDATE. Insert, вставку мы выполняем с каким-то пространством. Во всех моих операторах первым параметром идет то пространство, с которым мы работаем. Вторым параметром идут данные в виде массива. Массив представляет собой перечень каких-либо данных. Это могут быть как цифры, так и строки.

Второе действие – удаление, delete. Представляется только первичный ключ. Есть некий флаг. Флаг по умолчанию "first". Если это трое, то у нас возвращаются те данные, которые были удалены. Зачем это нужно, мы расскажем далее.

Обновление (Update) - это третье действие. Чем обновление отличается от вставки? Тем, что оно может выполнять изменения конкретных полей. Эти конкретные поля показаны в массиве. Допустим, мы изменяем первое поле. Там "spb" было, меняем на Москву ("msk"). Второе поле – было "Hello Word", изменили на "Hello Hi++".

Константин Осипов: Мне тоже есть что добавить. У всех операций есть возможность возвращать кортеж, с которым они работали. Если вы сделали вставку, вы можете сразу получить его назад обновленным. Вы можете получить назад обновление при удалении. Вы сразу можете получить назад кортеж, который вы удалили.

Если есть какие-то вопросы, простые вопросы задавайте сразу, длинные откладывайте до конца, будет отдельное время для вопросов.

Вопрос из зала: Автоключ есть?

Константин Осипов: Мы об этом расскажем. Вопрос был – есть ли автоинкрементные ключи.

Вопрос из зала: Первый ключ можно заменить?

Константин Осипов: Можно ли сделать замену первичного ключа?

Александр Календарев: Можно. 

Константин Осипов: Каким образом, я не совсем понимаю?

Александр Календарев: Можно, но не нужно.

Реплика из зала: Индекс 0 и новое значение.

Константин Осипов: Через одну операцию все-таки нельзя. Придется сначала удалить. Обновление всегда идентифицируется ключом. Вы предлагаете через действие "update"? Я думаю, что да, можно. 

Александр Календарев: Я экспериментировал – можно. Но практически – я не нашел, для чего это нужно.

Константин Осипов: Пожалуйста.

Вопрос из зала: На предыдущем слайде были "host" и "port", к которым мы подключаемся. Как быть, если несколько экземпляров?

Константин Осипов: Вопрос был, как определять host и port при нескольких экземплярах? Это "решает" Tarantool Proxy, о нем мы поговорим.

Вопрос из зала: Составной ключ. Как будет происходить работа с составным ключом?

Константин Осипов: В примере (на слайде) в качестве ключа передается скаляр (ключ – это либо число, либо строка). Но вы также можете передать массив. Это будет составной ключ.

Реплика из зала: В качестве составного ключа передается массив.

Александр Календарев: Да, у меня реализовано. Здесь представлен оператор "select", который делает выборку. Также есть оператор "multiselect" ("mselect"). Это аналог "multiget" в Memcached. Я о нем чуть позже расскажу.

Что собой представляет "select"? Это номер пространства, с которого мы делаем выборку, номер индекса и сам ключ. Номер индекса, как мы говорили, может быть простым или составным. Я просто до этого еще не дошел. Если у нас простой индекс, то ключ у нас либо число, либо строка. Если у нас индекс составной, то ключ представляет собой массив. При этом первый элемент массива – первый индекс, второй элемент массива – второй индекс, следующий массив.

Константин Осипов: Компоненты индекса. Первые и вторые компоненты.

Александр Календарев: Дальше есть два необязательных параметра – "limit" и "offset". Это аналог MySQL – те же "limit" и "offset".

Константин Осипов: Соответственно, "limit" и "offset" имеют значение, только если вы выбираете по неуникальному ключу. Если вы выбираете по уникальному ключу, у вас всегда один кортеж в результате. Либо, если у вас "mselect", "multiget", то есть вы выбираете кучу ключей сразу, но хотите первые 10 ключей из этого диапазона, которые вы передаете в Tarantool.

Вопрос из зала: Не является ли опасным обращение к полям по идентификаторам в случае изменения структуры данных?

Константин Осипов: Конечно, является. Но вы имеете дело с продуктом, который начинает свой жизненный путь. Мы, безусловно, будем поддерживать имена, но мы будем поддерживать их на уровне синонимов. 

Если в базе данных вы всегда привязываетесь к имени столбца, то у нас просто уникальным идентификатором будет число. Мы дадим возможность задавать синонимы для чисел любого рода. Это пока у нас в планах, мы об этом не упомянули. Сейчас физически вы всегда обращаетесь по номеру, да.

Вопрос из зала: Лучше не удалять поля?

Константин Осипов: Вы удаляете поля… На практике такой проблемы у нас пока не было. Везде поля удаляются. Но если они удаляются, то этот кортеж представляет собой список или какую-то такую структуру, или FIFO. У удаляемых полей относительный индекс может измениться, вам это и надо. Нужно, чтобы другое поле "съехало" на его место по номеру. Тогда вы получите другое поле. На практике такой проблемы пока не было.

Вопрос из зала: Получается, у нас пространство на каждый запрос указывает. Насколько это актуально? Может быть, пространство – это свойство объекта, так как на разных пространствах разные объекты. Какова идеология пространства?

Константин Осипов: Пространство – это коллекция. Вы должны указать, в какую коллекцию вы обращаетесь. Есть поддержка большого количества коллекций. Идеология простая. Пространство – это таблица. Как угодно можете смотреть на это. Я думаю, что в большинстве NoSQL СУБД это называется коллекцией. И в Mongo, и в Couch, и в MemcacheDB. Мы можем двигаться дальше?

Александр Календарев: Сейчас, я еще недорассказал. Я хотел сделать сравнение. Если у нас есть, допустим, запрос по составному ключу, то аналог SQL-оператора будет "select*from имя таблицы в key1 and key2". Если это у нас несоставной ключ, то без "and." 

Также я тут упомянул про оператор "mselect", который имеет точно такой же синтаксис. Там ключ представляет собой массив ключей. Выбор будет аналогичен: "select*from в key in" и множество ключей.  

Второй момент, о котором хотелось бы сказать. И "mselect", и "select" возвращают количество имеющихся данных. Они не возвращают сами данные. Сами данные возвращаются по циклу оператором "getTuple".

Константин Осипов: Пожалуйста, ваш вопрос.

Реплика из зала: У меня был как раз вопрос по поводу "select". 

Константин Осипов: Отлично. Значит, мы ответили на него. Тогда давайте двигаться дальше.

Я сейчас хотел бы поговорить о производительности. На самом деле, я лично не верю ни одному тесту, который я не написал и не выполнил сам. Поэтому я призываю и вас мне не верить. Тем не менее, для себя мы выполняли какие-то тесты, чтобы вообще понять, на каком уровне мы находимся.

Самый интересный опыт, который мы получили: есть DD, запись из Zero в файл на обычной машине дает производительность около 50-60 (ну, до 100) мегабайт в секунду. Большинство NoSQL-решений, включая Tarantool, дают на порядок меньше – 5-10 мегабайт в секунду в записи. 

Как в цифровом отношении это выражается? Есть машина Intel I5, 4 гигабайта RAM, ей где-то года 3. На производительность напрямую влияет даже не скорость диска, а скорость шины, памяти и процессора. Мы добились 120 тысяч запросов в секунду для записи, 270 тысяч (это для Redis). Tarantool – 100 тысяч запросов в секунду по записи и 260 тысяч по чтению. 

Есть еще более современное решение Intel I7. Мы здесь говорим о цифрах порядка 700 тысяч запросов на чтение и 300 тысяч запросов на запись. Конкретно в этом сконструированном тесте мы выигрываем у Redis. Я покажу, как примерно работает этот цикл тестирования.

Что здесь происходит? 10 потоков выполняют "select" в параллель. Это тест производительности на локальной машине. При сетевом тесте нужно как минимум 200-300 потоков, чтобы выжать эту производительность просто потому, что сетевые задержки скрадывают многое. Мы выбираем размер кортежа, начинаем с 200 байт в начале.

Вопрос из зала: Как Redis конфигурировать?

Константин Осипов: Redis конфигурировать с "Append Only File", в остальном – настройки по умолчанию. "Append Only File" включен. Tarantool сконфигурирован так, чтобы он синхронизировал данные каждую 0,01 секунды. 100 раз в секунду делается fsync для данных. Мы вставляем 300 тысяч кортежей. 

Запись начинает «втупливать» на уровне кортежа где-то в 350 байт. Что значит «втупливать»? Мы начинаем около 300 тысяч и заканчиваем на уровне 100 тысяч. При увеличении размера кортежей у нас падает скорость операций.

Вопрос из зала: Вы сейчас говорите про один экземпляр?

Константин Осипов: Да, про один экземпляр Tarantool.

Вопрос из зала: Write – это insert?

Константин Осипов: Write – это insert. Я призываю вас этим тестам не верить и продемонстрировать нам, что мы недостаточно производительная СУБД. Еще я могу сказать со своей стороны, что мы активно работаем в сторону приближения к dd, но к dd не из dev_zero. Над производительностью мы продолжаем работать. 

Вопрос из зала: Скажите, почему такой маленький размер – 300?

Константин Осипов: Что такое 300 байт?

Вопрос из зала: Да.

Константин Осипов: Например, типичный размер сессии, которую вы храните, а пользуетесь из Tarantool, типичное использование – это сессии. Это какие-то данные, которые достаточно ценны, но не на столько, что вы хотите доверить их NoSQL. Это 300 байт.

Вопрос из зала: Например, сортировку я в Redis делаю, достаточно большой объем. Что-то такое есть?

Константин Осипов: Если вы делаете сортировку в Redis, вы последовательно добавляете ключи в Redis. Правильно? Потом получаете их в отсортированном виде. Соответственно, в нашем случае вы будете использовать пространство с "деревом" в качестве индекса. Вы также будете добавлять в это пространство ключи (в нашем случае – кортежи). Потом вы будете получать из этого пространства ваши данные. Если вы делаете сортировку через Redis, то у вас размер запроса равен размеру вашего сортируемого элемента. 

Вопрос из зала: Насколько сильно будет тормозить в зависимости от объема сохраненных данных?

Константин Осипов: Классный вопрос. Мы in-memory хранилище, поэтому у нас доступ к каждому ключу имеет одинаковую скорость. 

Как мы тратим память? Если у вас сценарий такой, что вы можете кучу данных вытолкнуть на диск и работать только в режиме кэша, и у вас только 10 % ваших данных используется активно, возможно, Tarantool не для вас. Поэтому производительность остается такой же. 

На что влияет объем данных – на время старта и остановки. Если вы стартуете с нуля, у вас куча данных, то вы читаете эти данные с диска. Допустим, 10 гигабайт читается с диска примерно 20-40 секунд с учетом построения индексов и тому подобное. Нужно прочитать все данные в память, построить все индексы. Индексы на диске мы не храним. Мы их строим, когда стартуем.

Вопрос из зала: Что если памяти не хватит?

Константин Осипов: Конфигурируется размер памяти, которую вы отводите под кортежи. Соответственно, если памяти не хватает, Tarantool просто не будет позволять оставлять новые данные.

Александр Календарев: Но для этого есть масштабирование.

Константин Осипов: Я еще хотел сказать, что обычно у вас в одной ноде не хранится много данных. Почему? Типичная конфигурация. Допустим, на машине 64 гига памяти. Вы там пускаете 2 или 4 ноды, каждой ноде даете по 10 гигов. У вас есть память, чтобы брать снимки (англ. snapshots). Для них мы используем технику Copy-On-Write. 

У нас мгновенный консистентный снимок. Снимки обычно администраторы Mail.Ru берут по ночам, когда загрузка минимальна. Если у вас не слишком много обновлений, то ваши снимки практически ничего не потратят. 

Вопрос из зала: Я так понял, что вы сравниваете производительность записи просто строковых данных в Redis и Tuple из одного значения в Tarantool?

Константин Осипов: Из двух значений. Ключ значения. 

Вопрос из зала: Если говорить о более длинных Tuple, то, наверное, их следует сравнивать в Redis со списками...

Константин Осипов: В данном случае вопрос: если мы говорим о таких структурах данных, как списки Redis, с чем сравнивать их в Tarantool?

Реплика из зала: Меня интересует производительность.

Константин Осипов: Насколько влияет производительность на размер Tuplt? Это классный вопрос. По нашим измерениям, где-то до тысячи элементов вам хватает. Tuple из себя представляет некое упакованное значение в памяти. Если вы постоянно получаете доступ к тысячному элементу, то вам в пределе нужно сделать итерацию по всем. 

На диске ваше обновление хранится в компактном виде. Если вы добавляете 1000-й элемент, 1001-й элемент, то на диск идет запись именно этой команды: «Добавить Tuple такому-то элемент такой-то». Мы не перезаписываем данные на диск всегда, когда вы что-то добавляете. Нам нужно просто распаковать и запаковать новый Tuple. По ЦП производительность может снижаться при увеличении размера Tuple, но не по диску ввода-вывода.

Я хочу также упомянуть, что одна из наших целей – начать работать с Tuple по 10, 20 тысяч значений. Мы движемся в эту сторону.

Давайте вернемся к слайдам.

Мы даже не дошли до основной части нашего доклада. Я сейчас расскажу про автоинкремент. Я подозреваю, что аудитория должна знать, что такое автоинкремент. Автоматическое присваивание первичного ключа новой записи.

Александр расскажет, как это делать в PHP, я расскажу, как это делать с помощью хранимых процедур на Lua.

Александр Календарев: Здесь, в принципе, все программисты. Все интуитивно понятно. Есть операция "increment". Это упрощение "update". Это увеличение на единицу конкретного поля. Значение пространства, номер пространства, номер ключа, по которому мы делали, и номер поля, по которому увеличиваем. По умолчанию это всегда «единичка». В некоторых случаях нам нужно увеличить на «двоечку», на «троечку». Это необязательные поля. Также есть значение флага, которое возвращает это поле. 

Что мы сделаем. Мы в отдельном пространстве имен (я его называю «служебное», как правило, это нулевое, мне просто так удобно) храним разные служебные счетчики, которые обслуживают структуру других пространств. Дальше мы увеличиваем значение этого счетчика. Следующей операцией мы просто-напросто вставляем данные.

Так же "auto-increment" работает и в MySQL. Но здесь есть одно узкое место.

Константин Осипов: Здесь, на самом деле, нет узкого места.

Вопрос из зала: Насколько велика вероятность конфликта при развитии в разных потоках, вероятность того, что функция "increment" выдаст одинаковое значение?

Константин Осипов: Я просто поясню. Здесь два пространства. Каждый "update" в Tarantool атомарный. Соответственно, ваш "update" возвращает вам значение. Вы получаете свое уникальное значение инкремента всегда. Конфликтов в этом конкретном случае быть не может. Но мы сейчас продемонстрируем еще более простой способ, а именно автоинкрементную функцию на Lua.

В Tarantool вы можете создавать свои хранимые процедуры. Из хранимых процедур есть доступ к базовым функциям сервера. Хранимые процедуры – это клиент, который выполняется внутри сервера. Можно выполнить любое из действий – insert, update, delete, select. Можно получить Tuple, его распаковать и взять какие-то поля. Можно взять данные и послать их клиенту из хранимой процедуры.

Как работает хранимая процедура?

Все, что вы возвращаете из хранимой процедуры, возвращается на клиент. При этом если одна хранимая процедура вызывает другую хранимую процедуру. Все, что вы возвращаете из вызываемой хранимой процедуры, просто возвращается как возвращаемое значение функции в любом языке программирования. 

Как это можно воспринять? Наверху сидит главный вызывальщик. Все, что ему возвращают хранимые процедуры, он отправляет клиенту.

На слайде мы видим, что у нас в Lua есть доступ к Tarantool как к некоему встроенному хранилищу. В модуле "box" есть массив всех пространств, к которому мы можем обращаться по номеру пространства, в котором есть массив всех индексов, по номеру индекса. Индекс – это объект. У этого объекта мы можем вызвать метод – max, min, текущее число значений в индексе (узнать количество значений). Можем проитерировать по индексу и так далее. 

Как здесь работает автоинкремент? Мы просто вызываем хранимую процедуру, которая получает текущий максимум из индекса в этом пространстве и вставляет значение с max+1. Все это выполняется атомарно на сервере.

Вопрос из зала: После максимума там индексы блокируем?

Константин Осипов: У нас блокировок нет. Это выполняется атомарно без прерываний. Понимаете, большинство СУБД теряют как раз на блокировках. За свою работу в MySQL (я пришел в Mail.Ru из MySQL) я занимался тем, что ускорял производительность подсистемы блокировки MySQL.

Александр Календарев: Однопоточный сервер.

Вопрос из зала: Что, если процедура зависнет?

Константин Осипов: Не нужно писать "глючных" процедур. Типичная инсталляция Tarantool подразумевает, допустим, 5-10 отлаженных процедур под конкретный проект. Они отлаживаются, выполняются, но при этом любую процедуру можно обновить "на лету". Если у вас процедура с ошибкой, вы можете зайти в административную консоль, перекомпилировать ее, дать ей новое определение – и она тут же начинает использоваться во всех ваших клиентах.

Вопрос из зала: А с зависшими как быть?

Константин Осипов: К сожалению, если у вас зависла процедура, вы на административную консоль не зайдете.

Вопрос из зала: Что в этом случае?

Константин Осипов: "Убить" – вот и все.

Вопрос из зала: Два вопроса. Что по производительности? Насколько я вижу, здесь также предлагается вызывать из клиента эту процедуру, которая возвратит вам новый инкремент. Почему бы…

Константин Осипов: Нет, она не возвращает новый инкремент. Она делает вставку. Из клиента мы вызываем процедуру с нашими данными. Она все вставляет.

Вопрос из зала: Окей. Тогда что по производительности?

Константин Осипов: Это, на самом деле, ответ еще на один вопрос, который мне задают примерно так же часто, как вопрос «почему Tarantool называется Tarantool». Это, по сути, вопрос о том, почему мы выбрали Lua, а не Python, JavaScript – что угодно. Lua – это один из самых быстрых компилируемых "на лету" языков. Пожалуй, самый быстрый по тестам. Поэтому он и был выбран. Мы используем LuaJit 2.0 в нашем сервере.

Вопрос из зала: Все-таки, если процедура зависла, что с клиентом будет? Он тоже зависнет?

Константин Осипов: Да, клиент тоже зависнет. Смотрите: здесь нечему виснуть, если у вас все правильно написано. Вы отлаживаете ваши процедуры более или менее… Пока что мы не дошли до того уровня, что…

Вопрос из зала: Хорошо. Если вылетела память или еще что-то…

Константин Осипов: Что происходит, если в процедуре возникает ошибка? Tarantool внутри себя использует исключения.

Вопрос из зала: Он вернет их?

Константин Осипов: Да, любая ошибка хранимой процедуры возвращается клиенту в виде ошибки err.proc.lua. У нас есть набор стандартных ошибок. Они все задокументированы. Если у вас исключение в Lua, мы возвращаем исключение в Lua. 

Допустим, box.insert в конечном итоге вызывает код, написанный на "Си". Точнее, мы используем "Objective C". Если в этом коде произойдет исключение, то исключение пройдет сквозь хранимую процедуру и опять же вернется клиенту. Это одна из причин использовать LuaJit, потому что она совершенно шикарно сквозь себя "пробрасывает" исключения на GCC. Мы используем GCC для компиляции. Обработка ошибок выглядит очень просто.

Вопрос из зала: Можно еще вопрос? Вы сказали, что эта процедура выполняется атомарно. Что это означает? Что если другой клиент вызовет эту процедуру? Что произойдет?

Константин Осипов: Что подразумевает атомарность выполнения процедуры? У нас не происходит переключения контекста, пока процедура выполняется. Если одну и ту же процедуру одновременно вызвало 10 клиентов, то процессор их выполнит одну за другой. Для клиентов это будет выглядеть параллельно. Давайте не будем забывать, что производительность современных процессоров в 100 и 1000 раз быстрее производительности диска и сети. 

В типичном случае для вас это выглядит параллельным. В этом, собственно, и ключ архитектуры Tarantool. За счет современной скорости процессоров мы можем все выполнять последовательно, но для клиентов это будет выглядеть параллельно, потому что они все равно осуществляют доступ по сети или через диск.

Если процедура ваша медленная (в ней, допустим, итерация по ста тысячам элементов в пространстве), то да, все остальные будут ждать. Я об этом упомяну. 

Вопрос из зала: Простите, я уточню. Все остальные клиенты этой ноды?

Константин Осипов: Все остальные клиенты этой ноды. Идем дальше.

Сейчас мы расскажем о паттерне FIFO. Мы все-таки заявили наш доклад не как сессию вопросов и ответов, а как рассказ о паттернах, поэтому будем пытаться.

Что такое FIFO, объяснять не буду. Предположим, у нас есть некий список страниц, которые пользователь посещал. Последние 20 страниц.

Вот так это может выглядеть на PHP. Опять же, с использованием дополнительного пространства.

Александр Календарев: Здесь, в принципе, все очень просто. Первым действием мы увеличиваем счетчик, вторым мы удаляем данные. Если мы оставляем данные, то увеличиваем счетчик на единицу, получаем новые данные и вставляем данные. Следующий паттерн включает то же самое – он на хранимой процедуре представлен.

Константин Осипов: Да. Тут был классный вопрос о том, какая проблема с этим подходом? У нас есть два пространства. В одном мы храним указатель на начало и конец очереди для данного идентификатора очереди. Во втором пространстве мы храним саму очередь. Соответственно, push – это изменение двух пространств, pop – изменение двух пространств. Какая проблема с этим подходом?

Вопрос из зала: Состояние гонки.

Константин Осипов: Гонка. Ситуация гонки, совершенно верно. 

На Lua вы можете это сделать атомарно. Я бы хотел пояснить одну вещь. Что значит атомарность на данном этапе в Lua? Вы можете отдать управление в Lua. Я это покажу. Вы можете в вашей Lua-процедуре отдать управление. Потом оно к вам вернется. Опять же – использовать кооперативную многозадачность. 

Любое изменение данных, на самом деле, отдает управление. Операция "select" не отдает управление. В данном случае, когда у нас FIFO реализовано на Lua, мы тоже должны фактически в какой-то момент сделать две операции "update". Ситуация упрощается многократно, потому что "update", который мы должны сделать, – это создание пустого FIFO. Создание пустого FIFO можно сделать в цикле. Пока не получили для данного FIFO Tuple, создаем его.

Дальше. После того как мы нашли FIFO… В данном случае я использую схему данных, когда только одно пространство. Мы в этом пространстве храним: первое поле – это идентификатор FIFO, второе поле – начало FIFO, третье поле – конец FIFO. Дальше уже идет само FIFO.

Что я здесь делаю? Я получаю FIFO, получаю нужный мне Tuple, вычисляю новые начало и конец и выполняю команду "update" для этого FIFO. Это ничем не отличается от "update" из PHP, только это сделано на Lua. 

Здесь имя пространства и имя FIFO. Это закодировано, что-то вроде "print for". Мы обновляем, у нас тут 3 присваивания (===). Аргументом присваивания является указатель на скаляр Lua. "=pointer=pointer=pointer". Что мы присваиваем? Первое поле в "top", второе поле в "bottom" и поле "top" в "value". Мы помещаем новое поле в "top". Так примерно это выглядит на Lua. Это опять же атомарно, здесь нет никаких состояний гонки.

Еще один важный аспект, о котором я бы хотел поговорить. Это ответ на многие вопросы по поводу того, что произойдет, если процедура зависнет. 

В Lua есть функции по созданию кооперативных задач. Вы можете создать свою хранимую процедуру, которая будет выполняться как "зеленый поток" внутри Tarantool. Это что-то вроде временных триггеров, повторяющихся событий – как угодно. 

Когда, допустим, выполняются сложные вычисления, вы можете отдать управление. Есть функция "yield", которая отдает управление другим потокам. Соответственно, вы должны понимать, что если вы отдаете управление, когда оно к вам вернется, данные могут измениться. Другие операции "update", другие процедуры могут работать.

Как мы это используем в Mail.Ru. В Mail.Ru есть одно из центральных хранилищ, где мы храним все сессии всех пользователей. Вот те данные, с которыми я работал: 40 миллионов активных пользователей. Общее количество пользователей, допустим, около 100 миллионов. Соответственно, нужно хранить 40 миллионов сессий. Сессия представляет из себя набор «ключ – значение». Для каждого пользователя есть идентификатор. Речь идет где-то о 80-ти гигабайтах данных.

Сессии Mail.Ru хранит в Tarantool. Когда пользователь заходит, сессия создается. Для этого используются 4 машины. 2 машины – ведущие, 2 машины реплики. В Tarantool есть репликация. На каждом ведущем сервере работает по два экземпляра Tarantool.

Как они работают? В первую очередь, это ассоциативный массив «ключ – значение». Пользователь зашел – мы создали сессию, пользователь вышел – мы удалили сессию. Кроме того, работает алгоритм "expire process" (как в Memcached), который удаляет сессии по неактивности. "Expire process" постоянно итерирует по пространству сессий. Это тоже "зеленый поток". Он выполняется параллельно с потоками, которые обслуживают клиентов, и удаляет старые сессии. 

Что интересно, чего вы не можете добиться в Memcached или в любом другом сервере, который поддерживает удаление вследствие неактивности? Когда мы в Tarantool выполняем команду "expire", мы не просто удаляем кортеж. Мы выполняем некую кастомную бизнес-логику при удалении. 

В Mail.Ru есть политика, которая подразумевает, что пользователи, которые были неактивны два года, должны быть удалены из системы. Если вы не пользуетесь почтой два года, вас удаляют. Когда мы делаем expire-сессии, мы удаляем большую кортеж-сессию в 500 байт из пространства сессий и кладем маленький кортеж "user id data" в пространство «кладбища». Там мы храним те сессии, которые мы разлогинили. 

Есть отдельный процесс, который периодически проходит по «кладбищу». Если он обнаружит, что на этом «кладбище» пользователь уже два года (он не заходил два года), мы его можем удалить. В чем тут идея? Вы можете с использованием хранимых процедур закодировать свою кастомную логику, например, по "expire".

Я упомянул, что ЦП – наш главный ограничитель – меньше 20 % на этой системе используется.

Вопрос из зала: Как сделать шардинг на эти 4 Tarantool’а?

Константин Осипов: Вопрос: как сделать шардинг на 4 экземпляра Tarantool? Просто хеш от "user id".

Эта картинка демонстрирует, что у нас есть вторичные ключи. Мы сейчас будем рассказывать про Tarantool Proxy.

Это приложение, написанное Александром на "Си" как раз для шардинга.

Александр Календарев: Да. Из чего наш шардинг состоит? Есть некий алгоритм, по которому мы шардим данные. Алгоритм может быть "забит" в файлы конфигурации. Допустим, идентификаторы пользователей с первого по 10-миллионный у нас хранятся на сервере 1 с такими-то параметрами подключения. Следующие 10 миллионов хранятся на сервере 2, и так далее. Либо у нас может быть какая-то функция. Есть возможность написать свою кастомную функцию. 

Что из себя представляет приложение? Это многопоточный демон. Он проксирует запросы, определяет номер, куда проксировать, и отправляет ответ обратно. 

В Tarantool существует 2 вида операций. Это операции по первичному ключу и операции по вторичному ключу. Операции по первичному ключу приходят на Tarantool Proxy, по файлу конфигурации или по кастомной функции определяется номер шарда, и операция отправляется на соответствующий сервер. 

Если у нас существует операция по вторичному ключу (а по вторичному ключу у нас идет только "select"), то мы не знаем, на каком шарде все эти данные. Поэтому надо проверить все шарды. Просто идут запросы на все серверы, потом возвращаются. Дальше эти данные компонуются и отправляются обратно клиенту.

В принципе, здесь все понятно. Я готов ответить на вопросы.

Вопросы и ответы

Вопрос из зала: Получается, что Proxy должен быть один, потому что мы блокировки получим. Правильно?

Александр Календарев: Proxy может быть несколько. 

Вопрос из зала: Как мы тогда блокировок не получим? При операциях "insert".

Константин Осипов: Позвольте пояснить. У вас "insert" всегда выполняется по первичному ключу. Соответственно, задача Proxy – просто определить шард, на который его послать. Дальше на этом шарде все выполняется атомарно. 

Вопрос из зала: Этот широковещательный запрос на шарды отправляет запрос параллельно или последовательно?

Александр Календарев: В настоящее время он направляет запросы последовательно. Над тем, чтобы он параллельно отправлял запросы, я буду работать. Просто сперва надо довести до ума этот вариант, потом мы его будем улучшать. Над проектом я работаю всего 4 месяца. Не успеваю все сразу делать.

Вопрос из зала: Как сделать балансировку в данном случае? Что произойдет?

Александр Календарев: В данном случае балансировка не предусмотрена. Но балансировка заложена в проекте. Как только я сделаю параллельные запросы, я сразу займусь балансировкой. Здесь мы отправляем только на один сервер, а балансировка предполагает, что у нас, допустим, есть не один, а два шарда. 

Допустим, шард 1А находится на сервере А, шард 1B находится на сервере B. Из сервера A идет репликация на сервер B. Если у нас "вылетает" сервер A, то балансировщик переключит трафик на сервер B. Вот что подразумевает балансировка. Или Round Robin сделаем.

Вопрос из зала: Я имел в виду, что логичнее данные раскидывать параллельно на несколько серверов. Вопрос – как это сделать?

Александр Календарев: Репликация. Мы кидаем данные на сервер A…

Константин Осипов: Александр, позволь, я отвечу. Вы с помощью плагина можете выбрать алгоритм определения шарда. Соответственно, вы можете просто, как здесь сделано, выбирать шард по диапазону, а можете выбирать шард по хешу. У вас будет равномерное распределение по ключу. Но это не решает проблем "супергорячих" ключей и так далее.

Вопрос из зала: Скажите, как используется авторизация?

Александр Календарев: Авторизации нет.

Константин Осипов: Tarantool не предназначен для выставления на всеобщее обозрение. Это что-то, что работает в Интернете, поэтому авторизации у нас на данном этапе нет. 

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Илья Космодемьянский

Илья Космодемьянский

CEO и консультант в компании Data Egret, специалист по базам данных PostgreSQL, Oracle, DB2.

Илья Космодемьянский говорит о резервном копировании (Backup) и восстановлении (Recovery) в самых разных системах.

Борис Вольфсон

Борис Вольфсон

Борис Вольфсон занимается веб-разработкой и разработкой программного обеспечения с 2003 года. Карьеру начал в качестве программиста компании «Систем-Софт» в Оренбурге. С 2008 года – руководитель проектов и руководитель регионального отдела разработки в компании Softline.

Небольшой доклад в виде деловой игры, в виде симуляции по управлению рисками.

Александр Зиза

Александр Зиза

Занимается практикой развития лидеров и формирования управленческих команд для реализации digital-проектов с 2007 года. Организатор конференций по управлению в digital-культуре: TeamLeadConf, Aletheia-Business. Проводит программы по развитию управленческого мышления и трансформации культуры.

В докладе рассмотрены психологические основы возникновения и преодоления кризисов организации по мере роста компании от стартапа до организации внушительных размеров.