Рассказ будет про один контентный проект, в котором мне пришлось переделать архитектуру. Ранее была реализована классическая Лампа-схема (Linux-Apache-MySQL-PHP). Но кол-во посетителей прибавлялось и прибавлялось, уже стало подходить к 1М хитов и сервер БД переставал справляться. Первым делом, я предложил докупить еще один серак, но в данном сегменте конвертация в партнерских программах довольно низкая, так что, руководство проекта немного пожмотилось.
Если, интересно, как мне пришлось изменить архитектуру и при этом еще прикрутить систему ротации и рейтингов, то добро пожаловать под кат.
Особенность данного проекта в том, что он раздает видео контент, который находится на сайтах-донорах, типа твоей трубы (YouTube). Сайт должен отображать только ВВ-коды (определенный HTML). Поэтому, не было необходимости постоянно генерить HTML на лету, а делалось это через определенное время, например, раз в сутки, правда потом заменили на ротацию через 1000 показов. Apache заменили на nginx, а сам nginx отдавал просто сгенерированный статический контент HTML.
Каждый раз, придя на сайт, посетитель должен увидеть что-то новенькое. А новенькое, как часто бывает – это хорошо забытое старенькое. Вобщем, нужна была ротация видео превью (о них чуть позже). Существует несколько алгоритмов ротации. Вы даже не представляете изощренность ума маркетологов. Поэтому, расскажу только про один, наиболее простой.
В первые 10 слотов, вставляются только новые превьюшки. Далее, выбираются 90 превьюшек данной категории с максимальным CTR. Кто не знаком с этим термином, это показатель кликабельности, от англ. click-through rate: отношение числа кликов на картинку к числу её показов.
Видео может быть потенциально популярным, а вот превьюшка не презентабельной. Это с большой вероятность может быть, так как вместо студента, который сидит и выбирает самые сочные момменты видео, сидит робот и генерит превьюшки случайно выбранного кадра. Поэтому рейтинг, вполне интересного видео может уйти в “даун”. Чтобы, разнообразить сайт, да и выровнять эффект случайного кадра, используется локальный рейтинг: генерится три превью от одного видео, которые тоже ротируются. В ходе естественного отбора, остаются наиболее привлекательные картинки. Есть еще система голосований: пальчик вверх/пальчик вниз, но её тех-реализация один в один похожа на систему ротации.
Но, мы здесь собрались не SEO-сказки слушать, а поделиться тех деталями. В общем, вся Лампа технология была заменена на сайто-генератор. Nginx работал на отдачу статики. Остаётся только реализовать подсчет CTR.
Так как общее кол-во видео на сайте составляло в районе 100К, то вполне можно выбрать персистентное in-memory хранилище. Какие у нас есть альтернативы: Redis, Aerospike,Tarntool.
Из-за хороших функциональных возможностей и дружественной русско-говорящей поддержки ребят из MailRu выбор пал на Tarantool. MySQL у нас ни куда не делся, в нем продолжают храниться BB-коды видео, списки категорий и наименований, описание контента и прочая информация, которая необходима для сайто-генерации. Но, так как БД практически не использовалась, то ему отвели минимум памяти.
Теперь более подробно про Tarantool (далее по тексту Т*). О нем много было написано в разных статьях Я постараюсь рассказать, как это применимо на практике, опуская настройку и инсталляцию.
Немного скучной теории, чтобы понять что к чему: Все данные в Т* хранятся в пространствах: space. Это аналог таблицы в SQL или коллекции в MongoDb. Как таблица состоит из строк, коллекция из документов, так пространство включает в себя множества кортежей (аналог строки в MySQL).
Кортеж состои из элементов или полей. Мне элементы кортежа удобно называть полями и я буду придерживаться этой терминологии, что не идет в разрез с документацией tarantool.org/doc/book/box/index.html. В отличие от строк таблицы, поля в кортеже не имеют названий, а имеют только порядковый номер. Хотя, как вы увидите в последствии, это не принципиально.
Каждый кортеж должен иметь первичный ключ. Первичный индекс может иметь один из следующих типов: TREE, HASH, BITSET или RTREE. Так же, на пространство можно наложить вторичный индекс, что позволяет делать такие уникальные выборки, которые не возможно сделать в редисе.
На рис 1 изображена аналогия MySQL и T*.
Для хранения рейтингов создается пространство stats. Для этого зайдем в консоль и выполним команды:
box.cfg{} – загружает дефолтную конфигурацию box.schema.space.create("stats") – создает новое пространство
Проверим, как создалось наше пространство:
tarantool> box.space --- - stats: temporary: false engine: memtx ...
И присвоим его переменной stats
tarantool> stats = box.space
Если бы мы составляли схему для БД или MongoDb, то выбрали бы следующую схему:
1 key - первичный ключ, совпадает с id видео 2 clicks_1 – кол-во кликов для первой картинки 3 clicks_2 – – || – второй картинки 4 clicks_3 – – || – третьей картинки 5 clicks_sum_1 – общее кол-во кликов для первой картинки 6 clicks_sum_2 – – || – второй картинки 7 clicks_sum_3 – – || – третьей картинки 8 show_1 все тоже самое для показов … 13 show_sum_3 14 ctr_1 ctr для первой картинки за последний промежуток 15 ctr_2 16 ctr_3 17 ctr_sum_1 ctr для первой картинки за весь период 18 ctr_sum_2 19 ctr_sum_3 20 ctr ctr по всем картинкам за последний промежуток 21 ctr_sum ctr по всем картинкам за весь период
Первая колонка — это номер поля, определим константами имена полей:
-- первое поле это первичный ключ clicks_1 = 2 clicks_2 = 3 . . . ctr_sum = 22
Создадим в нашем пространстве первичный ключ, выбираем тип HASH:
stats:create_index('primary', {type = 'hash', parts = {1, 'NUM'}})
Проверим, что создали:
tarantool> stats.index --- - 0: &0 unique: true parts: - type: NUM fieldno: 1 id: 0 space_id: 513 name: primary type: HASH primary: *0 ...
Очень хорошо, если получилось, а теперь создадим функцию, которая будет инкрементировать поле clicks_1, и для отладки вставим несколько записей:
stats:insert{1,0,0,0,0,0,0} stats:insert{2,0,0,0,0,0,0} stats:insert{3,0,0,0,0,0,0}
Сперва проверим, что понавставляли:
tarantool> stats:select{2} --- - - [2, 0, 0, 0, 0, 0, 0] …
Замечательно, у нас все работает! Теперь напишем код инкрементации поля:
tarantool> stats:update(2,{{ '+',2,1 }}) tarantool> stats:select{2} - [2, 1, 0, 0, 0, 0, 0] tarantool> stats:update(2,{{ '+',2,1 }}) - [2, 2, 0, 0, 0, 0, 0]
Команда update имеет следующие параметры:
primary key — номер ключа, по которой производится обновление
вторым параметром идет список действий, каждый элемент которого представляет триплет (список из трех элементов):
— тип действия, в данном случае сложение
— номер поля, над которы проводятся изменения
— число
Подробнее о команде update в документации: tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update
Мы видим, что с каждым выполнением stats:update данные для key=2 второго поля увеличиваются на 1. Запишем в более читабельном виде. Ранее мы должны были задать:
tarantool> clicks_1 = 2
Выполним:
tarantool> stats:update(2,{{ '+',clicks_1,1 }}) - [2, 4, 0, 0, 0, 0, 0]
Теперь обернем это в функцию:
function click_inc(key) stats:update(key,{{ '+',clicks_1,1 }}) end
И проверим:
tarantool> click_inc(2) tarantool> stats:select{2} --- - - [2, 5, 0, 0, 0, 0, 0] ... tarantool> click_inc(2) tarantool> stats:select{2} --- - - [2, 6, 0, 0, 0, 0, 0] …
Добавим в нашу функцию номер картинки (номер начинается 0 – первая картинка):
function click_inc(key, img_num) stats:update(key,{{ '+',clicks_1 + img _num, 1}}) end
После проверки, приведем функцию в более лучшый вид в отдельнойм файле: click.lua
function click_inc(key, img_num)
if img_num >3 then
return false
end
box.space.stats:update(key, {{'+',clicks_1 + img_num,1}})
return true
end
Как видим, логика исполнения функции довольно проста: первый агрумент – id видео, следующий номер его превью. Теперь рассмотрим, как все это может быть применимо. Для WEB проекта, эту функцию можно вызвать тремя c половиной способами:
— используя пользовательское АПИ: из скриптов PHP/Python/Perl/Java и т.д.
— через tarantool-http, на который будут проксироваться запросы через nginx
или собственный lua-скрипт, используя http.lib или иной web сервер (например xavante)
— непосредственно из nginx, используя nginx_upstreem модуль.
Если есть интерес, могу подробнее рассказать про второй способ, но в данном случаи нами был выбран третий вариант. В статье и так много буковок, так что про установку и настройку модуля можно прочитать в статье Строим сервисы на базе nginx & Tarantool от авторов Т*.
Итак, наш click.lua будет следующий:
#!/usr/bin/tarantool
box.cfg{
log_level = 5;
listen = 10001;
}
click_1 = 2;
function click_inc(key, img_num)
if img_num >3 then
return 0
end
box.space.stats:update(key, {{'+',click_1 + img_num,1}})
return 1
end
Проверим его:
curl http://127.0.0.1:8081/echo --data '{"method":"click_inc","params":[2,1], "id":0}' {"id":0,"result":[[1]]}
Для проверки подконектимся к запущенному экземпляру Т*:
tarantool> console=require("console") tarantool> console.connect("127.0.0.1:10001") tarantool: connected to 127.0.0.1:10001 - true 127.0.0.1:10001> stats = box.space.stats 127.0.0.1:10001> stats:select{2} - - [2, 7, 0, 0] ...
Так же мы можем инкрементировать счетчик второй картинки:
curl http://127.0.0.1:8081/echo --data '{"method":"click_inc","params":[2,2], "id":1}' {"id":0,"result":[[1]]}
Проверим результат:
127.0.0.1:10001> stats:select{2} - - [2, 7, 1, 0] ...
Мы рассмотрели как просто сделать систему подсчета кликов. Теперь перейдем к системе показов.
Каждая страница множества превьюшек, условно назовем “категории”, по идеи должна вызвать сотню (будем считать что за одна страница категории содержит сто превьюшек из этой категории) раз процедуру инкрементации показов: show_inc. Но, как мы понимаем – это не оптимально. Есть следующий вариант: В теле HTML странице генерится переменная.
<script>
show_pictupies=”1,2,3,4,5” /* тут перечислены все id показываемых картинок*/
</script>
и далее по AJAX передавать весь этот список. Но тут, кроме id картинки надо передать и её вариант показа, поэтому список может принять сл вид: “1-1, 2-1, 3-1, 4-2”, где циферка после знака минус показывает вариант показа.
Такого аналога функции, как explode в lua, к сожалению, не существует, поэтому погуглив использовали этот код
function split(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={} ; i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
Далее проходимя циклом по таблице. Для осуществления цикла, реализуем функцию-итератор:
function values(t)
local i = 0
return function() i = i + 1; return t[i] end
end
for it in values(tt) do show_inc(it, 2 ) end
Как вы уже догодались, show_inc очень похожа на click_inc с тем немногим исключением, что переменную click_1 заменяем на show_1. Поэтому, можно создать более уневерсальную функцию, stat_inc(key, field, img_number ).
function stat_inc(key, field, img_num)
if img_num >3 then
return 0
end
box.space.stats:update(key, {{'+',field + img_num,1}})
return 1
end
Так как, мы осуществляем подсчет двух типов ctr: первый с момента последней генерации и общий, то создадим процедуру click, которую будем вызывать через nginx:
function click( key, img_num)
stat_inc(key, clicks_1, img_num )
stat_inc(key, clicks_sum_1, img_num)
end
а show:
function show( key_list)
list = slipt( key_list, ',')
for it in value(list)
do
pos = string.find(it, “-”);
key = string.sub(it, 0, pos-1);
img_num = string.sub(it,pos+1)
stat_inc(key, shows_1, img_num)
stat_inc(key, shows_sum_1, img_num)
end
end
Таким образом, у нас подсчитываются и клики, и показы.
При интересе к этой теме, я могу описать, как расчитывать ctr и как выбирать картинки для формирования HTML.
Автор: akalend