суббота, 24 апреля 2010 г.

Делаем ленту обновлений на MongoDB + PHP

Делаем ленту обновлений на MongoDB + PHP


Вы, конечно, много раз видели ленту обновлений на фейсбуке и твиттере. В двух словах — это список событий, которые произошли недвано на сайте и касаются Вас (чаще всего это деятельность Ваших друзей). Это очень удобный инструмент информирования пользователей и неотъемлемая часть современных социальных проектов. А как обстоит дело с реализацией?


На первый взгляд все очень просто, а на практике все сложно. Давайте разберемся детально (на примере MongoDB + PHP, заодно посмотрим на эту новую шумную СУБД).


Что такое эта “лента обновлений”?


лента обновлений


На этом скрине Вы видите ленту обновлений или “news feed” с фейсбука (facebook.com). Там представлены все события Ваших друзей.


Как будем делать? На первый взгляд все кажется довольно просто. У нас есть ограниченный набор действий (загрузить фото, обновить статус и т.п.). Для реализации подобной ленты новостей нужно выбрать все такие действия для каждого друга и отсортировать их по времени. Но сразу же становится видна недальновидность такого подхода:



  • Очень тяжелые выборки в нескольких таблицах (выбрать все фотки, все статусы и т.п.)

  • Требуется дополнительная сортировка и агрегирование данных на стороне системы (а не СУБД)

  • Кеширование будет неоправданным, т.к. сделает ленту неактуальной

  • Решение не масштабируется (не возможен шардинг)


Проведем небольшой анализ:



  1. Новые события в ленте могут появляться довольно динамично и сразу должны становиться доступны друзьям (загрузил фотку — друзья сразу об этом узнали). Тем не менее короткие задержки тут не критичны (в пределах минут).

  2. Продвинутый функционал ленты обычно включает в себя управление приватностью (кто из друзей и какие действия может видеть)

  3. Решение должно горизонтально масштабироваться. Учитывая то, что в больших системах количество генерируемых событий будет огромным, это самый важный момент


Архитектурное решение


Для начала уйдем от необходимости собирать все события с разных таблиц. Создадим одну таблицу типа “обновления” и будем складывать туда все события с такой информацией:



  • кто совершил действие (автор)

  • время действия

  • доп. данные о действии (напр., список загруженных фоток)


Для генерации ленты обновлений нам нужно выбрать из этой таблицы все действия, совершенные друзьями пользователя:


SELECT * FROM updates WHERE user_id IN (1,2,3,4) ORDER BY time DESC

Тут 1,2,3,4 — это ID друзей пользователя


Уже лучше. Сортировку можно делать прямо на стороне СУБД, не нужно делать дополнительную агрегацию. Но остаются следующие проблемы:



  • Таблица не шардится (ее невозможно разделить по какому-либо критерию для горизонтального масштабирования ввиду неопределенности списков друзей)

  • Учитывая возможный большой размер таблицы, запросы к ней могут быть очень медленными

  • Если понадобится учитывать приватность сообщений, запросы будут крайне тяжелыми


Опять не подойдет, думаем дальше:


Генерация пользовательской ленты


Необходимо устранить всякую записимость генерации ленты от количества данных и сложности функционала. Это возможно только тогда, когда мы для каждого пользователя генерируем ленту на лету. Т.е. при очередном событии делаем следующее:



  1. Выбераем список друзей пользователя

  2. Проверяем правила приватности для каждого из них

  3. Вставляем событие в персональную ленту тем, кому нужно


В этом случае мы избавляемся от проблемы медленной генерации ленты, т.к. ее теперь не нужно генерировать. Мы просто читаем ее из СУБД (элементарный запрос, никаких преобразований). Самое важное — пользовательская лента теперь на 100% персональная (из нее даже можно что-нибудь удалить, не затронув остальных пользователей).


Но теперь при каждом событии будет выполняться довольно много операций проверок и вставки события в нужную ленту. Это может очень замедлить работу сайта. Выход очень простой. Все эти действия очень хорошо подходят для выполения на фоне (смотрите очереди сообщений). Например:



  1. Пользователь загружает фотку

  2. Обрабатываем его запрос, сохраняем фото и отправляем пользователю ответ

  3. В фоне (асинхронно) выполняем обработку события и обновляем ленты его друзей


