Задача выглядит обманчиво простой — рядом с баннером игры из Одноклассников показывать текстовый тизер «эту игру играет Кот Матроскин и ещё 5 твоих друзей» (имя и количество берутся из друзей пользователя на Одноклассниках).
Как обрабатывать граф друзей проекта Одноклассники для этой задачи?
На этот простой вопрос дают разные ответы:
- взять графовую базу данных
- использовать матрицу инцидентности
- использовать список смежных вершин.
Если уточнить, что сырые данные занимают полтора терабайта, в графе 200 миллионов вершин и 13 миллиардов связей, то ручные решения сразу отметаются.
«Графовая база данных!» Стоит озвучить нагрузку в десятки тысяч запросов секунду и требования отвечать за миллисекунды (тысячные доли секунды!) как графовые базы сразу оказываются за бортом — типичное время ответа на простые запросы — единицы секунд.
Экс-разработчик MySQL и SciDB, ныне ведущий разработчик myTarget Олег Царёв расскажет, как решалась эта непростая задача в рамках проекта.
3. О чём этот доклад
Социальный граф
«Одноклассников»
+
myTarget
4. О чём этот доклад
• Социальный граф
«Одноклассников» + myTarget
• Можно ли из этого получить деньги?
• Каким именно образом?
• Какие технические проблемы
придётся решить?
5. Пара слов о докладчике
• Ведущий разработчик проекта
• Отвечает на вопросы «Как?», «Почему?»,
«Сколько?», «Где?»
• Оптимизирует узкие места
• Обучает коллег, руководит коллегами
• Пилил три разных СУБД (в том числе MySQL)
• Подслушивает в курилке умных людей
• Имеет вредный характер
6. Постановка задачи
"У нас есть задача использовать социальный граф
пользователя в рекламе. Первая модельная задача, которую
мы хотим решить - показывать пользователю, кто из его
друзей играет в какую-то игру (например, в ту, в которую
зашел сейчас пользователь или которую мы рекламируем).
Мы хотим иметь возможность получить эту информацию
очень быстро, т.к. хотим получить ее прямо во время
выполнения запроса, не подготавливая при этом обширно
данные.
Исходно у нас есть набор пользователей, для каждого
пользователя известен список его друзей и игр, в которые
он играет."
10. Это очень простая задача
#!/usr/bin/env python
friends = {1: [2,3], 2: [1], … }
games = {1: [game1], 2: [game2], …}
wp = defaultdict(defaultdict([]))
ok_id = 1
for friend_id in friends[ok_id]:
for game in games[friend_id]:
wp[ok_id][game].append(friend_id)
11. Технические требования
• Десятки* тысяч запросов в секунду
• Максимальное время ответа – единицы
миллисекунд
• Чем меньше требуется ресурсов – тем лучше
12. Дружба: общая информация
• Социальный граф
• Вершины: 200 миллионов пользователей
• Рёбра: 13 миллиардов связей
• Граф импортируется в виде лога обновлений
• По логу обновлений можно получить
состояние графа в любой момент времени
• Граф используется для других задач
• Размер лога за всё время: 1.2 терабайт
• Ежедневные обновления: 380 мегабайт
13. Дружба: как устроены события
События со следующими атрибутами:
• event_id: монотонно возрастает
• event_type:
• A – связь добавилась
• D – связь удалена
• ok_id_1: кто дружит
• ok_id_2: с кем дружит
• timestamp: когда связь поменялась
• Есть другие атрибуты (исключил из
рассмотрения)
14. Дружба: актуальный статус связи
Как узнать: дружат два пользователя или нет?
1. Найти все события (ok_id_1, ok_id_2)
2. Отсортировать по timestamp
3. Состояние связи: event_type последнего –
состояние связи
• Может поступить timestamp из прошлого (!)
• События могут дублироваться (!)
• Для обновления графа нужно хранить
последнее (по timestamp) обновление и
удалённые связи
15. Дружба: различные представления графа
• Обновляемое представление:
• живые связи: 360 гигабайт
• удалённые связи: 173 гигабайта
• всего: 533 гигабайта
• Необновляемое представление:
• Список рёбер: 200 гигабайт
• Списки смежных вершин: 90 гигабайт
16. Игры: общая информация
• Исходные данные – лог посещения страниц с
игрой пользователями:
(ok_id, game, timestamp)
• Лог за два месяца: 1.5 терабайта
• Если агрегировать: 20 гигабайт
17. Ключевые вопросы
• Как хранить граф?
• Как обновлять граф?
• Как хранить игры пользователей?
• Как обновлять игры пользователей?
• Как выбирать игры друзей?
• Как уложиться в таймауты?
• Как выдержать необходимую нагрузку?
18. Ключевые вопросы
• Группа вопросов №1
• Как хранить граф?
• Как обновлять граф?
• Как хранить игры пользователей?
• Как обновлять игры пользователей?
• Группа вопросов №2
• Как выбирать игры друзей?
• Как уложиться в таймауты?
• Как выдержать необходимую нагрузку?
19. Ключевые вопросы
• Хранение (вычисления)
• Как хранить граф?
• Как обновлять граф?
• Как хранить игры пользователей?
• Как обновлять игры пользователей?
• Доставка
• Как выбирать игры друзей?
• Как уложиться в таймауты?
• Как выдержать необходимую нагрузку?
20. Возможные решения
• Графовые базы данных
• Реляционные СУБД
• MySQL: join + group by
• PostgreSQL: CTE / WITH … RECURSIVE BY
• NoSQL: Tarantool
• Собственное решение
• HDFS / Hadoop
21. Графовые базы данных
• http://www.highload.ru/2014/abstracts/1611.html
• Чудовищно медленный импорт данных
• Поиск соседей вершины – больше секунды.
• Ни у кого в команде нет опыта эксплуатации и
использования.
• Непрогнозируемый результат и время
разработки.
• Условно годится (?) для хранение
• Не годится для доставка
22. Реляционные СУБД: PostgreSQL
• http://www.slideshare.net/quipo/rdbms-in-the-
social-networks-age
• Шикарно! Хочу попробовать.
• Нет опыта эксплуатации PostgreSQL L
• Не рискнул ввязываться.
• Расчёт - всего один запрос.
• Идеально для хранение (вычисления)
• Едва ли подойдет для доставка
• Нужно тестировать
23. Реляционные СУБД: MySQL (хранение)
• Если коротко: даже не пытайтесь
• >500 миллионов ключей: падение
скорости записи примерно в 20-25 раз
• За две недели так и не смогли выгрузить
• 200 миллионов ключей, 65 партиций...
• Больше 20 партиций => дохнет
• Для хранение не годится вообще L
24. Реляционные СУБД: MySQL (доставка)
MySQL и HandlerSocket на 1/128 части графа
• Все данные в памяти
• 6 CPU (+6 HT) – 100% использование
• MySQL 30 krps
• HandlerSocket 120krps
• MySQL: 99/95/90 frac: 100/41/19 мc
• HandlerSocket: 99/95/90 frac: 100/41/19 мc
• Каши не сваришь L
• Для доставка не годится
25. Собственное решение
• Начинающие программисты любят долго
писать Очень Универсальные Решения,
которые используются ровно в одном проекте
ровно до тех пор, пока программист не
уволится из проекта (а потом Очень
Универсальное Решение с облегчением
выпилят).
• У меня профдеформация: писал OLAP, OLTP и
NoSQL СУБД.
• И оценил срок разработки прототипа в 4
человеко-месяца минимум.
26. HDFS & Hadoop
• Широко используется в myTarget
• Группа Data Mining делает
умопомрачительные штуки
• http://www.highload.ru/2014/abstracts/1596.html
• Все данные уже там
• Прямой расчёт данных – почти сутки L
• Я смог это обойти (но про это позже)
• Идеально для хранения (вычисления)
• Не годится вообще для доставка
27. Tarantool
• Широко используется в myTarget
• Persistent, In Memory, Low Latency
• http://tarantool.org/
• Бенчмарк: 1 ядро, 100% загрузка
• 120 krps
• 99/95/90 frac: 6/3/2 мc
• Если пошардить – ещё меньше
• Идеален для доставка
• Хранение - 533 гигабайта RAM?...
28. Очевидное решение
• Считаем граф на hadoop (~90 гигабайт)
• Считаем игры на hadoop (~20 гигабайта)
• Заливаем в tarantool
1. вытаскиваем друзей пользователя:
• ok_id è [friend_id]
2. вытаскиваем игры друзей
• friend_idè [game]
3. Вытаскиваем имя и фамилию друга
• friend_idè {first name, surname}
29. Анализ и оценка
• У пользователей несколько* сот друзей в
среднем
• Шаг 1: одно хождение в тарантулы
• Шаг 2: несколько* сотен запросов, одно
хождение
• Шаг 3: единицы запросов, одно хождение
• X * 10 KRPS * 100-500 друзей =
в лучшем случае 1..5X миллионов RPS (!)
30. Проблемы
• 3 хождения (3-4 мс) – слишком долго!
• Десятки* миллионов запросов – слишком
много!
• Не хватит сети
• Не хватит CPU
• Нужно искать другой путь L
31. Менее очевидное решение
• Считаем граф на hadoop (~90 Гб)
• Считаем игры на hadoop (~20 Гб)
• Считаем на hadoop игры друзей (~ 400 Гб)
• Заливаем в tarantool
1. вытаскиваем игры друзей пользователя:
• ok_id è [{game, [friend_id]}]
2. вытаскиваем имя и фамилию друга
• friend_id è {first name, surname}
32. Анализ и оценка
• Два хождения – жить можно (1-2 мс)
• 400 Гб RAM – многовато L
• Сетевой траффик большой (сеть лагала)
• Но уже работало почти как хотелось
• Почему бы не усечь данные?
33. Усечение данных – попытка 1
• «Давайте игнорировать дружелюбных (>1000
друзей) пользователей»
• Проблема: Иван Ургант дружит с >1200
пользователей
• «Ургант играет в эту игру? Я тоже хочу»
• На самом деле Иван Ургант не играет в игры
на Одноклассники (это просто контпример)
• И всё равно ничего не усекается L
34. Усечение данных – попытка 2
• «Давайте хранить лишь для активных
пользователей»
• Пока проверял – нашёл баг у ОК и два у нас
• Недельная аудитория – десятки* миллионов
пользователей
• Их друзья – почти все пользователи
«Одноклассников» è нужно обрабатывать
полный граф
• Так тоже не получится L
35. Усечение данных – попытка 3
• «Давайте хранить информацию для
рекламируемых игр»
• Всё равно если нет рекламы è нет показов è
не нужна информация
• Количество игр уменьшилось почти в 20 раз
• И ещё один небольшой трюк, про него позже
• Итоговые данные занимают 70 гигабайт
• …
• Un tequila, por favor!
36. Выводы
• Хороший фильтр данных экономит немало
ресурсов
• Искать ответ нужно не в программировании, а
в предметной области
• Данный фильтр уменьшил объём данных J
• Ясно как представлять данные для доставка
• Осталось разобраться с вычислениями
37. Вычисления: эксперименты
• Полный граф считался почти 20 часов
• После кучи оптимизаций – в районе 12 часов
• Игры сначала 8 часов, потом 1 час
• Игры друзей – почти 16 часов
• Hadoop постоянно умирал
• Вокруг моего стола висело облако мата и
проклятий
• Решение нашлось, но не сразу
• Решение: инкрементальные вычисления
38. Концепция поколений
• Как рассчитывать инкрементально граф?
• Давайте каждый цикл расчёта маркировать
«поколением»
• Считаем лишь новые данные
• Результаты – новое поколение
• Разница между поколениями 1..X и X+1 –
дельта
• По дельте обновляем tarantool
• Sounds like a plan!
39. Данные в tarantool
message WhoPlay.Game {
// название игры
optional string game = 1;
// число играющих друзей
optional uint32 friends_count = 2;
// ok_id некоторых играющих друзей
repeated uint64 friend_id_list = 3;
}
message WhoPlay.Data {
// информация по играм
repeated Game games = 1;
}
40. Обновление данных в tarantool
message WhoPlay.Update {
// ok_id пользователя
optional uint64 user_id = 1;
// список игр, для которых больше нет who
play
repeated string deleted = 2;
// обновления / добавление информации по
играм
repeated Game games = 3;
}
44. Инверсия связей
friend_id => {
game: {ADDED: [ok_id],
DELETED: [ok_id],
KEEP: [ok_id]}
}
• Последний штрих: из списка играющих в игру друзей
случайным образом выбираем пять, остальные
выкидываем (для большинства пользователей не
выкидывает ничего, но вот отдельных странных
порезало основательно)
45. Результаты: хранение (вычисление)
• Порядка 600 гигабайт HDFS
• Порядка 8 часов цикл расчёта
• Порядка 2 часов заливка в tarantool
• Пересчитываем раз в сутки
• Высокая волатильность данных: 20%
• Работает стабильно
• 4.5 KLOC python
47. Результаты: бизнес
• Живёт на двух серверах
• Повышает CTR
• В confluence осталось куча документов
от research
• Получен бесценный опыт
• Зарабатывает деньги
• Этот доклад
48. Выводы
• Начинайте с аналитики
• Делайте бенчмарки
• Сверяйтесь с техническими
требованиями
• Сверяйтесь с бизнес-требованиями
• Не пытайтесь найти серебряную пулю
• Не пытайтесь изобретать велосипед