Чат на PHP: узкое место БД — как решить?

Есть задача организовать простой чат с веб-интерфейсом и полной историей на действующем сайте на самописном движке (PHP5.3.3/MySQL5.1). Гугление по существующим решениям ничего хорошего не дало, либо избыточно, либо производит ощущение «наколенной поделки» и чаще всего давно не поддерживается, да и хотелось бы иметь одну архитектуру и стиль кодирования. В общем принято решение реализовать самостоятельно. С кодированием особых проблем нет, прототип реализовали, но нагрузочное тестирование с разными вариантами индексов и таблиц показало, что при уже ~20 хостах «читателей» и одним «писателем» в секунду MySQL затыкается (VDS c 1Гб RAM, мускулу половина отдана, и 2ГГц проц, nginx+php-frpm под Debian) даже на денормализованной таблице, т. к. кэшированию средствами БД запросы не поддаются (фильтры у каждого «читателя» свои, ибо приват, фильтрация в серверном приложении вряд ли будет эффективней чем в БД, как мне кажется, а у клиента недопустима). А хотелось бы на этом «железе» хотя бы 40-50 держать помимо основной нагрузки. Что может помочь? Опыта «хайлоад» нет, возникли такие идеи:

— написать демона для чата на субдомене, чтобы читал в основном потоке из БД только при старте (последние N сообщений) или редких специфичных запросах, хранил их у себя в памяти процесса (убивая старые), а писал в БД только «логи» для следующего старта (тогда фильтрация будет эффективна, имхо, плюс её можно будет осуществлять опережающе и инкрементно, храня сами сообщения в едином пуле, а для каждого читателя добавлять в список ссылок на «его» сообщения при поступлении сообщения от «писателя» лично для него или публичного, и удалять их оттуда при прочтении)

— аналогичным образом задействовать мемкэш (хотя пока с трудом представляю как обеспечить целостность, до того только с файловыми кэшами работал, которые сами не «испаряются») для обычного PHP-обработчика (то есть чтобы куча воркеров имела доступ к общему пулу сообщений и инкрементным личным спискам ссылок на них между запросами)

— перевести чат на NoSQL СУБД (какую? главная задача эффективная фильтрация по паре полей последних записей, типа WHERE timestamp > {last_time} (или id>{last_id}) AND (recipient_id IS NULL OR recipient_id={user_id}) ORDER BY timestamp (или id) DESC LIMIT {max_records} )

Что стоит попробовать или ещё какие могут быть варианты? Демона писать не хочется, так как усложнит администрирование и сервера, и собственно чата (аналог IRC команд делать?), опыта работы с кэшем и NoSQL практически нет.
  • Вопрос задан
  • 7367 просмотров
Пригласить эксперта
Ответы на вопрос 15
eternals
@eternals
Во-первых, надо уточнить, что за VDS. Если мастерхост, то сразу предупредите, т.к. это отдельная песня.

Во-вторых, нужно понять, из-за чего тупит mySQL. Выше правильно писали про lock на время записи.
Так же может не успевать диск (из-за ограничений VDS).

Я бы попробовал перевести в бд innodb и commit в 0 выставить (сброс раз в секунду на диск).

Далее есть такая штука как представления. И у них есть режим с кэшем в памяти. А SQL запросы уже к нему делать. К тому же их можно наплодить на разные случаи.

Ещё хорошо бы не забыть про memory таблицы. Скажем, писать ещё и в не memory, но читать массово только из неё.

Попутно стоит помнить про индексы. Их отсутствие делает select долгим, а чрезмерное присутствие долгим insert. Да и сами индексы новички обычно неправильно делают.

Ещё бы я вернулся к настройкам VDS, потому что они режут среднюю нагрузку по процу, памяти и дисковым операциям. Вас тупо может это резать. И заодно ещё вспомнил про объём БД в памяти. Если там уже много данных, а кэши не большие, то резать по диску будет.
Ответ написан
@inkvizitor68sl
Linux-сисадмин с 8 летним стажем.
что за вдс то, действительно? Если OpenVZ — то указанные вами ресурсы вы никогда не получите.
Ответ написан
Комментировать
@kolesnevg
Советы от себя
1) попробывать сервер реализовать на node.js (один процесс вроде 800 соединений держит в секунду)
2) тяжелые запросы насколько я понимаю у вас в случае когда в чате 30 человек, один из них пишет, вы это сохраняете в базе данных, потом 29 человек присылают запрос на «обновление» чата… и выполняется 29 выборок из БД, на мой взгляд это не совсем правильно, попробуйте немного по другому: когда один юзер присылает сообщение, вы конечно один раз сохраните его для «истории», но при этом в том же мемкэше создавайте для всех «заинтересованных» юзеров очередь обновлений, не нужно хранить одно сообщение на всех, нужно для каждого хранить полный список того в чем он заинтересован, ну и естественно когда 29 человек обратятся за обновлениями, вы вообще не будете обращаться в БД, а просто возьмете данные из кэша (и почистите его естественно)
Ответ написан
@resurection
Я помню лет 10-15 назад чаты были очень популярными. Кроме чатов и форумов ничего ещё изобретено не было. Ни блогов, ни социалок, ни твиттеров ни, даже, ajax. И тусили в этих чатах тысячи человек (можно было открыть несколько каналов). Или сотка людей в чате без каналов.
Чаты были на фреймах и каждые 5 сек. фрейм со списком сообщений обновлялся ПОЛНОСТЬЮ (весь HTML со всей пачкой сообщений).
Железо в те времена было что-то вроде 300-400 МГц. Ваш ВДС, я уверен, порвал бы те сервера.
Как-то странно сейчас в 2010 году слышать, что чат не выдерживает больше 30 одновременных юзеров.

