Продолжаем warstory оптимизации PHP+mySQL сайта редчайших слов. Одним майским днем, копаясь в словах, мне пришла в голову мысль — расставить под этими словами textbox'ы — использовать [модный для Web 2.0] crowdsourcing. Заполняй чем хочешь, только про это конкретное слово. Но, должно было работать collaborative — как Google Docs — если ты и еще кто-то редактируют слова на одной и той же странице — изменения будут отображаться одновременно. Или, например, кто-то редактирует слово «google» на странице Гугла, а на странице TechMeme, к примеру, есть тоже слово «google» — и им в real-time покажутся эти изменения со страницы Гугла [это необязательно понимать]. Знал бы я какие последствия эта веселая задумка на mySQL окажет позже…
Сделать это оказалось несложно: prototype.js — в руки, ajax — каждые 10 секунд, смотрим в базе что менялось за последние 10 секунд, делаем пересечение со словами на текущей странице, отправляем js highlight effect и новый текст — вуаля. Людей было, но мало кто что-то вводил, тогда я придумал повесить в уголок страницы блок «только что было сказано». Например: «На странице 'fibonacci.com' только что было сказано, что 'fibonacci' → 'crazy math scientist'». Это уже значительно заинтересовало людей и заполняемость выросла в разы. Все видели движения на сайте и сами вовлекались.
Собственно, я сделал достаточно функционала для довольно забавной игрушки — «войны сайтов редкими словами», авто-категоризатор, синонимизатор и т.п. Я сидел мирно редактировал «живой» сайт, загружал новый файл, если видел ошибку — исправлял, не переживая, что это кто-то может видеть. Не подозревал я, что уже тысячи людей видят это. После очередной ошибки в названии функци что-то произошло… я не смог загрузить на FTP index.php… это был TechCrunch....
Один из посетителей сайта написал им статейку, они опубликовали ее («TheRarestWords: Intriguing Semantic SEO Project from Russia») и миллион их подписчиков пошел на сайт [ну дошло до сайта значительно меньше].
Нагрузка на сервер быстро возрасла. Я ничего не подозревая, в этот момент редактировал index.php, и допустим в нем ошибочку. Многие уже сидели на открытых страницах, и работали только ajax'ом. Последствия были уже неотвратимы — пройдена точка невозврата… LoadAvg неуклонно рос. А вкупе с базой данных на 72 млн записей в MySQL это не давало надежд на выживание… Минут через пять-десять-пятнадца мне удалось все же загрузить index.php — тут уже начались 18 часов «оптимизации в боевых условиях».
Первым делом index.php был заменен на:
которая сбила нагрузу и позволила мне исправить это на:
То есть — если loadavg становился больше 50 — сайт просто отключался. Это позволило мне поставить потолок, выше которого сервер не поднимался больше. Сначала он скакнул по-моему почти до сотни, 70 точно я видел.
Далее я начал резво думать какие блоки моего сайта отнимали больше всего времени на генерацию. Я знал что авто-категоризатор занимал добрые полсекунды для каждой страницы, поэтому он тут же был взят в if uptime()>5… else print «блок недоступен сейчас.» — loadavg значительно снизился, но все еще уходил под потолок в 50. Я продолжил назначать другим блокам потолок загрузки при котором они будут отключаться. Это сильно сняло нагрузку, но она продолжала быть под потолком, вызывая отключения сайта — все-таки миллион читателей TechCrunch делали свое дело, учитывая, что многие из них сидели на сайте и заполняли ячейки.
Быстро думаем, что делать дальше. Я вспомнил, что игрался с memcached немного и решил, что надо его применять.
Во-первых, задумавшись, я понял, что верхняя статистика заполненности слов: «Всего у нас в базе 17 миллионов слов, сейчас мы заполнили 12321 слово» отнимала массу времени на расчет, ибо брала таблицу log, в которой велись записи всех определений и делала GROUP BY word, чтобы вычислить количество уникальных слов. При этом для каждого пользователя это происходило раз в 10 секунд. Но я не хотел ее просто кэшировать, я хотел практически реал-тайм статистику любой ценой. Решение пришло очевидное — используем memcached как вторую базу данных. Итак memcached_set('count', реальный_счет_из_mysql(), 0), то есть ставил ключ, реально посчитав слова, и теперь всем отдавался этот самый ключ. А если кто-то вводил слово — я использовал memcached INCREMENT на этом ключе. Это не решало проблемы уникальных слов, поэтому по rand(0,1000)<=1 этот ключ заменялся на реальный счет из mysql. То есть примерно в течение этих 1000 запросов возникала небольшая ошибка (погрешность), но она возвращалась на пути своя реальным счетом [раз в 1000 запросов].
Собственно, это то о чем, говорят, разработчики Google — аггрегируемые данные нужно считать итеративно.
Собственно, в этот момент я понял, что работать с «живым» сайтом дальше нельзя, поэтому в httpd.conf я сделал полную копию блока «VirtualHost для субдомена beta.site.com, куда скопировал все скрпиты и работал уже с ними, чтобы пока я отлаживаю в реальном времени сайт под нагрузкой — народ не отваливался по сто-двести человек из-за того, что я опечатался в названии функции.
Ясное дело, что нужно было продолжать оптимизировать тот кусок, который делается чаще всего — а именно ajax. Окей, memcached как альтернативная база данных мне очень понравился, ибо это гарантировало отсутствие I/O, так что я принялся думать где можно его применить еще. [loadavg все реже доходил до 50, но это все равно было почти невозможно с сайтом работать]
Мое внимание привлек блок: «последнее, что было сказано.» Ведь он должен был находить последние 5 записей в mysql-таблице, а это, понятное дело не шутка, делалось это на 100000 таблице методом ORDER BY time1 DESC… каждые 10 секунд… для каждого из тысяч, кто одновременно сидел на сайте… то есть примерно сто раз в секунду. memcachedируем блок и вуаля… нагрузка спала!
Алилуя! стоп… а где коллаборативное редактирование? Я не вижу как сотни людей редактируют редкие слова на google.com… твою дивизию, ну конечно же… я кэшировал этот блок ajax, а он и вызывал изменение полей. Он кэшировался один раз для какой-то страницы и пытался вставлять эти же изменения на другие, только там таких слов не было.
Итак, мне нужно было сделать что-то, чтобы временно хранило что люди сделали на сайте за последние 10 секунд! Я организовал что-то типа массива в memcached — ключи с названием от 'tmp_cache_0' до 'tmp_cache_20' [20 — подобрал экспериментальным путем] и когда кто-то что-то вводил — проходил по ним всем, ища свободную ячейку, и когда находил ее — ставил этот ключ на 10 секунд в денормализованное значение 'word|page|что ввел человек'. Ведь раз в 10 секунд все находящиеся на сайте пользователи обращались к ajax и в следующие 10 секунд эти значения гарантировано не нужны — истечение ключей в memcached мне играло здесь явно на руку, ибо мне не было необходимости давать еще и DELETE, чтобы не забивать базу. Как, например, пришлось бы в случае с mySQL.
И теперь при каждом ajax вызове мне не нужно было вообще обращаться к mysql, за исключением случаев, когда кто-то что-то вводил новое — проверка 20 ключей memcached, что очень быстро и еще один ключ — обновить real-time stats. Вуаля, нагрузка спала до 10-15! Я был доволен как логотип Postgres'а и пошел спать — к этому моменту я уже 18 часов кодил.
Йои Хаджи,
вид с Хабра
Сделать это оказалось несложно: prototype.js — в руки, ajax — каждые 10 секунд, смотрим в базе что менялось за последние 10 секунд, делаем пересечение со словами на текущей странице, отправляем js highlight effect и новый текст — вуаля. Людей было, но мало кто что-то вводил, тогда я придумал повесить в уголок страницы блок «только что было сказано». Например: «На странице 'fibonacci.com' только что было сказано, что 'fibonacci' → 'crazy math scientist'». Это уже значительно заинтересовало людей и заполняемость выросла в разы. Все видели движения на сайте и сами вовлекались.
Собственно, я сделал достаточно функционала для довольно забавной игрушки — «войны сайтов редкими словами», авто-категоризатор, синонимизатор и т.п. Я сидел мирно редактировал «живой» сайт, загружал новый файл, если видел ошибку — исправлял, не переживая, что это кто-то может видеть. Не подозревал я, что уже тысячи людей видят это. После очередной ошибки в названии функци что-то произошло… я не смог загрузить на FTP index.php… это был TechCrunch....
Один из посетителей сайта написал им статейку, они опубликовали ее («TheRarestWords: Intriguing Semantic SEO Project from Russia») и миллион их подписчиков пошел на сайт [ну дошло до сайта значительно меньше].
Нагрузка на сервер быстро возрасла. Я ничего не подозревая, в этот момент редактировал index.php, и допустим в нем ошибочку. Многие уже сидели на открытых страницах, и работали только ajax'ом. Последствия были уже неотвратимы — пройдена точка невозврата… LoadAvg неуклонно рос. А вкупе с базой данных на 72 млн записей в MySQL это не давало надежд на выживание… Минут через пять-десять-пятнадца мне удалось все же загрузить index.php — тут уже начались 18 часов «оптимизации в боевых условиях».
Временные припарки
Первым делом index.php был заменен на:
print "I, for one, welcome our TechCrunch overlords, but we're kind of overloaded"; exit();
которая сбила нагрузу и позволила мне исправить это на:
if (uptime()>50) { print "I, for one, welcome our TechCrunch overlords :) but we're kind of overloaded"; exit(); }; function uptime() { $fp=@popen('uptime','r'); $s=@fgets($fp); @fclose($fp); @preg_match('#load average: ([0-9\.]+)#', $s, $m); return $m[1]; };
То есть — если loadavg становился больше 50 — сайт просто отключался. Это позволило мне поставить потолок, выше которого сервер не поднимался больше. Сначала он скакнул по-моему почти до сотни, 70 точно я видел.
Далее я начал резво думать какие блоки моего сайта отнимали больше всего времени на генерацию. Я знал что авто-категоризатор занимал добрые полсекунды для каждой страницы, поэтому он тут же был взят в if uptime()>5… else print «блок недоступен сейчас.» — loadavg значительно снизился, но все еще уходил под потолок в 50. Я продолжил назначать другим блокам потолок загрузки при котором они будут отключаться. Это сильно сняло нагрузку, но она продолжала быть под потолком, вызывая отключения сайта — все-таки миллион читателей TechCrunch делали свое дело, учитывая, что многие из них сидели на сайте и заполняли ячейки.
Быстро думаем, что делать дальше. Я вспомнил, что игрался с memcached немного и решил, что надо его применять.
memcached
yum install memcached
Во-первых, задумавшись, я понял, что верхняя статистика заполненности слов: «Всего у нас в базе 17 миллионов слов, сейчас мы заполнили 12321 слово» отнимала массу времени на расчет, ибо брала таблицу log, в которой велись записи всех определений и делала GROUP BY word, чтобы вычислить количество уникальных слов. При этом для каждого пользователя это происходило раз в 10 секунд. Но я не хотел ее просто кэшировать, я хотел практически реал-тайм статистику любой ценой. Решение пришло очевидное — используем memcached как вторую базу данных. Итак memcached_set('count', реальный_счет_из_mysql(), 0), то есть ставил ключ, реально посчитав слова, и теперь всем отдавался этот самый ключ. А если кто-то вводил слово — я использовал memcached INCREMENT на этом ключе. Это не решало проблемы уникальных слов, поэтому по rand(0,1000)<=1 этот ключ заменялся на реальный счет из mysql. То есть примерно в течение этих 1000 запросов возникала небольшая ошибка (погрешность), но она возвращалась на пути своя реальным счетом [раз в 1000 запросов].
Собственно, это то о чем, говорят, разработчики Google — аггрегируемые данные нужно считать итеративно.
Кстати, если у Вас возник вопрос почему я не сделал просто таблицу words, а сделал log, где хранил все исторические определения каждого слова — это из-за троллей. Первое время было жутко много всякого хлама вроде «fsdhjfsdfsd» — регистрации не требуется, пиши что хочешь. Поэтому чтобы откатывать быстро это, мат, рекламу (которой была просто уйма!), мне требовалась такая таблица, а в реальном времени переделывать это на новую таблицу не мог.
Live vs beta
Собственно, в этот момент я понял, что работать с «живым» сайтом дальше нельзя, поэтому в httpd.conf я сделал полную копию блока «VirtualHost для субдомена beta.site.com, куда скопировал все скрпиты и работал уже с ними, чтобы пока я отлаживаю в реальном времени сайт под нагрузкой — народ не отваливался по сто-двести человек из-за того, что я опечатался в названии функции.
Ясное дело, что нужно было продолжать оптимизировать тот кусок, который делается чаще всего — а именно ajax. Окей, memcached как альтернативная база данных мне очень понравился, ибо это гарантировало отсутствие I/O, так что я принялся думать где можно его применить еще. [loadavg все реже доходил до 50, но это все равно было почти невозможно с сайтом работать]
Мое внимание привлек блок: «последнее, что было сказано.» Ведь он должен был находить последние 5 записей в mysql-таблице, а это, понятное дело не шутка, делалось это на 100000 таблице методом ORDER BY time1 DESC… каждые 10 секунд… для каждого из тысяч, кто одновременно сидел на сайте… то есть примерно сто раз в секунду. memcachedируем блок и вуаля… нагрузка спала!
Алилуя! стоп… а где коллаборативное редактирование? Я не вижу как сотни людей редактируют редкие слова на google.com… твою дивизию, ну конечно же… я кэшировал этот блок ajax, а он и вызывал изменение полей. Он кэшировался один раз для какой-то страницы и пытался вставлять эти же изменения на другие, только там таких слов не было.
Массив в memcached
Итак, мне нужно было сделать что-то, чтобы временно хранило что люди сделали на сайте за последние 10 секунд! Я организовал что-то типа массива в memcached — ключи с названием от 'tmp_cache_0' до 'tmp_cache_20' [20 — подобрал экспериментальным путем] и когда кто-то что-то вводил — проходил по ним всем, ища свободную ячейку, и когда находил ее — ставил этот ключ на 10 секунд в денормализованное значение 'word|page|что ввел человек'. Ведь раз в 10 секунд все находящиеся на сайте пользователи обращались к ajax и в следующие 10 секунд эти значения гарантировано не нужны — истечение ключей в memcached мне играло здесь явно на руку, ибо мне не было необходимости давать еще и DELETE, чтобы не забивать базу. Как, например, пришлось бы в случае с mySQL.
И теперь при каждом ajax вызове мне не нужно было вообще обращаться к mysql, за исключением случаев, когда кто-то что-то вводил новое — проверка 20 ключей memcached, что очень быстро и еще один ключ — обновить real-time stats. Вуаля, нагрузка спала до 10-15! Я был доволен как логотип Postgres'а и пошел спать — к этому моменту я уже 18 часов кодил.
Надо оговориться, что я понимал, что memcached — не постоянное хранилище и ключи могут выпадать, но не делал fallback в mySQL, потому что это не банковское приложение и то, что кто-то не увидит одного-двух обновлений — катастрофы не делало. Я в мозгах инженер и если приложение работать надежно в 95% случаев — я не буду тратить еще 50% времени, чтобы доделать остальные 5%. Хотя, в моем случае, я не увидил ни одного потерянного ключа, но кто-то вполне возможно — мог.
Йои Хаджи,
вид с Хабра