Подскажите, пожалуйста, как устроена система личных сообщений между пользователями в крупных проектах (Вконтакте, Одноклассники, Topface т.п) с учетом масштабирования? Интересует именно хранение данных о пользователях/чатах/сообщениях и доступ к этим данным.
Допустим у нас есть 100 000 000 пользователей и необходимо сделать горизонтальное масштабирование (шардинг) этих данных на 200 MySQL серверов. На данный момент, я вижу это следующим образом: разделяем всех пользователей и данные на 200 серверов по user_id, получается примерно 500 000 юзеров на каждый сервер. Можно еще разделить данные на споты по 1000 юзеров и получится, что на каждом сервере БД будет 500 спотов по 1000 юзеров (всего 500 000 юзеров на сервер).
Доступ к серверам БД/спотам можно вычислять по user_id, например spot_id = user_id % 1000. Каждый спот будет хранить данные в виде таблиц, например:
Spot1:
- spot1_users (информация о пользователях)
- spot1_chats (информация о чатах между пользователями)
- spot1_messages (сообщения из чатов)
....
Проблема возникает тогда, когда необходимо хранить/получать общие данные между юзерами. Например, 2 пользователя начинают переписку между собой. В этом случае необходимо создать чат в таблице spotN_chats и поместить туда информацию chat_id (id чата), receiver_id(id получателя), sender_id (id отправителя). Сообщения будут хранится в таблице messages (chat_id, message, time).
Теперь начинается самое интересное - пользователи начинают переписку между собой. Здесь необходимо сделать такие базовые операции:
1) Создание нового чата между 2 пользователями
2) Получение информации о чате или списке чатов конкретного пользователя
3) Создание нового сообщения
4) Получение списка сообщений по chat_id
Также есть 2 варианта развития событий:
1) пользователи находятся на одном споте (например, spot1);
2) пользователи находятся на разных спотах (например, spot1 и spot2);
Задача 1. Пользователь1 решил начать переписку с пользователем2. В этом случае необходимо создать новый чат в БД. Если пользователи на одном споте, то можно просто создать новый чат в таблице spot1_chats, получать chat_id, а дальше создавать новые сообщения в таблице spot1_messages с полученным chat_id. Но если пользователи находятся на разных спотах (spot1, spot2), то такой подход не будет работать, поскольку чтобы каждый пользователь увидел список своих чатов, то их нужно дублировать на 2 споты одновременно. Но в таком случае chat_id будут разными для 2 таблиц(spot1_chats, spot2_chats) если использовать поле autoincrement для chat или же нужно строить какой-нибудь общий для 100 млн. пользователей генератор id для новых чатов. Кроме того, если 2 пользователи на одном споте, то при дублировании чатов все равно будет создан только 1 чат в таблице spot1_users, а вот если мы решим перенести 1 пользователя на другой спот, то как дублировать информацию о чатах?
Задача 2. Пользователь1 отправляет сообщение пользователю2, chat_id у нас уже есть после создания нового чата. Здесь возникает та же самая проблема, что и в первой задаче. Если 2 пользователи на одном споте, то мы просто добавляем новое сообщение в таблицу spot1_messages, но если в будущем захотим перенести пользователя на другой спот, то как дублировать сообщения? Если же пользователи на разных спотах, то для отправки сообщения необходимо создать новое сообщение в таблице spot1_messages и в таблице spot2_messages. Кроме того, если мы хотим обновлять какой-нибудь счетчик новых сообщений или время последнего сообщения в чатах, то нужно будет также обновлять информацию о чатах в таблицах spot1_chats и spot2_chats. Получается для простой отправки одного сообщения необходимо будет сделать несколько запросов в БД, а именно: создать новое сообщения в таблицах spot1_messages и spot2_messages, а также обновить информацию о чате в таблицах spot1_chats и spot2_chats.
Подскажите, пожалуйта, как правильно решить данные задачи или возможно есть какой-то другой более простой/надежный способ хранения сообщений из реального опыта.
Спасибо за информацию. Опыт интересный, но у них слишком кастомное решение + свой движок сообщений. То есть использовать его из коробки скорее всего не получится или займет слишком много времени, чтобы интегрировать в приложение.
Не пытайтесь сделать что то мегаоптимальное сразу, это как выше сказали сложно... но можно подойти к вопросу творчески.
Во первых, храните не сообщения пользователей, а чаты, соответственно и шардинг делайте не по пользователям а по чатам. Тогда сообщения одного пользователя будут размазаны сразу по нескольким серверам но сгруппированы по чатам, как раз чтобы сам чат (с любым количеством пользователей, обычно их количество в чате значительно меньше общего количества пользователей проекта) задействовал только один сервер.
Во вторых, разделяйте оперативную и архивную информацию, для чатов это окно сообщений, видимое пользователям по умолчанию при открытии чата (не все же вы загружаете, всегда это происходит порциями), но не делайте правило переноса между этими группами как аксиому, пусть это будет некий системный процесс, который на основе нагрузки и статистики перемещает данные. Если ограничить пользователей по редактированию и удалению старых сообщений, то возможно использовать разные подходы по хранению оперативной и архивной информации, разные технологии индексации или даже самого хранения, в т.ч. оптимизации по буферизации... грубо говоря хранить readonly архивные данные можно где угодно, ведь для доступа к ним не требуется синхронизация и блокировки.
Не обязательно городить прослойку, дающую информацию по тому где что хранится, например место размещения архивных сообщений чата можно хранить там же где оперативная, ее все равно будут запрашивать , мало того нужно везде стараться делать так, чтобы сразу было ясно, на каком сервере хранятся данные, самый простой способ это хеш от идентификатора (простой остаток от деления на количество серверов более чем подходит).
Оперативные данные будут генирировать самую высокую нагрузку, всеми силами делайте так, чтобы эти данные были как можно дольше в оперативной памяти, т.е. сервера, обрабатывающие их должны быть настроены отлично от архивных.
p.s. массовые рассылки от проекта или партнеров (не кривитесь, все это хотят делать) не делайте обычными чатами, а то будут получаться странные выверты с мегадублированием информации и огромной нагрузкой в момент их проведения, пусть это будет отдельная сущность (остальное разрулите в интерфейсе). С другой стороны, если вам нужна обратная связь, в тот момент, когда пользователь вдруг решает ответить в такой недочат - конвертировать его в обычный.
1. Можно переводить пользователей на гео-усреднённый и менее загруженный спот обработки очередей сообщений (IP-адрес) динамически (по гео-расположению пользователей, на основе их IP).
2. Обслуживать чат-очереди через Redis.
3. Если получатель(-и) сообщения off-line - скидываем сообщение сразу в базу офф-лайн сообщений.