Xpoint
   [напомнить пароль]

Кэширование динамических страниц сайта (PHP)

Метки: [без меток]
2007-01-19 12:40:33 [обр] ask[досье]

При написании CMS подошел к вопросу кэширования страниц и предлагаю Вам поучаствовать в обсуждении этой проблемы.
Сразу скажу, что используется PHP5.

  1. Есть динамический контент (например, каталог некоторой продукции). Всё это меняется админами в базе непредсказуемым образом.
  2. Есть главная страница, состоящая из разнородных блоков (новости, ссылки на статьи и т.п.), генерируемых разными частями программы.

Возникает вопрос: как это дело кэшировать для снижения нагрузки на сервер при повышении посещаемости?

  1. Первый приходящий на ум способ - кэшировать в базе или в файлах страницы просто по признаку URL + QueryString.

Но как тогда назначать срок жизни этих закэшированных страниц? Ведь он явно должен быть разным (у новостей - короткий, у статей - долгий).

  1. Для каждой записи в БД (например, для товаров в каталоге) записывать время её создания/обновления, чтобы потом выбирать самое свежее время и сверять со временем создания кэша этой страницы. И, в зависимости от различий, выдавать юзеру страницу из кэша или перегенеренную заново страницу.

Но тогда нагрузка на систему получается аналогичная, т.к. нужно всё равно отобрать записи (товары) только для этой страницы, найти у них максимальное время и сравнить с временем кэша, а потом только выдать кэш. То на то и выходит по производительности.
С главной страницей дела обстоят ещё хуже: нужно найти время последнего изменения записей КАЖДОГО блока, потом найти общее время последнего изменения всех блоков. И тогда уже сравнивать с кэшем.

Что посоветуете?

P.S. Встроенное кэширование шаблонизаторов (Smarty и т.п.) не предлагать, т.к.:

  1. От сторонних шаблонизаторов хочу отказаться и использовать свой упрощенный и оптимизированный под мои задачи (Smarty - хорошая вещь, но слишком универсальный и из-за этого довольно тормознутый).
  2. До кэширования шаблонизатора дело всё равно не доходит, т.к. программа сначала получает и обрабатывает данные для страницы, а потом передает их Smarty, который будет судить о необходимости кэширования. Смешно.
  3. Smarty написан на php4, хотя и работает приемлемо на php5. Но не хочу бороться потом с возможными глюками несовместимости.
  4. Скомпилированные шаблоны Smarty представляют собой мусорник из html со вставками <?php ...?>
спустя 8 минут [обр] Сергеев Александр(0/30)[досье]
- пользователь запросил страницу;
- смотрим есть ли она кешированная;
- если нет генерим кеш и выдаем его, если есть — сразу выдаем кеш;
- при обновлении информации в базе убиваем нужный кеш(в зависимости от того чего обновили).
спустя 16 минут [обр] ask[досье]

to Сергеев Александр

Что произойдет в таком случае, если на форуме 2 человека одновременно добавят сообщение?
Как бороться с конфликтами при перегенерации шаблона, если в данный момент этот шаблон уже генерит какой-то пользователь?

спустя 25 минут [обр] Андрей Анатольич+(0/45)[досье]
Но тогда нагрузка на систему получается аналогичная

Зачем тогда кэшировать? ИМХО, кэшировать нужно то, что медленно работает. Допустим, если у вас запрос работает тысячные доли секунды, то необходимости в кэшировании нет, необходимость возникает когда запрос работает десятые доли секунды и более.

Советую также пристально посмотреть в сторону кэширования контента на клиенте — это очень эффективное решение. Читайте статью — Слежение за контентом на динамических сайтах

Так же, опять же ИМХО, лучше кэшировать не куски HTML страниц, а данные (полученные из БД), которые выводятся на страницах, если получение этих данных связано с большими затратами по времени. Конечная страница все равно собирается до неприличия быстро — данные просто оборачиваются в шаблоны и отдаются клиенту. Есть очень хороший набор классов от Дмитрия Котерова, это ((dklab.ru/lib/DbSimple/ DbSimple)) . Кстати, класс поддерживает кэширование.
Если вы используете MySQL, то начиная с версии 4.1 (вроде как, точно не помню, надо бы проверить) у него имеется встроенное кэширование.

