Архитектура Ленты на
Одноклассниках
Алина Васильева
разработчик сервиса Лента
Одноклассники
alina.vasiljeva@odnoklassniki.ru
Одноклассники
• Социальный портал
• Граф друзей
• Аудитория:
– 250 млн аккаунтов
– 6 млн пользователей онлайн
– более 40 млн посетителей в день
1
в среднем
90 друзей
12 групп
Лента
2
Лента
• Один из ключевых сервисов портала
• Цели
– показ пользователю действий и событий друзей и групп
– распространение контента по порталу
– стимулирование пользователей создавать контент
• Задачи
– показ интереснейшего контента максимально большому
количеству пользователей
– агрегация
– группировка
– сортировка
– выживание в условиях высокой нагрузки
3
События
• В ленту попадают события более чем 100 разных типов
– Фото
– Статусы
– Дружбы
– Классы
– Подарки
– Игры
– Группы
– Музыка
– Видео
– и многое другое...
4
Больше всего добавляется
классов к фото
По показам лидируют
темы из групп
Запись и хранение событий
• Для достижения цели необходимо записывать и
хранить события каждого пользователя
• Главная сущность: Feed
• Для каждого пользователя нужно хранить List<Feed>
5
class Feed {
long createDate;
short feedTypeId;
Long referenceId;
...
}
Запись и хранение событий
• SQL ?
6
• SELECT, JOIN, COUNT...
Нагрузка
• В час пик пользователи генерируют 1 миллион
событий в 5 минут (>3000 операций записи в секунду)
7
• Запросы ленты конкретного пользователя: >4000 в сек
• Запросы общей ленты на главной: >9000 в сек
История развития хранилища фидов
• SQL решение не рассматривалось изначально
– высокая нагрузка
– необходима высокая доступность
– характер данных, запросов и нагрузки
8
История развития хранилища фидов
• Oracle Berkeley DB
– Key / Value хранилище CP-типа
– Key: long feedOwnerId
– Value: List<Feed>  byte[]
– Хранятся последние N записей (500)
9
• Через ~2 года добавили Voldemort + Tarantool
– Key / Value хранилище AP-типа
– Хранение данных в памяти
– Более высокая производительность
– Более высокая доступность
– Хранятся последние 30 записей
– Использовался одновременно с Berkeley
Переход на Cassandra
• Причины
– Нестабильность Berkeley
– Ограничение на объѐм хранимых данных
– Упирались в трафик
• необходимо пересылать по сети все данные
• Преимущества
– Высокая доступность, распределѐнность
– Масштабирование, восстановление на ходу
– Высокая скорость записи
– Скорость чтения не зависит от объема
– Возможность реализации бизнес-логики
10
Cassandra
• Хранение данных в Column Family
– Row Key
– Column Key + Column Value + Timestamp
11
Хранение фидов в Cassandra
12
Общая картина
Feed Proxy -
координирующее
Java-приложение
13
Инфраструктура ленты
14
• Сервера распределены по трѐм дата-центрам
Feed Proxy кластер:
21 000 запросов/сек
Feed Storage кластер:
120 000 запросов/сек
Инфраструктура ленты
15
• Выдерживаем потѐрю целого дата-центра
Приложения
обновляются
путѐм отключения
серверов в одном ДЦ
Обновление
кластера Proxy:
5 минут
Обновление
кластера Storage:
30 минут
Общая лента
• Задача – показ ленты событий от всех друзей
16
распространение
информации
получение
информации
Общая лента
17
Список подписок
• При дружбе пользователи добавляются друг к другу в
список подписок (ObservedList)
• Формируется список друзей, за событиями которых
пользователь следит и которые попадают к нему в ленту
• Храним список подписок для каждого пользователя
18
class ObservedList {
List<ObservedItem> items;
...
}
class ObservedItem {
long feedOwnerId;
long lastUserAccessTime;
long lastFeedOccurrenceTime;
...
}
Хранение списка подписок
• SQL снова не подходит
– 350 добавлений в секунду (18 млн за сутки)
– 9000 чтений в секунду
– характер данных и запросов
• Хранили в Berkeley DB, перешли на Cassandra
19
1. Получаем список подписок из ObservedList
2. По каждой подписке получаем список фидов из Storage
3. Объединяем, сортируем
Алгоритм сборки общей ленты [simplified]
List<Feed> feeds = new ArrayList<Feed>();
ObservedList observedList = getObservedList(feedFollowerId);
for (ObservedItem item: observedList.getItems()){
List<Feed> userFeeds =
getFeeds(observedItem.getFeedOwnerId());
feeds.addAll(userFeeds);
}
Collections.sort(feeds, FEEDS_COMPARATOR);
20
Выполняется
на Feed Proxy
И опять нагрузка
• 9000 запросов получения общей ленты в секунду
– даже с учѐтом кэширования на вебе
– помним про 90 друзей и 12 групп у пользователей!
– 9000 * 102 = 918 000 походов в базу в секунду
• Базы данных не в состоянии эффективно обработать
такое количество запросов
21
Нужен кеш событий
общих лент пользователей
Feed Cache
• Java-приложение, цель которого получение, хранение
и отдача общих лент
22
Масштабы Feed Cache
• Самое мощное приложение инфраструктуры ленты
• 64 сервера, распределѐнные по трѐм дата-центрам
• Кол-во запросов: 100 тысяч в секунду (~1500 на сервер)
• В кэши входит 100 млн событий в 5 минут
• Трафик: 1 Гб/сек
– 10 Мб/сек входящего на 1 сервер
– 6 Мб/сек исходящего на 1 сервер
• Объем хранимых данных: 6 ТБ (в RAM)
– ~100 ГБ на 1 сервере
– в среднем 100 KБ на пользователя
23
Хранение данных в Feed Cache
• По ключу идентификатора пользователя хранится список фидов
• Длина списка ограничена
– 1000 записей, но, бывает, уменьшаем (при повышенной нагрузке)
– специальный алгоритм по вытестению данных из кэша
– планируем переход на Cassandra
24
Хранение данных в Feed Cache
• Помимо списка фидов хранятся также дополнительные данные
– время последнего логина
– время последнего открытия ленты
– время последнего обновления данных с Feed Proxy
– значение последнего выбранного фильтра
• Данные сериализуются и хранятся в Off-Heap памяти
• Раз в 12 часов сервер записывает данные на диск (снепшот)
• При рестарте приложение стартует со снепшота ~8 минут
• При старте без снепшота есть механизмы обеспечивающие
плавную загрузку данных
25
Кластер Feed Cache
26
• Шардинг – каждый сервер обслуживает 1/64 часть
пользователей
– партиционирование по id пользователя
• У каждого сервера есть «заместитель», на который
перенаправляются запросы в случае недоступности
• Обновление приложений всего кластера занимает
~1 час (обновление по ¼ серверов)
Feed Cache - Отдача данных
• Feed Cache запрашивает новые события с Feed Proxy по
истечению временного периода (15 минут)
– инфраструктура уже выдерживает обновления раз в 1 минуту
27
Время последнего события
• И всѐ же нагрузка на хранилище фидов получается
черезмерно большая
• При каждом обновлении на Feed Proxy обходить базы
всех друзей неоправданно дорого
• ведь новые события могли появиться всего у пары из них, а то
вообще ни у кого
• Решение - отдельно хранить дату добавления
последнего события в ленту пользователя
• Отдельная база: Voldemort + Tarantool (key / value)
– long feed_owner_id  long last_create_date
28
База UpdateInfo
• При добавлении нового события обновляется
timestamp в базе UpdateInfo
• Далее это значение проверяется при сборке событий
для общей ленты
29
• Перед запросом в базу Feeds проверяется значение
UpdateInfo - появились ли новые события
Алгоритм сборки общей ленты [improved]
30
public List<Feed> collectRecentFeeds(
long feedFollowerId, long lastUpdated){
1. ПОЛУЧАЕМ СПИСОК ПОДПИСОК (OBSERVED LIST)
2. ДЛЯ КАЖДОЙ ПОДПИСКИ
3. ПРОВЕРЯЕМ UpdateInfo
if (hasNewFeeds(item.getId(), lastUpdated)){
4. ПОЛУЧАЕМ ФИДЫ
5. ДОБАВЛЯЕМ ФИДЫ В ОБЩИЙ СПИСОК
}
}
...
}
Хотя в UpdateInfo тоже ходим не каждый раз.
Значения кэшируются в ObservedList.
Группировка событий
• Проблема – повторяющиеся бесполезные события
31
Группировка событий
• Решение – группировка событий
32
• Исключение событий
Группировка событий
• Решение – инфраструктура мержеров событий
33
List<Feed> feeds = getFeeds();
for (Merger merger: mergers){
merger.mergeFeeds(feeds);
}
Длина списка
уменьшается
на ~25%
Приоритеты событий
• Проблема – событий много, нужно показать
пользователю самое интересное
• Решение – подсчѐт весов событий и пересортировка
на их основании
• Вес подсчитывается при входе события в Feed Cache
• Также анализируется состояние кэша
– подсчитывется сколько событий каких типов уже присутствует в кэше
34
множество дополнительных коэффициентов и параметров
Feed Stats
• Отдельное Java-приложение с хранением данных в
Cassandra
• Сбор статистики о предпочтениях пользователей
• Записывает действия, которые пользователь
совершает по отношению к своим друзьям
– 53 000 записей в секунду
• На основании собранной статистики подсчитывает
веса друзей
• Далее этот вес используется лентой при подсчѐте
веса события
35
Полная картина
36
Real-time доставка событий
• Проблема: чтобы увидеть новые события
пользователю необходимо обновить страницу
• С учѐтом многоуровнего кэширования и ограничений
новое событие может прийти в ленту с задержкой
• Задача: доставлять события в ленты пользователей
моментально и автоматически
37
Real-time доставка событий
После добавления нового события Feed Storage
нотифицирует Feed Cache друзей в онлайне, отсылая
им фид, который далее переправляется прямо на Web
38
Спасибо!
Алина Васильева
разработчик сервиса Лента
Одноклассники
alina.vasiljeva@odnoklassniki.ru
odnoklassniki.ru/alina
v.ok.ru
job@odnoklassniki.ru