Платформа


Для управления лентами нам понадобятся простые операции работы со списками: добавить/удалить элемент, сортировка и возможно фильтрация.

Понятно, что использовать тяжелую СУБД, такую как MySQL, нецелесообразно для таких простых операций. Очень хорошо в этом случае подойдет, например, Redis или же MongoDB.


В этом примере мы рассмотрим реализацию на основе MongoDB. Для хранения ленты будет использовать внутренний массив документа. В качестве клиентской реализации используем PHPMongo.


Для выполнения задач на фоне можно использовать любую удобную систему очередей (например, RabbitMQ).


Пример


Для начала опишем главную функцию — вывод ленты обновлений (функцию get_feed() опишем позже):



<?
# Получаем ленту
# $user_id содержит идентификатор текущего пользователя
$feed = get_feed($user_id);

echo "<h3>Что делают Ваши друзья</h3>";

# Выводим все события из ленты
foreach ( $feed as $feed_item )
{
# Время, имя пользователя и текст события
echo "{$feed_item['time']} :: {$feed_item['user_name']} {$feed_item['text']}<br />";
}

Обновление ленты


Теперь посмотрим, как будет выглядет типичное действие, которое обновляет ленты друзей пользователя. Например, пользователь меняет свой статус. Обработчик будет выглядеть следующим образом:



<?
$status = $_POST['status'];

# коннектимся к Mongo (по умолчанию — localhost:27017)
$m = new Mongo();

# Обновляем статус пользователя (БД user, коллекция status)
# "'upsert' => true" — вставить запись, если ее нет
$m->user->status->update(
array('user_id' => $user_id),
array('$set' => array('status' => $status)),
array('upsert' => true)
);

# Регистрируем событие для лент (реализация дальше)
update_feed($user_id, 'обновил статус: ' . $status );

Регистрация события


В предыдущем кусочке кода функция update_feed() регистрировала событие для добавления в ленты друзей. Реализация этой функции будет следующая:



<?
function update_feed($user_id, $text )
{
# Получаем список друзей пользователя
$friends = get_friends($user_id);

$m = new Mongo();

# Вставляем событие каждому другу в ленту
foreach ( $friends as $id )
{
# Получаем имя друга
$user_name = get_user_name($id);

# $data = array(
'time' => date('H:i (m.d)'),
'user_name' => $user_name,
'text' => $text
);

# Вставляем событие в массив Mongo-объекта "list"
# БД user, коллекция feed
$m->user->feed->update(
array('user_id' => $user_id),
array('$push' => array('list' => $data)),
array('upsert' => true)
);
}
}

Чтение ленты


Осталось описать функцию чтения ленты для пользователя:



<?
function get_feed($user_id)
{
$m = new Mongo();

# Выбираем события из БД
$data = $m->user->feed->findOne( array('user_id' => $user_id) );

return $data['list'];
}

Некоторые замечания


Нужно следить, чтобы количество элементов в массиве не росло очень сильно (например, сотни тысяч), иначе объекты будут очень тяжелыми. Массивы необходимо периодически обрезать (пока в MongoDB нет удобного механизма это делать — надеюсь, появится).


Для реализации приватности необходимо в функцию update_feed() добавить функционал по проверке прав доступа для каждого пользователя.


В MongoDB пока довольно ограниченный набор функционала с вложенными массивами, поэтому фильтрацию ленты (если понадобится) придется делать на стороне PHP. Будем следить за развитием MongoDB.


Не забудьте поставить индексы на колонки по которым делаете выборки в Mongo (как это делать?).



Google Bookmarks Digg I.ua Ru-marks Ruspace Zakladok.net Reddit delicious Technorati Yahoo My Web News2.ru БобрДобр.ru Memori.ru rucity.com



Related posts:

  1. Твиттер на основе MemcacheDB и PHP
  2. PHP + Redis платформа “Ключ=Значение”
  3. Mongo DB — документо-ориентированная база данных и MySQL

Комментариев нет:

Отправить комментарий