Вы пробовали на другом сервере? Хотя бы на локале сколько держит?
Ответ написан
AotD
@AotD
PHP, Redis, Postgresql
Можно написать демон. Обычный PHP-демон.
nanoserv или phpDaemon (о последнем информация проскакивала на хабре, но он для настоящих кунг-фу панд :)).
Обычные асинхронные сокет-соединения, зачем вам БД? Зачем вам такие выборки? Вы хотите хранить историю чатов?
Если только публичные-приватные сообщения — элементарно при поступлении сообщения демон либо отдает его конкретному сокету, либо бруткастит всем. Никаких «запросов раз в секунду».
На стороне клиента флешка, держащая этот самый сокет и получающая/отдающая сообщения.
Ответ написан
Vladson
@Vladson
Была как-то задача переписать чат чтоб были минимальные тормоза (сервак был и так перегружен) я воспользовался APC (в БД закидывал только отправленные сообщения «для истории») в принципе могу посоветовать подобный подход…
Ответ написан
Комментировать
slang
@slang
Да тут же так и просится key-value NoSQL сторейдж + polling, чтобы после фикса затыка базы, не наступил затык скриптов на вебсервере.
Ответ написан
Комментировать
AmdY
@AmdY
PHP и прочие вебштучки
пиши демона, алгоритм очень простой:
1. скрипт на который отсылаются сообщений. не демон. сообщение кладёшь в базу и при желании в мемкэш, чтобы был доступен демону. я делал без мемкэша.
2. демон, работает и в цикле перезаписывает файлик post.js в котором хранятся в json виде последних N сообщений. В определённый момент времени можно чистить таблицу и убирать старые посты вовсе или в другую таблицу.
3. с сайта ajax-ок забираешь файлик post.js?r=случайное_число и обновляешь панель сообщений. т.к. забирается js, не поднимается php такую нагрузку выдержит даже apache.
можно вовсе бд не использовать, оставив только memcache или пользуясь шареной памятью php.net/shmop
Если нужно делить посты для пользователей, то генерировать не один файл post.js, а несколько /{user_key}/post.js и давать секретный ключ пользователям.
Ответ написан
standov
@standov
ну если хочется именно БД то я-б думал в сторону промежуточного слоя — т.е. данные из него в БД пишутся асинхронно при определенном заполнении а чтение только из него, из бд в него данные заливаются только в слычае тотального краша, П.С. это мысли не более, тему про «невозможность кеширования запросов» не осилил
Ответ написан
blo
@blo
инженер-программист
хотелось бы узнать среднее кол-во запросов в базу при в момент затыка mysql. может проблема во фронтенде? каким образом подгружаются данные? случайно не периодическим опросом?
Ответ написан
@gonzazoid
как только Вы зададитесь вопросом — какие проблемы решает использование sql — жить станет гораздо легче. проверено.
Конкретно в Вашем случае я бы рекомендовал все таки глянуть в сторону perl демона. Есть готовые наработки, потребуется только напильник.
Ответ написан
Wott
@Wott
Не совсем понятно зачем вообще база. Насколько я понимаю она используется для передачи данных между процессами запросов ( запросы тоже не нужны — используйте websocket ) а для этого либо надо писать отдельный демон, либо использовать сервер с настоящим fastcgi для php или fastcgi на любом другом языке, где данные будут общими. Ну и разруливать их семафорами корректно.

Еще как вариант — мемкеш, но в нем надо делать структуру данных что бы было удобно добавлять сообщения и собирать ответ, зато не надо думать о сборке мусора.

Может быть база нужна для истории… хотя кому она нужна в чатах? :) сделать ее можно в офлайне, кроном.
Для старта сервера — если все правильно сделать то и ее не надо будет, поскольку клиенты имеют текущую ситуацию, надо просто их оповещать об изменениях. А они на старте не нужны.
Ответ написан
@shagguboy
попробуй таблицы INNODB.
1)они умеют держать в памяти не только индексы но и данные.
2) он не блокируются при записи целиком
давай сюда EXPLAIN запросов.
Ответ написан
<?php
# Connect to memcache:
global $memcache;
$memcache = new Memcache;

# Gets key / value pair into memcache… called by mysql_query_cache()
function getCache($key) {
global $memcache;
return ($memcache)? $memcache->get($key): false;
}

# Puts key / value pair into memcache… called by mysql_query_cache()
function setCache($key,$object,$timeout = 60) {
global $memcache;
return ($memcache)? $memcache->set($key,$object,MEMCACHE_COMPRESSED,$timeout): false;
}

# Caching version of mysql_query()
function mysql_query_cache($sql,$linkIdentifier = false,$timeout = 60) {
if (($cache = getCache(md5(«mysql_query». $sql))) !== false) {
$cache = false;
$r = ($linkIdentifier !== false)? mysql_query($sql,$linkIdentifier): mysql_query($sql);
if (is_resource($r) && (($rows = mysql_num_rows($r)) !== 0)) {
for ($i=0;$i<$rows;$i++) {
$fields = mysql_num_fields($r);
$row = mysql_fetch_array($r);
for ($j=0;$j<$fields;$j++) {
if ($i === 0) {
$columns[$j] = mysql_field_name($r,$j);
}
$cache[$i][$columns[$j]] = $row[$j];
}
}
if (!setCache(md5(«mysql_query». $sql),$cache,$timeout)) {
мутим тут запрос к БД и возвращаем ответ
}
}
}
return $cache;
}
?>
Ответ написан
Комментировать
Vasya_Sh
@Vasya_Sh
Записывать надо в memcache, по крону сбрасывать инфу в БД (раз в 5 минут например). Т.е. держать все актуальные данные в мемкэше.
Еще лучше — поставить REDIS, чаты с ним делать одно удовольствие.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы