Многие слышали о NoSQL базе данных Tarantool, знают о том, что она умеет хранить данные в памяти, очень быстро их обрабатывает и обладает высокой производительностью. Тарантул был написан серьезными ребятами, которые обслуживают сервисы с сотнями тысяч запросов в секунду.
Система кажется сложной. Несмотря на российские корни, изначально даже не было документации на русском языке. Чем же может помочь этот мощный инструмент обычным парням — программистам и начинающим разработчикам? Остальные могут сразу посмотреть результат
Попробуем написать простой занимательный сервис, способный выдержать большую нагрузку. И никакого SQL!
Чтобы подогреть интерес к процессу изучения, возьмем простой и интересный пример использования Тарантула в веб-разработке. Мы запустим сайт, на котором выводится попарное голосование за какие-нибудь картинки. Выкладывать фотографии студенток колледжа, как Марк Цукерберг, мы не будем, но зато устроим голосование по выбору стикера для мессенджера Telegram. Наш алгоритм путем голосования будет выбирать топ из лучших наклеек, но на самом деле наша задача найти главного урода коллекции – мистера The Ugly. Теперь, когда появилась мотивация, самое время заняться скучными делами.
Вороне где-то бог послал кусочек сыру. Нам же достался скромный виртуальный сервер с одним процессорным ядром и оперативной памятью 1 Гбайт — младший отпрыск из одной благовоспитанной немецкой семьи. Операционная система — Debian, поэтому мы можем установить Тарантул из официального репозитория: tarantool.org/download.html
В инструкции написано Copy and Paste, что мы и делаем с большим энтузиазмом:
Маленький сюрприз от разработчиков: в официальном репозитории не поддерживается архитектура i386, хотя сервис хостинга пакетов исправно предоставляет все файлы, кроме основного. Убедитесь, что вы устанавливаете пакеты на систему AMD64, чтобы избежать неприятностей.
После установки, у нас появляется процесс tarantool, запущенный с тестовой конфигурацией example.lua, примерно такой:
Для работы с Tarantool используется скриптовый язык Lua, на котором пишутся команды управления сервером, описываются хранимые процедуры и триггеры. Можно подгрузить готовые программные модули или написать свои.
Запустим утилиту tarantoolctl, напечатаем первую программу и убедимся, что всё работает.
Сейчас мы находимся в тестовой конфигурации, которая поставляется вместе с дистрибутивом. Согласно идеологии Debian, настройки находятся в директории /etc/tarantool/instances.available/*, а запускаемая конфигурация программы создаётся при помощи символической ссылки в директории /etc/tarantool/instances.enabled/*. Скопируем файл примера под новым имением и создадим наш проект.
Наш проект называется the good, the bad and the ugly, сокращённо — gbu. Используйте сокращение полного названия проекта до первых букв, и ваши коллеги всегда будут с уважением и трепетом относиться к вашей работе!
Теперь немного поправим gbu.cfg и запустим сервис. Напомню, что для конфигурации используется синтаксис языка Lua, в котором комментарии начинаются с двух дефисов.
Запускаем новый инстанс командой tarantoolctl start gbu и убеждаемся, что всё работает как надо:
Мы в деле!
HASH-индекс: значение должно быть уникальным, и может быть произвольным. Так организуются всем известные Key/Value-хранилища, известные также как Map. Хороший пример — контрольная сумма файла MD5 hash.
TREE-индекс: значение может быть неуникальным, но должно быть «плотным» для организации сортированного списка. Получается массив (Array), у которого могут быть пропущенные элементы. Хороший пример — номер заказа, который увеличивается на единицу.
Если нужно уникальное значение, то вы можете использовать HASH или TREE, при этом HASH будет быстрее на разреженных данных. Если же нужно не уникальное поле, по которому будет делаться сортировка, то можно использовать только TREE-индекс.
Также есть индексы RTREE для поиска на двумерной плоскости и BITSET для работы с битовыми данными, но нам они не потребуются. Подробнее об этом написано в документации.
Картинка из статьи Евгения Шадрина Осваиваем Тарантул. Кстати, хорошее руководство для первоначального знакомства.
В модели данных нашего приложения создадим пространство stikers для хранения информации о файлах. Обратите внимание, что нумерация полей начинается с 1, поскольку используется синтаксис языка Lua. Кортеж включает следующие поля:
В пространстве packs мы будем хранить список стикер-паков:
В пространстве secrets мы будем хранить токен для шифрования ссылки на картинку, чтобы реализовать простейшую защиту от накрутки:
В пространстве sessions мы будем записывать посетителей и собирать статистику:
В пространстве server будем просто собирать статистику работы сайта:
Обратите внимание, что для назначения индекса нужно явно указать тип поля. Этот тип должен быть выбран из списка возможных вариантов Тарантула.
Остальные поля могут иметь произвольный тип, который поддерживает встроенный интерпретатор Lua. Этот дуализм типов данных является особенностью Тарантула и упоминается в документации. Для удобства, мы указали тип данных Lua в скобках при описании модели.
Важная часть в моделировании — составление индексов. Большое преимущество Тарантула заключается в том, что он умеет делать сложные составные индексы. Благодаря этому, мы можем писать быстрые аналитические запросы на основе различных полей кортежа без уменьшения быстродействия системы. Добавим первичный индекс типа TREE для поля id, чтобы обеспечить случайный выбор элемента для голосования. Второй индекс — типа TREE по полю Raiting, чтобы выводить рейтинг, конечно же! Добавим составной индекс по полям Pack + Emoj типа HASH, который должен быть уникальным. Его можно использовать для анализа популярности наборов стикеров.
Код создания базы данных мы разместим в нашем файле gbu.lua, в процедуре инициализации
Прежде чем перезапустить сервер с настройками, попробуйте создать схему командами в консоли. Если что-то пойдёт не так, можно удалить пространство целиком командой:
или отдельный индекс:
Не стесняйтесь использовать подсказку по клавише TAB. Для удобства работы в консоли, названия создаваемых элементов схемы мы пишем с маленькой буквы. Команды для работы в консоли становятся интуитивно понятны после краткого ознакомления с документацией.
Очистить пространство:
Удалить пространство:
Всё происходит мгновенно, как и положено для In-memory Database!
Хороший современный язык программирования должен иметь статическую строгую типизацию, Homoiconicity — свойство, позволяющее манипулировать кодом как данными, поддержку ООП, FFI к библиотекам на C, поддержку дженериков, конкурентного программирования, Functions as first-class citizens, lambdas.
Ничего этого в PHP, конечно же, нет! Поэтому мы будем писать код примера именно на нём.
Для начала поставим проверенные инструменты — веб сервер Nginx и интерпретатор PHP — php-fpm: wiki.debian.org/ru/nginx/nginx+php-fpm
В корневой путь конфигурации nginx добавим правило перезаписи запросов:
Таким образом, мы можем получать в PHP-скрипте красивые ссылки вида /good из массива
Еще у нас есть локейшин для выполнения CGI-запросов:
Командой
Теперь необходимо поставить модуль для работы с Тарантулом:
Читаем, что написано в выводе программы-установщика. В моем случае было сообщение:
Добавляем указанную строчку в файл конфигурации в файл /etc/php5/fpm/php.ini и в /etc/php5/cli/php.ini. К сожалению, при запуске PHP получаем ошибку! Чтобы не страдать с отладкой web-сервера, мы добавили новую библиотеку и в cli-конфигурацию, поэтому можно проверить работоспособность из командной строки.
На момент написания статьи модуль в репозитории PEAR содержал ошибку, поэтому остается лишь путь джедая — сборка драйвера из исходных кодов.
Создадим первый файл и назовём его test.php, в нём мы проверим работу нашей базы данных.
Запускаем из командной строки
Теперь можно написать парсер, который соберёт данные с нужного нам сайта. Мы будем исследовать tlgrm.ru/stickers. Сначала загрузим таблицу pack, в которой у нас лежит перепись стикер-паков. Вот так выглядит команда insert в командной строке tarantool:
Эта команда добавляет новый ключ “key1” (в поле 1) и значение 0 (в поле 2). Если запись существует, то обновляется для этой же записи (знак =) в поле 2 значением 0. Как мы помним, в поле 2 у нас содержится рейтинг, который мы первоначально в 0. Команду upsert удобно использовать для многократного запуска парсера при отладке, чтобы не удалять каждый раз внесенные данные. PHP-вариант команды будет выглядеть так:
Ай-ай! В PHP нумерация полей с 0, а в Lua c 1. Поэтому
Запись для стикера мы вносим при помощи встроенной процедуры auto_increment, которая автоматически увеличивает первичный индекс. Команда Тарантула:
PHP:
Итак, скрипт написан. Запускаем его — вжух, и магия! Теперь у нас есть база данных с 16 000 записей!
Для начала сделаем простейший роутер запросов, как это обычно делается в PHP:
Отметим наличие двух переменных $vote_plus и $vote_minus, которые будут передаваться в POST-запросе при голосовании за одну или другую картинку. Дело в том, что, зная имя и путь файла очень легко накрутить голосование ботами, а нам это не нужно. Поэтому мы будем для страницы голосования генерировать пару уникальных токенов, по одному на каждую картинку. После голосования токен будет удаляться, делая невозможным повторное использование голоса.
Поскольку в PHP до выхода версии 7.0 дела с криптобезопасными функциями обстоят печально, то очень помогу богатые возможности Тарантула по работе с криптографией.
Для начала в функции action_main инициируем генератор случайных чисел криптобезопасным (т.е. действительно случайным) seed:
Функция $tarantool->evaluate() используется непосредственно для запуска кода Lua без возни с созданием хранимой процедуры. Затем два раза вызываем функцию create_random_vote(), которая выберет случайный элемент в пространстве и создаст URL для картинки и токенов.
Тут использовались ещё две функции: $tarantool->call() для вызова встроенных процедур и $tarantol->insert() для вставки новой записи.
Приведём пример процедуры обновления голосования, которая обновляет рейтинг записи:
Полный список методов класса Tarantool смотрите в документации tarantool-php.
Обратите внимание на параметр "
Код файла index.php на момент написания статьи находится под спойлером:
Осталось сделать HTML-шаблоны, которые будут выводить данные с картинками для голосования на сайт и страницы рейтингов.
Кому-нибудь удалось повстречать в жизни дизайнера-верстальщика, который бы идеально отформатировал HTML-код?
Из вышеприведённого кода нетрудно догадаться, что мы выдали для левой и правой картинки одинаковую пару токенов, только поменяли местам vote_plus и vote_minus. (Обычно такими фразами автор как бы подчеркивает своё интеллектуальное превосходство над читателями). Таким образом, на какую бы картинку не кликнул пользователь, она получит плюс, а её конкурент — минус. Неудачник с каждым разом получает всё больше минусов и валится всё ниже в преисподнюю. Туда ему и дорога ХА – ХА – ХА!
Читателю, который добрался до конца статьи, вытерпел троллинг автора и не раз вскипал от его глупости, полагается заслуженная награда. Что может быть интереснее, чем реальный работающий пример, который можно потыкать и покликать? Приходи и голосуй за стикеры Telegram на сайт ugly.begetan.me, чтобы узнать, какой же из них самый уродский. Поковыряй мышкой наш русский хайлоад, собранный из NGNIX, PHP-FPM и Tarantool. И не забудь показать ссылку своей подружке, ведь необходимо достаточно много голосов, чтобы получить статистически-достоверную картину голосования.
Система кажется сложной. Несмотря на российские корни, изначально даже не было документации на русском языке. Чем же может помочь этот мощный инструмент обычным парням — программистам и начинающим разработчикам? Остальные могут сразу посмотреть результат
Попробуем написать простой занимательный сервис, способный выдержать большую нагрузку. И никакого SQL!
Наша цель
Чтобы подогреть интерес к процессу изучения, возьмем простой и интересный пример использования Тарантула в веб-разработке. Мы запустим сайт, на котором выводится попарное голосование за какие-нибудь картинки. Выкладывать фотографии студенток колледжа, как Марк Цукерберг, мы не будем, но зато устроим голосование по выбору стикера для мессенджера Telegram. Наш алгоритм путем голосования будет выбирать топ из лучших наклеек, но на самом деле наша задача найти главного урода коллекции – мистера The Ugly. Теперь, когда появилась мотивация, самое время заняться скучными делами.
Установка
Вороне где-то бог послал кусочек сыру. Нам же достался скромный виртуальный сервер с одним процессорным ядром и оперативной памятью 1 Гбайт — младший отпрыск из одной благовоспитанной немецкой семьи. Операционная система — Debian, поэтому мы можем установить Тарантул из официального репозитория: tarantool.org/download.html
В инструкции написано Copy and Paste, что мы и делаем с большим энтузиазмом:
curl http://download.tarantool.org/tarantool/1.7/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`
# install https download transport for APT
sudo apt-get -y install apt-transport-https
# append two lines to a list of source repositories
sudo rm -f /etc/apt/sources.list.d/*tarantool*.list
sudo tee /etc/apt/sources.list.d/tarantool_1_7.list <<- EOF
deb http://download.tarantool.org/tarantool/1.7/debian/ $release main
deb-src http://download.tarantool.org/tarantool/1.7/debian/ $release main
EOF
# install
sudo apt-get update
sudo apt-get -y install tarantool
Маленький сюрприз от разработчиков: в официальном репозитории не поддерживается архитектура i386, хотя сервис хостинга пакетов исправно предоставляет все файлы, кроме основного. Убедитесь, что вы устанавливаете пакеты на систему AMD64, чтобы избежать неприятностей.
Начало работы
После установки, у нас появляется процесс tarantool, запущенный с тестовой конфигурацией example.lua, примерно такой:
ps xauf | grep taran
root 2735 0.0 0.2 13972 2132 pts/0 S+ 22:05 0:00 \_ grep taran
taranto+ 568 0.0 0.8 812304 8632 ? Ssl 17:03 0:03 tarantool example.lua <running>
Для работы с Tarantool используется скриптовый язык Lua, на котором пишутся команды управления сервером, описываются хранимые процедуры и триггеры. Можно подгрузить готовые программные модули или написать свои.
Запустим утилиту tarantoolctl, напечатаем первую программу и убедимся, что всё работает.
tarantoolctl connect '3301'
connected to localhost:3301
localhost:3301> print ('The good!')
---
...
localhost:3301>
Сейчас мы находимся в тестовой конфигурации, которая поставляется вместе с дистрибутивом. Согласно идеологии Debian, настройки находятся в директории /etc/tarantool/instances.available/*, а запускаемая конфигурация программы создаётся при помощи символической ссылки в директории /etc/tarantool/instances.enabled/*. Скопируем файл примера под новым имением и создадим наш проект.
Наш проект называется the good, the bad and the ugly, сокращённо — gbu. Используйте сокращение полного названия проекта до первых букв, и ваши коллеги всегда будут с уважением и трепетом относиться к вашей работе!
Теперь немного поправим gbu.cfg и запустим сервис. Напомню, что для конфигурации используется синтаксис языка Lua, в котором комментарии начинаются с двух дефисов.
box.cfg {
-- Хорошая привычка сразу менять порт по умолчанию для сервиса
listen = 3311;
-- Подгоняем размер памяти под наши нужды
slab_alloc_arena = 0.2;
-- Остальные параметры оставляем без изменений
}
local function bootstrap()
local space = box.schema.create_space('example')
space:create_index('primary')
-- Закомментируем пользователя по умолчанию
-- box.schema.user.grant('guest', 'read,write,execute', 'universe')
-- Создадим нового пользователя
box.schema.user.create('good', { password = 'secret' })
box.schema.user.grant('good', 'read,write,execute', 'universe')
end
-- При первом запуске создаем пространство и назначаем привилегии
box.once('example-2.0', bootstrap)
Запускаем новый инстанс командой tarantoolctl start gbu и убеждаемся, что всё работает как надо:
tarantoolctl connect "good:secret@0:3311"
connected to 0:3311
0:3311>
Мы в деле!
База данных
- Записи в Tarantool хранятся в пространствах (space), это такой аналог таблицы в реляционной базе данных SQL. Их можно сделать столько, сколько потребуется — до 65 тысяч
- Внутри пространства находятся кортежи (tuples), которые похожи и на строку в таблице SQL, и на JSON-массив данных. Максимальный размер кортежа — до 1 Мб при настройках по умолчанию. Этот параметр можно менять.
- Чтобы от базы данных была польза, необходимо создать индексы, так же, как в SQL. Благодаря им, вместо перебора полного списка всех элементов поиск происходит по более быстрому алгоритму. Также доступна сортировка. Выбор индекса зависит от типа данных.
HASH-индекс: значение должно быть уникальным, и может быть произвольным. Так организуются всем известные Key/Value-хранилища, известные также как Map. Хороший пример — контрольная сумма файла MD5 hash.
TREE-индекс: значение может быть неуникальным, но должно быть «плотным» для организации сортированного списка. Получается массив (Array), у которого могут быть пропущенные элементы. Хороший пример — номер заказа, который увеличивается на единицу.
Если нужно уникальное значение, то вы можете использовать HASH или TREE, при этом HASH будет быстрее на разреженных данных. Если же нужно не уникальное поле, по которому будет делаться сортировка, то можно использовать только TREE-индекс.
Также есть индексы RTREE для поиска на двумерной плоскости и BITSET для работы с битовыми данными, но нам они не потребуются. Подробнее об этом написано в документации.
Картинка из статьи Евгения Шадрина Осваиваем Тарантул. Кстати, хорошее руководство для первоначального знакомства.
Модель данных проекта
В модели данных нашего приложения создадим пространство stikers для хранения информации о файлах. Обратите внимание, что нумерация полей начинается с 1, поскольку используется синтаксис языка Lua. Кортеж включает следующие поля:
- unsigned id — уникальный номер стикера
- integer rating — рейтинг стикера
- string pack — название стикер-пака
- string name — название файла стикера
- (string) path — URL стикера
- (number) up — количество голосов за стикер
- (number) down — количество голосов против стикера.
В пространстве packs мы будем хранить список стикер-паков:
- string pack — название стикер-пака
- integer rating — рейтинг стикер-пака
- (string) path — ссылка на страницу описания
В пространстве secrets мы будем хранить токен для шифрования ссылки на картинку, чтобы реализовать простейшую защиту от накрутки:
- string token — случайный токен для стикера
- integer time — время создания токена (пригодится для удаления старых)
- (integer) id — уникальный номер стикера (ключ пространства stickers)
- (string) url — URL стикера
В пространстве sessions мы будем записывать посетителей и собирать статистику:
- string uuid — уникальный символьный идентификатор посетителя
- integer uuid_time — время создания сессии (пригодится для удаления старых)
- (number) user_votes — сколько раз проголосовал посетитель
- (string) ip — IP-адрес посетителя
- (string) agent — тип браузера посетителя
В пространстве server будем просто собирать статистику работы сайта:
- Integer id — просто ключ
- (number) visitors — количество уникальных пользователей
- (number) votes — общее количество показов голосований
- (number) clicks — общее количество кликов в голосовании
Обратите внимание, что для назначения индекса нужно явно указать тип поля. Этот тип должен быть выбран из списка возможных вариантов Тарантула.
Остальные поля могут иметь произвольный тип, который поддерживает встроенный интерпретатор Lua. Этот дуализм типов данных является особенностью Тарантула и упоминается в документации. Для удобства, мы указали тип данных Lua в скобках при описании модели.
Важная часть в моделировании — составление индексов. Большое преимущество Тарантула заключается в том, что он умеет делать сложные составные индексы. Благодаря этому, мы можем писать быстрые аналитические запросы на основе различных полей кортежа без уменьшения быстродействия системы. Добавим первичный индекс типа TREE для поля id, чтобы обеспечить случайный выбор элемента для голосования. Второй индекс — типа TREE по полю Raiting, чтобы выводить рейтинг, конечно же! Добавим составной индекс по полям Pack + Emoj типа HASH, который должен быть уникальным. Его можно использовать для анализа популярности наборов стикеров.
Код создания базы данных мы разместим в нашем файле gbu.lua, в процедуре инициализации
function bootstrap()
local function bootstrap()
box.schema.user.create('good', { password = 'secret' })
box.schema.user.grant('good', 'read,write,execute', 'universe')
-----------------------------------
-- Пространство стикеров
local stickers = box.schema.create_space('stickers')
-- Индекс для файла
stickers:create_index('primary', {
type = 'TREE', parts = {1, 'unsigned'}
})
-- Индекс для рейтинга
stickers:create_index('secondary', {
type = 'TREE',
unique = false,
parts = {2, 'integer'}
})
-- Индекс для названий стикеров
stickers:create_index('ternary', {
type ='HASH', parts = {3, 'string', 4, 'string'}
})
-----------------------------------
-- Пространство стикер-пака
local packs = box.schema.create_space('packs')
-- Индекс для стикер-пака
packs:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})
-- Индекс для рейтинга пака
packs:create_index('secondary', {
type = 'TREE',
unique = false,
parts = {2, 'integer'}
})
-----------------------------------
-- Пространство секретных ссылок
local secret = box.schema.create_space('secret')
-- Индекс для секретного ключа
secret:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})
-- Индекс для времени создания ключа
secret:create_index('secondary', {
type = 'TREE',
unique = false,
parts = {2, 'integer'}
})
-----------------------------------
-- Пространство сессий пользователей
local sessions = box.schema.create_space('sessions')
-- Индекс для уникального пользователя
sessions:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})
-- Индекс для времени создания сессии
sessions:create_index('secondary', {
type = 'TREE',
unique = false,
parts = {2, 'integer'}
})
-----------------------------------
-- Пространство статистики сервера
local server = box.schema.create_space('server')
-- Просто индекс
server:create_index('primary', {
type = 'TREE', parts = {1, 'unsigned'}
})
-- Создадим запись
server:insert{1, 0, 0, 0}
end
Прежде чем перезапустить сервер с настройками, попробуйте создать схему командами в консоли. Если что-то пойдёт не так, можно удалить пространство целиком командой:
box.space.stickers:drop()
или отдельный индекс:
box.space.stickers.index.ternary:drop()
Не стесняйтесь использовать подсказку по клавише TAB. Для удобства работы в консоли, названия создаваемых элементов схемы мы пишем с маленькой буквы. Команды для работы в консоли становятся интуитивно понятны после краткого ознакомления с документацией.
Очистить пространство:
box.space.stickers:truncate()
Удалить пространство:
box.space.stickers:drop()
Всё происходит мгновенно, как и положено для In-memory Database!
Установка компонентов
Хороший современный язык программирования должен иметь статическую строгую типизацию, Homoiconicity — свойство, позволяющее манипулировать кодом как данными, поддержку ООП, FFI к библиотекам на C, поддержку дженериков, конкурентного программирования, Functions as first-class citizens, lambdas.
Ничего этого в PHP, конечно же, нет! Поэтому мы будем писать код примера именно на нём.
Для начала поставим проверенные инструменты — веб сервер Nginx и интерпретатор PHP — php-fpm: wiki.debian.org/ru/nginx/nginx+php-fpm
В корневой путь конфигурации nginx добавим правило перезаписи запросов:
location / {
try_files $uri $uri/ /index.php?q=$uri&$args;
}
Таким образом, мы можем получать в PHP-скрипте красивые ссылки вида /good из массива
$_REQUEST['q']
, и реализуем роутинг HTTP-запросов.Еще у нас есть локейшин для выполнения CGI-запросов:
location ~* \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
expires -1;
}
Командой
expires -1;
мы отключаем кэширование запросов, оно не нужно для страниц голосования и вывода Топ-чартов. Остальные локейшины кешируют данные на 24 часа или 30 дней из вышестоящих настроек HTTP. Наверное, у каждого есть свой сборник готовых опции Nginx.Теперь необходимо поставить модуль для работы с Тарантулом:
sudo apt-get install php5-cli php5-dev php-pear
pecl channel-discover tarantool.github.io/tarantool-php/pecl
pecl install Tarantool-PHP/Tarantool-beta
Читаем, что написано в выводе программы-установщика. В моем случае было сообщение:
Build process completed successfully
Installing '/usr/lib/php5/20131226/tarantool.so'
install ok: channel://tarantool.github.io/tarantool-php/pecl/Tarantool-0.0.13
configuration option "php_ini" is not set to php.ini location
You should add "extension=tarantool.so" to php.ini
Добавляем указанную строчку в файл конфигурации в файл /etc/php5/fpm/php.ini и в /etc/php5/cli/php.ini. К сожалению, при запуске PHP получаем ошибку! Чтобы не страдать с отладкой web-сервера, мы добавили новую библиотеку и в cli-конфигурацию, поэтому можно проверить работоспособность из командной строки.
php -v
PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib/php5/20131226/tarantool.so' - /usr/lib/php5/20131226/tarantool.so: undefined symbol: tarantool_schema_destroy in Unknown on line 0
PHP 5.6.29-0+deb8u1 (cli) (built: Dec 13 2016 16:02:08)
На момент написания статьи модуль в репозитории PEAR содержал ошибку, поэтому остается лишь путь джедая — сборка драйвера из исходных кодов.
pecl uninstall Tarantool-PHP/Tarantool-beta
cd ~
git clone https://github.com/tarantool/tarantool-php.git
cd tarantool-php
phpize
./configure
make
make install
Загрузка данных
Создадим первый файл и назовём его test.php, в нём мы проверим работу нашей базы данных.
<?php
$tarantool = new Tarantool('localhost', 3311, 'good', 'secret');
try {
$tarantool->ping();
} catch (Exception $e) {
echo "Exception: ", $e->getMessage(), "\n";
}
?>
Запускаем из командной строки
php config.php
и проверяем, как сработало. Если настроить неправильно, то получим сообщение об ошибке. Проверьте!Теперь можно написать парсер, который соберёт данные с нужного нам сайта. Мы будем исследовать tlgrm.ru/stickers. Сначала загрузим таблицу pack, в которой у нас лежит перепись стикер-паков. Вот так выглядит команда insert в командной строке tarantool:
box.space.packs:upsert({'key1',0}, {{'=',2,0}})
Эта команда добавляет новый ключ “key1” (в поле 1) и значение 0 (в поле 2). Если запись существует, то обновляется для этой же записи (знак =) в поле 2 значением 0. Как мы помним, в поле 2 у нас содержится рейтинг, который мы первоначально в 0. Команду upsert удобно использовать для многократного запуска парсера при отладке, чтобы не удалять каждый раз внесенные данные. PHP-вариант команды будет выглядеть так:
$tarantool->upsert('packs', array ($pack,0), array (
array(
"field" => 1,
"op" => "=",
"arg" => 0
)
));
Ай-ай! В PHP нумерация полей с 0, а в Lua c 1. Поэтому
"field" => 1
из массива PHP соответствует записи {'=',2,0}
в Lua. Везде, где массивы начинаются с нуля, текущие коннекторы работают так же. Это поведение было изменено начиная с версии 1.6. Читая примеры в интернете, обращайте внимание на версию Тарантула! Эта статья написана по версии 1.7, а про версию 1.5 разработчики просят не вспоминать вовсе.Запись для стикера мы вносим при помощи встроенной процедуры auto_increment, которая автоматически увеличивает первичный индекс. Команда Тарантула:
box.space.stickers:auto_increment({0,'pack2','sticker2'})
PHP:
$tarantool->call('box.space.stickers:auto_increment', array(
array(0,$pack, $i . '.png', $url, 0, 0)
));
Итак, скрипт написан. Запускаем его — вжух, и магия! Теперь у нас есть база данных с 16 000 записей!
Пишем программу
Для начала сделаем простейший роутер запросов, как это обычно делается в PHP:
# Get routes from request
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : '';
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : '';
switch ($route) {
case '/good':
action_good();
break;
case '/bad':
action_bad();
break;
case '/ugly':
action_ugly();
break;
case '/about':
action_about();
break;
default:
if (!empty($vote_plus) && !empty($vote_minus)) {
sleep (1);
do_post($vote_plus, $vote_minus);
}
action_main();
}
Отметим наличие двух переменных $vote_plus и $vote_minus, которые будут передаваться в POST-запросе при голосовании за одну или другую картинку. Дело в том, что, зная имя и путь файла очень легко накрутить голосование ботами, а нам это не нужно. Поэтому мы будем для страницы голосования генерировать пару уникальных токенов, по одному на каждую картинку. После голосования токен будет удаляться, делая невозможным повторное использование голоса.
Поскольку в PHP до выхода версии 7.0 дела с криптобезопасными функциями обстоят печально, то очень помогу богатые возможности Тарантула по работе с криптографией.
Для начала в функции action_main инициируем генератор случайных чисел криптобезопасным (т.е. действительно случайным) seed:
$r = $tarantool->evaluate(
"digest = require('digest')
return (digest.urandom(4))"
);
$seed = unpack('L', $r[0])[1];
srand($seed);
Функция $tarantool->evaluate() используется непосредственно для запуска кода Lua без возни с созданием хранимой процедуры. Затем два раза вызываем функцию create_random_vote(), которая выберет случайный элемент в пространстве и создаст URL для картинки и токенов.
function create_random_vote()
function create_random_vote() {
# Get random sticker id
global $tarantool;
$tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
$id = $tuple[0][0];
$url = $tuple[0][4];
# Create random sticker token
$token = $tarantool->evaluate(
"digest = require('digest')
return ( digest.md5_hex(digest.urandom(32)))"
)[0];
$time = time();
# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
$tarantool->insert('secret', array ($token, $time, $id, $url));
return array (
$url,
$token
);
}
Тут использовались ещё две функции: $tarantool->call() для вызова встроенных процедур и $tarantol->insert() для вставки новой записи.
Приведём пример процедуры обновления голосования, которая обновляет рейтинг записи:
function update_votes($id, $plus, $minus)
function update_votes($id, $plus, $minus) {
global $tarantool;
###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
$tarantool->update("stickers", $id,
array (
array(
"field" => 5,
"op" => "+",
"arg" => $plus
),
array(
"field" => 6,
"op" => "+",
"arg" => $minus
)
)
);
}
Полный список методов класса Tarantool смотрите в документации tarantool-php.
Обратите внимание на параметр "
op" => "="
, который означает, что происходит замена поля в существующем кортеже. Также есть параметр +,- и некоторые другие операции. Они выполняют очень важную задачу. Обычно для замены значения в базе данных мы сначала читаем какое-то поле, затем изменяем его. Чтобы сохранить консистентность данных, приходится блокировать доступ к таблице и использовать транзакции. В Тарантуле же, благодаря его архитектуре, команды update и upsert срабатывают атомарно внутри процесса сервера без блокировки базы данных. Это позволяет строить чертовски быстрые системы!Код файла index.php на момент написания статьи находится под спойлером:
index.php
<?php
# Init database
$tarantool = new Tarantool('localhost', 3301, 'good', 'bad');
try {
$tarantool->ping();
} catch (Exception $e) {
echo "Exception: ", $e->getMessage(), "\n";
}
const MIN_VOTES = 20; // Number of votes to show the ugly
const UPDATE_PLUS = 1; // Increment for positive update
const UPDATE_MINUS = -1; // Increment for negative update
const NO_UPDATE = 0;
const COOKIE = 'uuid'; // Cookie name
const HIDDEN = '/img/Question.svg';// Picture for hidden element
# Get routes from request
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : '';
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : '';
# Get cookie from request or create new value
$cookie = isset($_COOKIE[COOKIE]) ? $_COOKIE[COOKIE] : update_user('');
switch ($route) {
case '/good':
action_good();
break;
case '/bad':
action_bad();
break;
case '/ugly':
action_ugly($cookie);
break;
case '/about':
action_about();
break;
default:
# This is post request:
if (!empty($vote_plus) && !empty($vote_minus)) {
sleep (1);
$cookie = update_user($cookie);
do_post($vote_plus, $vote_minus);
}
setcookie(COOKIE, $cookie, time() + (86400 * 30), "/");
action_main();
}
exit();
function action_main() {
global $tarantool;
# Get crypto safe random seed from Tarantool LUA module
# https://tarantool.org/doc/reference/reference_lua/digest.html
$r = $tarantool->evaluate(
"digest = require('digest')
return (digest.urandom(4))"
);
$seed = unpack('L', $r[0])[1];
srand($seed);
list ($left_url, $left_token_plus) = create_random_vote();
list ($right_url, $right_token_plus) = create_random_vote();
$left_token_minus = $right_token_plus;
$right_token_minus = $left_token_plus;
update_stats(UPDATE_PLUS, NO_UPDATE);
$title = 'Хорошие и плохие стикеры Telegram';
include_once('main.html');
}
function action_good() {
$title = 'ТОП лучших стикеров Telegram';
$top = get_top(10,Tarantool::ITERATOR_LE);
$active_good ='class="active"';
$active_bad ='';
include_once('top.html');
}
function action_bad () {
$title = 'ТОП худших стикеров Telegram';
$active_bad ='class="active"';
$active_good ='';
$top = get_top(10,Tarantool::ITERATOR_GE);
# Hide the ugly
$top[0][4] = HIDDEN;
include_once('top.html');
}
function action_ugly($user) {
$title = 'Худший стикер Telegram';
$top = get_top(1,Tarantool::ITERATOR_GE);
$votes = get_session($user);
# Hide the ugly until getting enough votes
if ($votes < MIN_VOTES) {
$ugly_message = "Голосуй " . MIN_VOTES . " раз чтобы увидеть результат<br>";
$ugly_message .= "Осталось " . (MIN_VOTES - $votes) . " голосований";
$ugly_img = HIDDEN;
} else {
$ugly_img = $top[0][4];
}
include_once('ugly.html');
}
function action_about() {
$title = 'Как это сделано?';
list($stickers, $shows, $votes, $visitors) = get_server_stats();
include_once('about.html');
}
function do_post($vote_plus, $vote_minus) {
global $tarantool;
$tuple_plus = $tarantool->select("secret", $vote_plus);
$tuple_minus = $tarantool->select("secret", $vote_minus);
$id_plus = $tuple_plus[0][2];
$id_minus = $tuple_minus[0][2];
# Clean up used tokens
if (!empty($vote_plus) && !empty($vote_minus)) {
$tarantool->delete("secret", $vote_plus);
$tarantool->delete("secret", $vote_minus);
}
# Get actual tuple data
if (!empty($id_plus) && !empty($id_minus)) {
$raiting = +1;
update_rating($id_plus, $raiting);
$raiting = -1;
update_rating($id_minus, $raiting);
update_votes($id_plus, UPDATE_PLUS, NO_UPDATE);
update_votes($id_minus, NO_UPDATE, UPDATE_MINUS);
update_stats(NO_UPDATE, UPDATE_PLUS);
}
}
function create_random_vote() {
# Get random sticker id
global $tarantool;
$tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
$id = $tuple[0][0];
$url = $tuple[0][4];
# Create random sticker token
$token = $tarantool->evaluate(
"digest = require('digest')
return ( digest.md5_hex(digest.urandom(32)))"
)[0];
$time = time();
# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
$tarantool->insert('secret', array ($token, $time, $id, $url));
return array (
$url,
$token
);
}
function update_rating($id, $update) {
global $tarantool;
#################################################
#
# box.space.stickers:update(7856, {{'+', 2, 10}})
#
#################################################
$tarantool->update("stickers", $id, array (
array(
"field" => 1,
"op" => "+",
"arg" => $update
)
));
}
function update_votes($id, $plus, $minus) {
global $tarantool;
###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
$tarantool->update("stickers", $id,
array (
array(
"field" => 5,
"op" => "+",
"arg" => $plus
),
array(
"field" => 6,
"op" => "+",
"arg" => $minus
)
)
);
}
function update_user($cookie) {
global $tarantool;
# Create uuid if first time user
if (empty($cookie)) {
##################################
#
# uuid = require('uuid')
# uuid()
#
##################################
$uuid = $tarantool->evaluate(
"uuid = require('uuid')
return (uuid.str())"
)[0];
} else {
$uuid = $cookie;
}
$time = time();
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
$agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
# Create session or update user stat inside
###########################################################
#
# box.space.sessions:upsert({'111222333', 123456, 0, 'ip', 'agent'},
# {{'=', 2, 1}, {'+', 3, 1}, {'=', 4, 'ip'}, {'=', 5, 'agent'}})
#
###########################################################
# Please check https://github.com/tarantool/tarantool-php/issues/111
$tarantool->upsert("sessions", array($uuid, $time, 0, $ip, $agent),
array (
array(
"field" => 1
"op" => "=",
"arg" => $time
),
array(
"field" => 2
"op" => "+",
"arg" => 1
),
array(
"field" => 3
"op" => "=",
"arg" => $ip
),
array(
"field" => 4,
"op" => "=",
"arg" => $agent
)
)
);
return($uuid);
}
function update_stats($vote, $click) {
global $tarantool;
########################################################
#
# box.space.server:update(1, {{'+', 3, 1}, {'+', 4, 1}})
#
########################################################
$tarantool->update("server",1,
array (
array(
"field" => 2,
"op" => "+",
"arg" => $vote
),
array(
"field" => 3,
"op" => "+",
"arg" => $click
)
)
);
}
function get_session($sid) {
global $tarantool;
##########################################
#
# box.space.sessions:select('id')
#
#########################################
if (strlen($sid) > 16) {
return $tarantool->select("sessions", $sid)[0][2];
} else {
return 0;
}
}
function get_top($limit, $iterator) {
global $tarantool;
######################################################################################
#
# box.space.stickers.index.secondary:select({primary}, {iterator = box.index.GE, offset=0, limit=10})
#
######################################################################################
$result = $tarantool->select("stickers", null, 'secondary', $limit, 0, $iterator);
return $result;
}
function get_server_stats() {
global $tarantool;
$time = time() - 30*86400; // one month before
$stickers = $tarantool->call('box.space.stickers:count')[0][0];
$tuple = $tarantool->select('server',1);
$shows = $tuple[0][2];
$votes = $tuple[0][3];
$visitors = $tarantool->call('box.space.sessions.index.secondary:count',
array($time, array('iterator' => Tarantool::ITERATOR_GE))
)[0][0];
# $shows, $votes, $visitors) = get_server_stats();
return array($stickers, $shows, $votes, $visitors);
}
?>
Осталось сделать HTML-шаблоны, которые будут выводить данные с картинками для голосования на сайт и страницы рейтингов.
Пример кода для картинок с голосованием
<!-- НАЧАЛО ГОЛОСОВАНИЯ -->
<div class="voting container">
<div class="voting-zone">
<!-- ПЕРВЫЙ КОНТЕЙНЕР С КАРТИНКОЙ -->
<div class="sticker" onclick="myFunction()">
<form name="voteFormLeft" id="idForm" method ="POST" action="/" >
<input class= "pic1" id="left_url" type="image" src="<?php echo $left_url?>" alt="Vote left" >
<input type="hidden" name="vote_plus" value="<?php echo $left_token_plus?>">
<input type="hidden" name="vote_minus" value="<?php echo $left_token_minus?>">
</form>
</div>
<!-- ВТОРОЙ КОНТЕЙНЕР С КАРТИНКОЙ -->
<div class="sticker" onclick="myFunction()">
<form name="voteFormRight" id="idForm" method ="POST" action="/">
<input class= "pic2" id="right_url" type="image" src="<?php echo $right_url?>" alt="Vote right" >
<input type="hidden" name="vote_plus" value="<?php echo $right_token_plus?>" >
<input type="hidden" name="vote_minus" value="<?php echo $right_token_minus?>" >
</form>
</div>
</div>
</div>
<!-- КОНЕЦ ГОЛОСОВАНИЯ -->
Кому-нибудь удалось повстречать в жизни дизайнера-верстальщика, который бы идеально отформатировал HTML-код?
Из вышеприведённого кода нетрудно догадаться, что мы выдали для левой и правой картинки одинаковую пару токенов, только поменяли местам vote_plus и vote_minus. (Обычно такими фразами автор как бы подчеркивает своё интеллектуальное превосходство над читателями). Таким образом, на какую бы картинку не кликнул пользователь, она получит плюс, а её конкурент — минус. Неудачник с каждым разом получает всё больше минусов и валится всё ниже в преисподнюю. Туда ему и дорога ХА – ХА – ХА!
Читателю, который добрался до конца статьи, вытерпел троллинг автора и не раз вскипал от его глупости, полагается заслуженная награда. Что может быть интереснее, чем реальный работающий пример, который можно потыкать и покликать? Приходи и голосуй за стикеры Telegram на сайт ugly.begetan.me, чтобы узнать, какой же из них самый уродский. Поковыряй мышкой наш русский хайлоад, собранный из NGNIX, PHP-FPM и Tarantool. И не забудь показать ссылку своей подружке, ведь необходимо достаточно много голосов, чтобы получить статистически-достоверную картину голосования.
Поделиться с друзьями
oxidmod
Немного не по теме, но все же спрошу. А не идет ли гдето в недрах mail.ru разработка чегото похожего на Doctrine ODM для тарантула? Хотелось бы попробовать, но останавливает необходимость писать поверх клиента свой костыль-орм
danikin
Мы пока размышляем на эту тему. Как вариант предлагаем вам попробовать написать на Lua прямо внутри Tarantool, используя его не только как СУБД, но и как сервер приложений.
nekufa
Мы для себя написали простую обёртку и используем её в продакшне.
Посмотреть можно здесь: https://github.com/tarantool-php/mapper