Ламер с 20 летнем стажем
0,0
рейтинг
18 января в 01:32

Разработка → Система рейтингов в высоконагруженном проекте

Рассказ будет про один контентный проект, в котором мне пришлось переделать архитектуру. Ранее была реализована классическая Лампа-схема (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. Так же, на пространство можно наложить вторичный индекс, что позволяет делать такие уникальные выборки, которые не возможно сделать в редисе.

image

На рис 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
карма
72,5
рейтинг 0,0
Ламер с 20 летнем стажем
Реклама помогает поддерживать и развивать наши сервисы

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

Самое читаемое Разработка

Комментарии (22)

  • 0
    1M хитов за какое время? В секунду или в сутки?
    • 0
      в сек — это наверно только гугль имеет такой трафик
      • 0
        Ну не так это и много.
        • 0
          гугль обрабатывает в день 3 млрд поисковых запросов — в сек будет около 35 тыс
          по яндексу я не нашел информации (хотя при большом желании можно найти по слайдам с YAC)
          • 0
            гугль обрабатывает в день 3 млрд поисковых запросов — в сек будет около 35 тыс


            60к рек-сек усредненно за 2015 год.
            • 0
              информация за 15 год www.searchengines.ru/news/archives/google_obrabaty.html
              Поисковая система Google обрабатывает свыше 3 миллиардов запросов в день. Такую цифру назвал журналист Стивен Леви (Steven Levy) из Backchannel
              3 млр / 24 /60 /60 = ср по больнице 34722,22…
              но 60К в сек — это вполне нормальная цифра с учетом нелинейность и пиков
              • 0
                Я видел эту цифру на английском ресурсе и там речь шла только про мобильный траффик. Но мне лень искать. Пусть будет 35к.
                • 0
                  пустой спор… точную цифру знает только гугль
                  да и не столь это важно и к теме статьи не имеет отношение
                  но все равно — благодарю за комментарий
            • +1
              вот нашел интересный проект www.internetlivestats.com/google-search-statistics
              3,7 млн на момент просмотра
          • 0
            35rps как-то совсем мало для их масштабов… А где это у них указано? Почитаю хоть.
            • +1
              Просто погуглите «google load request rate» =). Почему мало. Вполне норм. Это же только поисковая система. Один из сотен сервисов гугла. У визы например жалких 2к рек-сек. А у твитера всего 6к в сек новых твитов.
            • 0
              www.3dnews.ru/645304 данные за 13 год, думаю сейчас больше но не на два порядка
            • 0
              чуть выше ссылка на расчеты
      • –1
        Онлайн игры и поболее хитов в сутки имеют. Возьмите тот же Clash of Clans. Это, безусловно, много, но не редкость.
        • +2
          «дайте мне точку опоры и я переверну землю» Архимед
          «дайте мне кучу серверов и я выдержу любую нагрузку» ⓒ
          но вопрос именно в том, чтоб выдержать повышающуюся нагрузку с имеющимися ресурсами
    • –2
      1M хитов в сутки — это 1000000 / 24 / 60 / 60 ~ 11.5 rps Это совсем не много
      • –1
        хотя по перцентилям может быть иная картина, 11rps — это среднее по больнице
  • 0
    >> как расчитывать ctr

    Довольно интересно было бы почитать. Так же интересно было бы увидеть статистику сравнения (если есть) с другими решениями (MySQL, Redis и т.п.).
    • +1
      какую статистику? пользователей больше не стало. Что касается времени отдачи HTML, то выросло раз в 5 (так как все равно HTML был в кеше).
      если хочешь сравнить производительность редиса и тарантула, то вот интересная статья highscalability.com/blog/2015/12/30/how-to-choose-an-in-memory-nosql-solution-performance-measur.html
      • 0
        Спасибо.
  • 0
    Прочитал. Хай-лоад не увидел. 1млн хитов в сутки это 11 рек-сек. Ну допустим они не равномерны и у вас даже 50 рек-сек. И где хайлоад?
    • +1
      хостов а не хитов (поправил), один хост проводит на сайте от 30 мин до 2х часов, да запросы сильно не равномерны
      в среднем от хоста идет просмотр от 15 до 50 страниц контента так что нагрузка 500 рек в сек — это норма

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