Есть задача организовать простой чат с веб-интерфейсом и полной историей на действующем сайте на самописном движке (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 практически нет.
Во-первых, надо уточнить, что за VDS. Если мастерхост, то сразу предупредите, т.к. это отдельная песня.
Во-вторых, нужно понять, из-за чего тупит mySQL. Выше правильно писали про lock на время записи.
Так же может не успевать диск (из-за ограничений VDS).
Я бы попробовал перевести в бд innodb и commit в 0 выставить (сброс раз в секунду на диск).
Далее есть такая штука как представления. И у них есть режим с кэшем в памяти. А SQL запросы уже к нему делать. К тому же их можно наплодить на разные случаи.
Ещё хорошо бы не забыть про memory таблицы. Скажем, писать ещё и в не memory, но читать массово только из неё.
Попутно стоит помнить про индексы. Их отсутствие делает select долгим, а чрезмерное присутствие долгим insert. Да и сами индексы новички обычно неправильно делают.
Ещё бы я вернулся к настройкам VDS, потому что они режут среднюю нагрузку по процу, памяти и дисковым операциям. Вас тупо может это резать. И заодно ещё вспомнил про объём БД в памяти. Если там уже много данных, а кэши не большие, то резать по диску будет.
VDS не мастерхостовский, KVM — обещано 1 физ. ядро и 1 Гб ОЗУ, завтра сам сервер погоняю на предмет бенчмарков всяких, но разве что с диском проблемы. Индекс для выборки сделан (вернее все возможные сделаны), судя по EXPLAIN работает (только один из всех). Представления использовать не хочется, не люблю логику в БД :( А вот насчёт memory таблиц — спасибо за идею! Как-то в голову не пришло, что можно писать параллельно в две таблицы :) В принципе получается тот же мемкэш, только без стороннего софта и со знакомой логикой :)
выскажу свое не сильно авторитетное мнение, но KVM еще дико сырой чтобы его использовать под хорошей нагрузкой. (KVM vs Xen)
те тесты что я видел конечно сильно древние, но они показывали, что машина с 10-15 виртуалками на борту может вести себя как угодно, но только не стабильно.
ЗЫ на всякий пожарный уточните про то каким образом у них устроена файловая система на dom0, как организовано дисковое пространство под вашу виртуалку (раздел или файл) — возможно у них уже на основной машине проблемы с дисковым IO
Советы от себя
1) попробывать сервер реализовать на node.js (один процесс вроде 800 соединений держит в секунду)
2) тяжелые запросы насколько я понимаю у вас в случае когда в чате 30 человек, один из них пишет, вы это сохраняете в базе данных, потом 29 человек присылают запрос на «обновление» чата… и выполняется 29 выборок из БД, на мой взгляд это не совсем правильно, попробуйте немного по другому: когда один юзер присылает сообщение, вы конечно один раз сохраните его для «истории», но при этом в том же мемкэше создавайте для всех «заинтересованных» юзеров очередь обновлений, не нужно хранить одно сообщение на всех, нужно для каждого хранить полный список того в чем он заинтересован, ну и естественно когда 29 человек обратятся за обновлениями, вы вообще не будете обращаться в БД, а просто возьмете данные из кэша (и почистите его естественно)
Да, именно так всё. Ну, мой второй вариант похож на то, что вы предлагаете, только лучше, по-моему, хранить в мемкэше общий пул сообщений, а для каждого клиента полный список ссылок на те сообщения, в которых он заинтересован. Нормализация типа :)
а еще лучше, имхо, ввести понятие «канал» (приват считать отдельным каналом).
клиент запрашивает обновления в канале, указывая время или номер последнего имеющегося у него сообщения.
или постит сообщение, и оно добавляется в этот «канал».
ну и хранить там историю/буфер длинной в сколько-то строк.
Я помню лет 10-15 назад чаты были очень популярными. Кроме чатов и форумов ничего ещё изобретено не было. Ни блогов, ни социалок, ни твиттеров ни, даже, ajax. И тусили в этих чатах тысячи человек (можно было открыть несколько каналов). Или сотка людей в чате без каналов.
Чаты были на фреймах и каждые 5 сек. фрейм со списком сообщений обновлялся ПОЛНОСТЬЮ (весь HTML со всей пачкой сообщений).
Железо в те времена было что-то вроде 300-400 МГц. Ваш ВДС, я уверен, порвал бы те сервера.
Как-то странно сейчас в 2010 году слышать, что чат не выдерживает больше 30 одновременных юзеров.
Вы пробовали на другом сервере? Хотя бы на локале сколько держит?
Лет 10 назад я сам такой чат (сотня людей в одном канале + приваты в этом же фрейме)поддерживал, так что представляю, но в том чате не было БД вообще, всё на плоских файлах (включая авторизацию и бан-листы) и никакой истории/поиска, даже для админа (логи пробовали писать, но работать с ними через веб-морду не реально было малой кровью).
Вообще узкое место, вернее даже два, нашёл: блокировка таблицы на запись (перешёл на innodb, случайно оказалось myisam была, не могу даже представить как, но факт — все таблицы в базе innodb, а эта myisam) — стал 50 держать, и свопование из-за другого приложения, уменьшил выделяемую ему память, чтобы не своповалось — загрузить до задержек не смог, больше 70 окон gnome-terminal локально открыть не смог. Пока сместил приоритеты.
Константин: дак это все мода: фреймворки, ООП, современные языки — каждый хочет отхапать себе столько ресурсов, сколько бы хватило всему серверу на cgi (или на чем тогда чаты писались)...
Можно написать демон. Обычный PHP-демон.
nanoserv или phpDaemon (о последнем информация проскакивала на хабре, но он для настоящих кунг-фу панд :)).
Обычные асинхронные сокет-соединения, зачем вам БД? Зачем вам такие выборки? Вы хотите хранить историю чатов?
Если только публичные-приватные сообщения — элементарно при поступлении сообщения демон либо отдает его конкретному сокету, либо бруткастит всем. Никаких «запросов раз в секунду».
На стороне клиента флешка, держащая этот самый сокет и получающая/отдающая сообщения.
Можно пользовать websocket, но он не всеми поддерживается, поэтому либо флешка эмулирующая его (проскакивала информация про javascript библиотеку, которая использует наивный сокет, а при невозможности пользует флеш), либо просто своя флеш реализация которая держит соединение и через которую идет вся информация (javascript регистрирует нужные события и обеспечивает связь отображения с однопиксельной флешкой держащей сокет). На nanoserv со знакомым писали заготовку чата, с моей стороны было не более 150 строк кода. Правда фронтенд был полностью на флеше и простейший текстовый telnet like протокол
Можно подсмотреть как реализована лента событий квонтакте, при каком-либо изменении (друг залил фото...) появляется новое событие без перезагрузки страницы.
Была как-то задача переписать чат чтоб были минимальные тормоза (сервак был и так перегружен) я воспользовался APC (в БД закидывал только отправленные сообщения «для истории») в принципе могу посоветовать подобный подход…
пиши демона, алгоритм очень простой:
1. скрипт на который отсылаются сообщений. не демон. сообщение кладёшь в базу и при желании в мемкэш, чтобы был доступен демону. я делал без мемкэша.
2. демон, работает и в цикле перезаписывает файлик post.js в котором хранятся в json виде последних N сообщений. В определённый момент времени можно чистить таблицу и убирать старые посты вовсе или в другую таблицу.
3. с сайта ajax-ок забираешь файлик post.js?r=случайное_число и обновляешь панель сообщений. т.к. забирается js, не поднимается php такую нагрузку выдержит даже apache.
можно вовсе бд не использовать, оставив только memcache или пользуясь шареной памятью php.net/shmop
Если нужно делить посты для пользователей, то генерировать не один файл post.js, а несколько /{user_key}/post.js и давать секретный ключ пользователям.
Интересный вариант! А почему просто не сделать чтобы скрипт, который пишет в базу формировал и файлик(и)? И зачем случайное число или отключение кэширования заголовками не всегда эффективно?
ну если хочется именно БД то я-б думал в сторону промежуточного слоя — т.е. данные из него в БД пишутся асинхронно при определенном заполнении а чтение только из него, из бд в него данные заливаются только в слычае тотального краша, П.С. это мысли не более, тему про «невозможность кеширования запросов» не осилил
Куча разных (условия в WHERE разные) запросов на чтение идут раз в секунду, раз же в секунду база изменяется, кэшировать результаты у СУБД не получается.
хотелось бы узнать среднее кол-во запросов в базу при в момент затыка mysql. может проблема во фронтенде? каким образом подгружаются данные? случайно не периодическим опросом?
Тестировали даже вообще без фронтенда (если иметь в виду под ним php скрипт), шелловским скриптом SQL запросы напрямую к серверу, запускаемым в параллельных шеллах. Без фронтенда держит около 30, потом не успевает за секунду отвечать, то есть ответ ещё не пришёл, а уже надо посылать новый запрос, чем дольше работает и чем больше клиентов, тем сильнее отставание нарастает. Опросы да, периодические, раз в секунду каждый клиент спрашивает у сервера «дай мне новые публичные и мои приватные сообщения начиная с id такого-то, но не более 50». В тесте вероятность того, что появилось новое сообщение чуть больше 50% (один скрипт каждую секунду шлёт INSERT с публичным сообщением, следующую секунду со случайным получателем). Собственно потому и появилась идея хранить где-то в глобальной памяти пул свежих сообщений и списки ссылок на нужные для каждого клиента сообщения, что заносить один раз сообщение в БД, а потом его оттуда 100 раз читать разными запросами явно не рационально, если БД не может (а она явно не может), при инсерте подготавливать кэш ответов заранее.
1. странно что vds не выдерживает 30 запросов в сек. либо проблема запросами (индексы созданы?), либо проблема в системе (надо тюнить)
2. периодический запрос раз в секунду — не самое оптимальное решение, по сути вы сами себя ддосите. чтобы значительно уменьшить количество запросов советую приглядеться к вебсокетам, подгрузке данных через iframe или long-polling запросам
как только Вы зададитесь вопросом — какие проблемы решает использование sql — жить станет гораздо легче. проверено.
Конкретно в Вашем случае я бы рекомендовал все таки глянуть в сторону perl демона. Есть готовые наработки, потребуется только напильник.
— хранение истории (плюс работа с ней в админке, но до этого ещё не дошёл)
— передача данных между запросами
— основное приложение :)
C perl'ом совсем не знаком, и судя по тем примерам, что видел, мне проще демона на простом Си написать будет с нуля, чем допиливать готовое решение на перле
дело не в языке, но с тем что самому написать легче чем допилить (при определенном багаже опыта) -согласен. Основной мой посыл был — нафига там SQL, этот посыл в общем то ниже подробно развернули.
Для истории и прочего — достаточно инсертить накопившиеся данные раз в пару минут. Для всего остального SQL не нужен.
Не совсем понятно зачем вообще база. Насколько я понимаю она используется для передачи данных между процессами запросов ( запросы тоже не нужны — используйте websocket ) а для этого либо надо писать отдельный демон, либо использовать сервер с настоящим fastcgi для php или fastcgi на любом другом языке, где данные будут общими. Ну и разруливать их семафорами корректно.
Еще как вариант — мемкеш, но в нем надо делать структуру данных что бы было удобно добавлять сообщения и собирать ответ, зато не надо думать о сборке мусора.
Может быть база нужна для истории… хотя кому она нужна в чатах? :) сделать ее можно в офлайне, кроном.
Для старта сервера — если все правильно сделать то и ее не надо будет, поскольку клиенты имеют текущую ситуацию, надо просто их оповещать об изменениях. А они на старте не нужны.
попробуй таблицы 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;
}
?>
Записывать надо в memcache, по крону сбрасывать инфу в БД (раз в 5 минут например). Т.е. держать все актуальные данные в мемкэше.
Еще лучше — поставить REDIS, чаты с ним делать одно удовольствие.