Главная > Теория и практика > Делаем ленту обновлений на MongoDB + PHP

Делаем ленту обновлений на 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

Статьи по теме

  1. Джек
    24 Апрель 2010 в 22:35 | #1

    Гарно написано, як тут використувується RabbitMQ ?

  2. 25 Апрель 2010 в 22:51 | #2

    @Джек
    Что именно Вас интересует в плане использования очередей?

  3. Andrey Kopachevsky
    26 Апрель 2010 в 11:14 | #3

    Хорошая статья. Меня тоже заинтересовал RabbitDB, можно ли с мопошью него сделать систему распределенной обработки задач. Например есть несколько серверов для непосредственной обработки, которые все настроены на один и тот же источник сообщений, в jms это называется “topic” еще не знаю как это определяется в Reddit. В принципе и сам вижу, что такое возможно реализовать, но можно ли гарантировать равномерное распределение нагрузки на каждый из “рабочих” серверов.

    P.S. Каптча в форме коментов - просто окорбление, при неправильном вводе весь введенный текст улетает в ничто, приходится перенабирать

  4. 26 Апрель 2010 в 11:24 | #4

    @Andrey Kopachevsky
    Андрей, распределение нагрузки в RabbitMQ есть, детали лучше уточнять тут: http://www.rabbitmq.com/faq.html#scale-balance

  5. Andrey Kopachevsky
    26 Апрель 2010 в 11:38 | #5

    Да, уже натнулся на эту ссылку, по моему там говорится о кластеризации самой системы передачи сообщений, в любом случае Rabbit уже нравится, более легкая штука чем JMS

  6. Андрей
    26 Апрель 2010 в 12:14 | #6

    Den, а что исходя из ваших тестов (вы ведь и с тем и с тем уже работали) производительней — Redis или MongoDB?

  7. 26 Апрель 2010 в 12:17 | #7

    @Андрей
    Конечно Redis быстрее, но ведь это не прямой конкурент MongoDB, т.к. они на порядок отличаются по уровню функционала.

  8. 27 Апрель 2010 в 00:25 | #8

    А почему бы не реализовать на каждого пользователя по очереди в RabbitMQ,а брокер бы сам доставлял нужную запись во френдленту каждого юзера?

  9. 27 Апрель 2010 в 16:43 | #9

    @<fb:name linked=”false” useyou=”false” uid=”1130004039″>Frenky Andrey</fb:name>
    Уточните, “почему бы не сделать так” по сравнению с чем :) ? В статье рассматривается только стратегия хранения данных и доступа к ним. Система очередей нужна только для асинхронизации процесса наполнения лент. Ее можно и не использовать.

  10. 27 Апрель 2010 в 21:00 | #10

    @Den Golotyuk

    Я имею ввиду хранить все френдленты не в монго ДБ а в RabbitMQ

    • 27 Апрель 2010 в 21:44 | #11

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

  11. 8 Май 2010 в 11:46 | #12

    знайшов непоганий бенчмарк щодо редіс:
    http://www.ezwebsitemonitoring.com/blog/php-benchmark-memcached-with-pecl-memcache-php-memcached-redis-with-predis-rediska-part-2
    http://github.com/toymachine/libredis/blob/master/test.php

    поки що цікавить чи реалізує libredis весь протокол

  1. Пока что нет уведомлений.