Архитектура Ленты на Одноклассниках

  • 1.
    Архитектура Ленты на Одноклассниках АлинаВасильева разработчик сервиса Лента Одноклассники alina.vasiljeva@odnoklassniki.ru
  • 2.
    Одноклассники • Социальный портал •Граф друзей • Аудитория: – 250 млн аккаунтов – 6 млн пользователей онлайн – более 40 млн посетителей в день 1 в среднем 90 друзей 12 групп
  • 3.
  • 4.
    Лента • Один изключевых сервисов портала • Цели – показ пользователю действий и событий друзей и групп – распространение контента по порталу – стимулирование пользователей создавать контент • Задачи – показ интереснейшего контента максимально большому количеству пользователей – агрегация – группировка – сортировка – выживание в условиях высокой нагрузки 3
  • 5.
    События • В лентупопадают события более чем 100 разных типов – Фото – Статусы – Дружбы – Классы – Подарки – Игры – Группы – Музыка – Видео – и многое другое... 4 Больше всего добавляется классов к фото По показам лидируют темы из групп
  • 6.
    Запись и хранениесобытий • Для достижения цели необходимо записывать и хранить события каждого пользователя • Главная сущность: Feed • Для каждого пользователя нужно хранить List<Feed> 5 class Feed { long createDate; short feedTypeId; Long referenceId; ... }
  • 7.
    Запись и хранениесобытий • SQL ? 6 • SELECT, JOIN, COUNT...
  • 8.
    Нагрузка • В часпик пользователи генерируют 1 миллион событий в 5 минут (>3000 операций записи в секунду) 7 • Запросы ленты конкретного пользователя: >4000 в сек • Запросы общей ленты на главной: >9000 в сек
  • 9.
    История развития хранилищафидов • SQL решение не рассматривалось изначально – высокая нагрузка – необходима высокая доступность – характер данных, запросов и нагрузки 8
  • 10.
    История развития хранилищафидов • Oracle Berkeley DB – Key / Value хранилище CP-типа – Key: long feedOwnerId – Value: List<Feed>  byte[] – Хранятся последние N записей (500) 9 • Через ~2 года добавили Voldemort + Tarantool – Key / Value хранилище AP-типа – Хранение данных в памяти – Более высокая производительность – Более высокая доступность – Хранятся последние 30 записей – Использовался одновременно с Berkeley
  • 11.
    Переход на Cassandra •Причины – Нестабильность Berkeley – Ограничение на объѐм хранимых данных – Упирались в трафик • необходимо пересылать по сети все данные • Преимущества – Высокая доступность, распределѐнность – Масштабирование, восстановление на ходу – Высокая скорость записи – Скорость чтения не зависит от объема – Возможность реализации бизнес-логики 10
  • 12.
    Cassandra • Хранение данныхв Column Family – Row Key – Column Key + Column Value + Timestamp 11
  • 13.
  • 14.
    Общая картина Feed Proxy- координирующее Java-приложение 13
  • 15.
    Инфраструктура ленты 14 • Серверараспределены по трѐм дата-центрам Feed Proxy кластер: 21 000 запросов/сек Feed Storage кластер: 120 000 запросов/сек
  • 16.
    Инфраструктура ленты 15 • Выдерживаемпотѐрю целого дата-центра Приложения обновляются путѐм отключения серверов в одном ДЦ Обновление кластера Proxy: 5 минут Обновление кластера Storage: 30 минут
  • 17.
    Общая лента • Задача– показ ленты событий от всех друзей 16 распространение информации получение информации
  • 18.
  • 19.
    Список подписок • Придружбе пользователи добавляются друг к другу в список подписок (ObservedList) • Формируется список друзей, за событиями которых пользователь следит и которые попадают к нему в ленту • Храним список подписок для каждого пользователя 18 class ObservedList { List<ObservedItem> items; ... } class ObservedItem { long feedOwnerId; long lastUserAccessTime; long lastFeedOccurrenceTime; ... }
  • 20.
    Хранение списка подписок •SQL снова не подходит – 350 добавлений в секунду (18 млн за сутки) – 9000 чтений в секунду – характер данных и запросов • Хранили в Berkeley DB, перешли на Cassandra 19
  • 21.
    1. Получаем списокподписок из ObservedList 2. По каждой подписке получаем список фидов из Storage 3. Объединяем, сортируем Алгоритм сборки общей ленты [simplified] List<Feed> feeds = new ArrayList<Feed>(); ObservedList observedList = getObservedList(feedFollowerId); for (ObservedItem item: observedList.getItems()){ List<Feed> userFeeds = getFeeds(observedItem.getFeedOwnerId()); feeds.addAll(userFeeds); } Collections.sort(feeds, FEEDS_COMPARATOR); 20 Выполняется на Feed Proxy
  • 22.
    И опять нагрузка •9000 запросов получения общей ленты в секунду – даже с учѐтом кэширования на вебе – помним про 90 друзей и 12 групп у пользователей! – 9000 * 102 = 918 000 походов в базу в секунду • Базы данных не в состоянии эффективно обработать такое количество запросов 21 Нужен кеш событий общих лент пользователей
  • 23.
    Feed Cache • Java-приложение,цель которого получение, хранение и отдача общих лент 22
  • 24.
    Масштабы Feed Cache •Самое мощное приложение инфраструктуры ленты • 64 сервера, распределѐнные по трѐм дата-центрам • Кол-во запросов: 100 тысяч в секунду (~1500 на сервер) • В кэши входит 100 млн событий в 5 минут • Трафик: 1 Гб/сек – 10 Мб/сек входящего на 1 сервер – 6 Мб/сек исходящего на 1 сервер • Объем хранимых данных: 6 ТБ (в RAM) – ~100 ГБ на 1 сервере – в среднем 100 KБ на пользователя 23
  • 25.
    Хранение данных вFeed Cache • По ключу идентификатора пользователя хранится список фидов • Длина списка ограничена – 1000 записей, но, бывает, уменьшаем (при повышенной нагрузке) – специальный алгоритм по вытестению данных из кэша – планируем переход на Cassandra 24
  • 26.
    Хранение данных вFeed Cache • Помимо списка фидов хранятся также дополнительные данные – время последнего логина – время последнего открытия ленты – время последнего обновления данных с Feed Proxy – значение последнего выбранного фильтра • Данные сериализуются и хранятся в Off-Heap памяти • Раз в 12 часов сервер записывает данные на диск (снепшот) • При рестарте приложение стартует со снепшота ~8 минут • При старте без снепшота есть механизмы обеспечивающие плавную загрузку данных 25
  • 27.
    Кластер Feed Cache 26 •Шардинг – каждый сервер обслуживает 1/64 часть пользователей – партиционирование по id пользователя • У каждого сервера есть «заместитель», на который перенаправляются запросы в случае недоступности • Обновление приложений всего кластера занимает ~1 час (обновление по ¼ серверов)
  • 28.
    Feed Cache -Отдача данных • Feed Cache запрашивает новые события с Feed Proxy по истечению временного периода (15 минут) – инфраструктура уже выдерживает обновления раз в 1 минуту 27
  • 29.
    Время последнего события •И всѐ же нагрузка на хранилище фидов получается черезмерно большая • При каждом обновлении на Feed Proxy обходить базы всех друзей неоправданно дорого • ведь новые события могли появиться всего у пары из них, а то вообще ни у кого • Решение - отдельно хранить дату добавления последнего события в ленту пользователя • Отдельная база: Voldemort + Tarantool (key / value) – long feed_owner_id  long last_create_date 28
  • 30.
    База UpdateInfo • Придобавлении нового события обновляется timestamp в базе UpdateInfo • Далее это значение проверяется при сборке событий для общей ленты 29
  • 31.
    • Перед запросомв базу Feeds проверяется значение UpdateInfo - появились ли новые события Алгоритм сборки общей ленты [improved] 30 public List<Feed> collectRecentFeeds( long feedFollowerId, long lastUpdated){ 1. ПОЛУЧАЕМ СПИСОК ПОДПИСОК (OBSERVED LIST) 2. ДЛЯ КАЖДОЙ ПОДПИСКИ 3. ПРОВЕРЯЕМ UpdateInfo if (hasNewFeeds(item.getId(), lastUpdated)){ 4. ПОЛУЧАЕМ ФИДЫ 5. ДОБАВЛЯЕМ ФИДЫ В ОБЩИЙ СПИСОК } } ... } Хотя в UpdateInfo тоже ходим не каждый раз. Значения кэшируются в ObservedList.
  • 32.
    Группировка событий • Проблема– повторяющиеся бесполезные события 31
  • 33.
    Группировка событий • Решение– группировка событий 32 • Исключение событий
  • 34.
    Группировка событий • Решение– инфраструктура мержеров событий 33 List<Feed> feeds = getFeeds(); for (Merger merger: mergers){ merger.mergeFeeds(feeds); } Длина списка уменьшается на ~25%
  • 35.
    Приоритеты событий • Проблема– событий много, нужно показать пользователю самое интересное • Решение – подсчѐт весов событий и пересортировка на их основании • Вес подсчитывается при входе события в Feed Cache • Также анализируется состояние кэша – подсчитывется сколько событий каких типов уже присутствует в кэше 34 множество дополнительных коэффициентов и параметров
  • 36.
    Feed Stats • ОтдельноеJava-приложение с хранением данных в Cassandra • Сбор статистики о предпочтениях пользователей • Записывает действия, которые пользователь совершает по отношению к своим друзьям – 53 000 записей в секунду • На основании собранной статистики подсчитывает веса друзей • Далее этот вес используется лентой при подсчѐте веса события 35
  • 37.
  • 38.
    Real-time доставка событий •Проблема: чтобы увидеть новые события пользователю необходимо обновить страницу • С учѐтом многоуровнего кэширования и ограничений новое событие может прийти в ленту с задержкой • Задача: доставлять события в ленты пользователей моментально и автоматически 37
  • 39.
    Real-time доставка событий Последобавления нового события Feed Storage нотифицирует Feed Cache друзей в онлайне, отсылая им фид, который далее переправляется прямо на Web 38
  • 40.
    Спасибо! Алина Васильева разработчик сервисаЛента Одноклассники alina.vasiljeva@odnoklassniki.ru odnoklassniki.ru/alina v.ok.ru job@odnoklassniki.ru