Если вы используете какой-либо шаблонизатор или у вас есть свой, то было бы очень неплохо, если бы он умел кэшировать, т.е. транслировать код шаблонизатора в php код. Это тоже увеличит скорость, не нужно от этого отказываться.

И еще: обратите на индексы в ваших таблицах пристальное внимание. Неиспользование индексов очень сильно замедляет выбор данных из БД, и порой скорочть работы сайта можно очень сильно увеличить.

спустя 7 минут [обр] Андрей Анатольич+(0/45)[досье]
ask[досье] Вообще-то, при изменении данных в источнике данных, связанный с этими данными кэш должен очищаться. Тогда проблем не будет.
спустя 13 минут [обр] ask[досье]

Андрей Анатольич[досье]

Вообще-то, при изменении данных в источнике данных, связанный с этими данными кэш должен очищаться. Тогда проблем не будет.

Кэш будет очищаться и генериться заново. Но в этот момент файл с кэшем (или таблица БД) будет залочен для записи, что приведет к ошибке добавления/изменения записей другими пользователями в этот момент.
Как это решить?

спустя 12 минут [обр] Андрей Анатольич+(0/45)[досье]

ask[досье] Такой проблемы не возникнет.
Например, у нас есть тема в форуме, которая кэшируется. Итак, файл с закэшированной темой лежит на диске. Приходит пользователь и добавляет новое сообщение в тему. Происходит следующее:

  • В БД добавляется сообщение пользователя
  • Удаляется файл кэша темы (1)

С момента удаления кэша темы, первый пользователь, который начнет читать тему, вызовет следующие действия:

  • Получит всю тему целиком из БД, а не из кэша (т.к. кэш на этот момент будет очищен). И соответственно, в теме будет содержаться добавленное накануне сообщение.
  • Полученная тема кладется в файл кэша (2).

(1) Если перед тем, как должен удалиться файл с кэшем, кто-то просмотрит тему и тема захочет закэшироваться, а файл с кэшем уже существует, нужно просто перезаписать этот файл (2).

При таком алгоритме ошибок с кэшем быть не должно.

спустя 9 минут [обр] ask[досье]
Тогда резюмирую ход действий, как и было описано выше:
  1. пользователь запросил страницу;
  2. смотрим есть ли она кешированная;
  3. если нет генерим кеш и выдаем его, если есть — сразу выдаем кеш;
  4. при обновлении информации в базе убиваем нужный кеш(в зависимости от того чего обновили).
спустя 9 минут [обр] ask[досье]

Вопрос номер 2, вытекающий из вышеописанного решения.

  1. Имеем каталог товаров.
  2. Пользователь может просматривать товары постранично (например, по 20 товаров на лист).
  3. Может отфильтровывать товары по какому-либо признаку и опять же просматривать результаты постранично.
  4. Всё это кэшируется по признаку url+QueryParams, т.е. кэшируются просмотренные страницы с 1 по N для каждого выбранного фильтра (например, для автомобилей только седан, и/или только с бензиновым двигателем, и/или с ABS, и/или ещё чего-либо). Получается много страниц кэша. Всё это чудесно и реально ускорит работу приложения для последующих юзеров.

Вопрос: какие страницы кэша удалять при изменении параметров некоторых товаров? По какому признаку?

спустя 1 час 21 минуту [обр] ask[досье]
  1. Пример запроса:

/index.php ? topic=T & page=N & filter1=F1 & filter2=F2 & filter3=F3

  1. Кэшируем страницу в файл:

cache_T_N_F1_F2_F3.html

  1. Меняем параметры некоторых товаров в каталоге

Какой файл кэша удалять, если неизвестно в какой из этих кэш-файлов попадут измененные товары?
Удалять все кэш-файлы данного каталога? Но тогда при ежедневном изменении каталога (внесение новых товаров, изменение цен) будет очищаться весь кэш, что уничтожает весь смысл кэширования.
В случае форума ещё хуже.

спустя 12 минут [обр] Sm0ke(0/8)[досье]
ask[досье] Наверно удалять кэш, в фильтр которого попадает новый товар?
Или добавлять новый товар на последнюю страницу в кэшах.
спустя 25 минут [обр] ask[досье]
Наверно удалять кэш, в фильтр которого попадает новый товар?

Для этого нужно выбрать все типы фильтров из кэша, просканировать базу на каждый фильтр, попадают ли измененные товары туда, и просканировать всё это для каждой просмотренной страницы.
На это уйдет не один десяток минут.
Комбинация (количество комбинаций фильтров)*(количество просмотренных страниц при этих фильтрах) даст такое количество вариантов, что ни о каком поиске кэш-файлов, в которых будут содержаться измененные/добавленные товары, речь не может идти.

Или добавлять новый товар на последнюю страницу в кэшах.

Речь идет не только о новых товарах, но и о частом изменении старых, хотя я не совсем понял, что Вы имеете в виду и где в кэшах последняя страница.

Выбирать кэш-страницы для удаления нужно по какому-то другому принципу.
Или очищать кэш для всего каталога при любом изменении/добавлении любого товара, что лишает кэширование смысла.

Ваши мысли, господа?

спустя 1 час 3 минуты [обр] Андрей Гора(0/14)[досье]

Разделите содержимое страницы/сайта на блоки и кешируйте их отдельно. Потом инклудите в основной документ. Это быстрая операция.

Т.е суть в том, что не всегда оправдано кешировать все запросы (т.е. страниицу полностью), запросы могут по нагрузке отличаться в тысячи раз и нужно избавиться от реально тяжелых. Например, при постраничной выдаче информации нет необходимости что-то кешировать, поскольку само по себе сложно найти формулу кеша и кроме того, оно не оправдано: такие запросы легкие, в них нет груповых функций, ORDER BY RAND(), сложных вычислений и выборка идет (должна) по ключу.

А вот на главной странице часто находиться какаято обобщающая информация.
Для определения, что именно грузит, используйте профайлеры, можно простейшие самописные.

Еше, чтобы не влазить в сложности с правилами когда что кешировать, иногда можно пренебречь реал-тайм точностью и поставить задачу на крон. Частота в интервале 1-60 мин для разных блоков.

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

если нет генерим кеш и выдаем его

С этим не соглашусь. Т.е. юзер будет ждать, пока будет сгенерен кеш, т.е. изначально тяжелая на генерацию страница? А зачем для этого ждать юзера?

спустя 47 минут [обр] ask[досье]

Андрей Гора[досье]

Например, при постраничной выдаче информации нет необходимости что-то кешировать, поскольку само по себе сложно найти формулу кеша и кроме того, оно не оправдано: такие запросы легкие, в них нет груповых функций, ORDER BY RAND(), сложных вычислений и выборка идет (должна) по ключу.

В выборке списка по нескольким фильтрам вполне может быть и группировка. И не сказал бы, что такая выборка чуть ли не самый легкий запрос.
Вопросы оптимизации БД и кода программы мы тут не затрагиваем. Тема - кэширование и как его реализовать. А не зачем оно нужно.

Я думаю всё-таки остановиться на комбинированном блочно-страничном кэшировании.
Некоторые страницы имеет смысл кэшировать полностью (статьи, например). А некоторые - блоками (главная, каталог).
Кэш перегенерировать по факту их изменения/создания.

С форумом тоже можно кэшировать полностью первые (PagesCount-1) страниц, где ничего уже меняться не будет. А последнюю, где идет обсуждение - кэшировать поблочно.

спустя 1 час 48 минут [обр] Андрей Анатольич+(0/45)[досье]
ORDER BY RAND()

Такие запросы как раз кэшировать не надо, иначе пропадает смысл этой операции.

ask[досье] Это же не универсально. Вы что, будете писать логику кэширования для каждого модуля в отдельности?

Вообще же кэш должен быть полностью автоматический, т.е. пользователи и разработчики не должны его замечать. Пользователи должны видеть, что если они добавили новое сообщение в тему, то в списке тем в соответствующей теме счетчик бы инкрементировался незамедлительно, сразу после добавления сообщения.
Разработчики же — должны указывать области кэширования, как ob-функции в php:

cache_start();
...
cache_end();

Кэш должен обновляться автоматически.
Это все понятно — остается самое трудное — реализовать эту функциональность :)

ask[досье] Задайтесь вопросом — часто ли посетители используют фильтры, просматривая каталог? Стоит ли вообще кэшировать данные, когда используются фильтры?
Стоит ли вообще в этом случае использовать кэширование? Возможно все-таки лучше принять другие меры: провести оптимизацию запросов, выявить bottleneck's в коде, обновить серверное железо?

И, как я уже говорил — если будете кэшировать, то кэшируйте на более низком уровне, на уровне получения данных.

Кэш перегенерировать по факту их изменения/создания.

Это не так просто, как кажется на первый взгляд. Вам нужно будет знать идентификатор кэша, чтобы это сделать. А вот из MySQL запроса получить идентификатор проще простого, использую хэширование md5(). И кэшировать нужно уже только запрос.

Пользоваться этим можно будет так:

// Получаем данные
$db->cache_start();
$db->setCacheTables('table', 'table2');
$db->query("SELECT * FROM table JOIN table2 ON (...)")
$db->cache_end();


$db->clearCacheForTables('table2'); // Очищаем кэш
$db->execute("INSERT INTO table2 (...) VALUES (...)"); // Обновляем данные

Файл кэша для первого запроса будет выглядеть примерно так:
table@table2-<MD5_HASH>

Способ не ахти, но как проще сделать — не понятно.

Опять же, я уже говорил вам, что MySQL давно научился кэшировать запросы. Так возможно вам просто стоит обновить MySQL и настроить кэширование?

спустя 2 дня 19 часов [обр] ask[досье]

Андрей Анатольич[досье]

Это же не универсально. Вы что, будете писать логику кэширования для каждого модуля в отдельности?

Как раз не универсально использовать общее кэширование, не учитывающее особенностей разных разделов системы. На мой взгляд каждый модуль должен сам решать, каким образом нужно кэшировать результаты его работы и кэшировать ли вообще. Это будет гораздо более гибкое решение, учитывающее все различия в сроках и способах обновления контента. Естесственно, это должно делаться через общий единый интерфейс или наследуемые методы.

Опять же, я уже говорил вам, что MySQL давно научился кэшировать запросы. Так возможно вам просто стоит обновить MySQL и настроить кэширование?

Хочу повторить, что здесь не ищется бутылочное горлышко, не обсуждается конкретная техническая проблема низкой скорости выдачи страниц сайта, причиной которой может быть действительно неправильно построенная БД, старая версия mysql или какой-нибудь старенький Pentium 150MHz на сервере. Поэтому советы по апгрейду hardware/software прошу не предлагать.

Рассматривается система кэширования именно на уровне приложения и возможные варианты её реализации. Свежие идеи крайне приветствуются.

спустя 9 дней [обр] Александр Носов(0/9)[досье]

ask[досье] Если мы рассматриваем какое-то универсальное решение, тогда идею с очисткой кэша после обновления данных IMHO надо оставить. Такое решение в любом случае потребует индивидульного подхода под каждый вид данных. Возможно и удастся написать какую-то универсальную программу-обработчик для этого, но ей все равно потребуются какие-то описательне мета-данные под каждый конкретный случай.

Остается только вариант с периодическим удалением/созданием кэш-файла (для страницы целиком или какой-то отдельной ее части) через определенные интервалы времени. Какие у нас здесь есть варианты:

  1. Мне нравится предложение Андрей Гора[досье] по использованию крона. Недостатком данного метода можно назвать излишняя нагрузка на сервер по созданию кэш-файлов, которые никогда не будут использованы.
  2. Можно генерировать кэш непосредственно в той программе которая отдает контент клиенту. Недостаток этого метода - первый юзер получит страницу "с тормозами", но зато последующие посетители получат ее быстро. Я думаю такая методика вполне годится для страниц типа главной.

 Тут для проверки того, а не устарел ли файл кэша и не пора ли его обноть - можно использовать следующую методику: присваивать дату и время создания файла не тогда, когда он создан, а тогда, когда он устареет touch(...). Т.о. мы сможем задавать различные периоды хранения кэша для разных типов файлов.

  1. Я также рекомендую отслеживать нажатие в браузере клавиш F5 и Ctrl+F5 - в этих случаях все кэши связанные с данной страницей должны быть очищены и заново созданы.

И на последок вопрос который здесь еще не поднимался - очень часто данные выводимые на странице зависят от ролей юзера. Другими словами разные юзеры могут видеть по разному одну и ту-же страницу, если у них разные роли. Советую этот момет тоже учитывать при создании кэш-файла.

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

Powered by POEM™ Engine Copyright © 2002-2005