Это вторая, дополнительная (upd: дополненная), часть моей статьи посвященной кэшированию информации при веб-разработке. Первая имеет название Теория кэша.
UPD: После многочисленных коментариев я сильно переработал статью, внес в неё больше конкретики и примеров, а так же убрал спорные моменты (например, касательно memcached). Спасибо всем, за конструктивную критику.
В данной статье я попытаюсь описать практические стороны кэширования, ориентированные, прежде всего, на сайты и системы управления контентом. Сразу предупреждаю, это мое личное мнение, которое не претендует на истину в последней инстанции. Большинство терминологии — моё, вы можете использовать его, если считаете нужным на своё усмотрение. Конструктивная критика приветствуется.
Итак.
Как ни покажется странным, кэшировать целесообразно далеко не всё. Кэширующие механизмы сами по себе потребляют ресурсы, и если скажем, информация изменяется так же часто, как и выводится, то смысла кэшировать эту информацию никакого нет (например, системы статистики). Так же не стоит кэшировать процессы, которые и без того протекают быстро.
Например: Данные изменяются раз в 1 секунду. Происходит запрос на получение этих данных раз в 2 секунды. Данные кешируются 0,1 секунды, а отдаются из кеша 0,2 сек. Тогда при обращении к данным всегда будет получатся ситуация, когда нам потребуется перестроение данных — которая будет занимать 1 + 0,1 сек. А отдаваться данные из кеша практически не будут. Мы проигрываем эти 0,1 сек из-за кеша.
Первое, что должно быть кэшировано – информация, которая вычисляется крайне долго и ресурсоемко, и используется очень часто. Применимо к сайтам – это результаты выполнения модулей (или приложений), которые используют сложные запросы к базе данных. Кроме этого к ресурсоемким процессам относятся обращения к внешним ресурсам, использующим установку соединения (сокеты, curl и др.), а так же работу с большим объемом сложных данных (парсинг шаблонов, работа с изображениями и др.).
Далее в порядке убывания по скорости доступа к кэшу:
Память. Это memcached, APC, XCache и другие подобные технологии в том же духе. Они (как правило) предоставляют максимальную скорость доступа, но объем сильно ограничен. Память не подходит для больших данных, но хорошо подходит для сравнительно небольших объемов наиболее часто используемых данных, таких как скажем распарсеные шаблоны и др.
Например, у нас используется SAX-парсер. Он медленно парсит шаблоны. Сделаем следущее:
При запросе к шаблону сначала проверяется, находится ли он в памяти. Если да, то вытаскивается, делается unserialize и отдается. Если нет, мы создаем объект (парсим шаблон), сериализуем (serialize) его и помещаем в память (имя обозначим как хэш-код от пути файла). Осталось только решить, на каком основании будем обновлять данные кэша. Это можно сделать 2-мя способами:
1. Мы вообще не будем проверять изменения, а кэш будет существовать некоторый прмежуток времени, скажем, обновляться раз в час. Соответственно, при физическом изменении шаблона они вступят в силу максимум через час.
2. Мы будем контролировать изменения шаблона по времени его последнего сохранения. Для этого нам понадобиться еще одна переменная в памяти — время заненсения данных к кэш (можно назвать как имя переменной шаблона в памяти с префиксом time). Соответственно, мы должны будем её устанавливать равным текущему времени при сохранении объекта шаблона в памяти. Далее, при обращении к шаблону мы сначала сравниваем время изменения файла шаблона (filemtime) со временем закешированного в памяти. И если время изменения шаблона больше времени кэша, то выполняем его обновление. При таком подходе кэш может существовать вечно, если сам шаблон никогда не будет изменятся. Но как только он изменится, перестроится сам кэш.
Файловая система. Наиболее частоиспользуемый способ. Но и тут есть свои подводные камни. Доступ к файлам существенно замедляется, в директории их становится очень много (чем больше файлов, тем меньше скорость), а на некоторых файловых системах вообще существуют ограничения на количество (ext2 – 32768 файлов в директории). За этим надо строго следить. Например, нельзя для кэширования каких-то табличных данных сваливать их в одну директорию и делать названия равными первичным ключам. У вас такая схема просто когда-нибудь переполнится.
Вот так можно это сделать на php:
База данных. Тоже может быть использована для кэша. У базы данных есть сильное преимущество – выборка посредством SELECT. Если данных немного, но они зависят от огромного количество условий, то использование БД вполне оправдано, особенно если грамотно создать индексы в таблице. Напрмер, таблицу в БД можно использовать как хранилище результата выборки сложного запроса с объединением многих больших таблиц с применением большого количества условий. Саму выбоку можно поместить во временную таблицу, и выбирать данные уже из неё. Условия выборки будут тоже сложные, но многочилсенных JOIN уже не будет, что повысит скорость (особенно если используется ENGINE MEMORY применимо к MySQL).
Еще одно достоинство кэширование в БД – это упреждающая подготовка кеша. Скажем, можно первым и единственным запросом вытащить все данные для кэширования на конкретной странице и потом использовать уже их, если это нужно. Кэширование в БД конечно медленное, но грамотная организация может серьезно повысить эффективность использования кэша. И еще – существует SQLite, которая тоже хорошо подходит для этого. Особым эксклюзивом считается создание самой базы SQLite в memcached.
Использование БД для кэша мне представляется редким вариантом. Это больше возможность, нежели практическое применение. Просто, не стоит её упускать из вида.
Для кэширования обычно используются хеширование строки, содержащей все параметры от которых зависит кэш. Если хотя бы какой-нибудь параметр изменился, то и сам хэш-код изменится. Для хранения в файловой системе от хеша «отрубаются» первые несколько символов и создаются соответствующие директории, чтоб не было переполнения файловой системе. Время изменения для файловой системы – это время модификации файла, для БД нужно отдельное поле, для памяти – отдельный параметр (см. пример выше).
Без хеш-кода у вас строка зависимостей может сильно распухнуть, тем более, если их много.
Развернутый пример:
Скажем, в базе 50000 статей. Запрос к базе на получение одной статьи работает долго, что не удивительно с таким объемом. Простой счучай — у нас нет JOIN других талиц.
Делаем следущее — прописываем некоторую зависимость в таблицу зависимостей. Таблица у нас может быть простым массивом, который сериализован и положен в файл. Это нужно для проверки актуальности кэша, что бы решить перестроить закешированные данные в связи с их изменением или можно использовать имеющиеся в кэше. При любом изменении нашей таблице в БД обновляем эту зависимость в таблице, т.е. устанавливаем время равное текущему. Объемы у нас большие, значит лучше использовать кеширование в файловую систему.
Кладем результат выполнения в кэш. При повторном запросе, сравниваем время изменения файла кэша с временем из таблицы зависимостей. Если время файла кэша меньше, перестраиваем — иначе отдаем то что в кэше.
Теперь. Если у нас в одной из 50000 статей мы изменили одну букву, то кэш дропнется для всех, что не является эффективным. Попробуем этого избежать:
Предположим, у нас для каждой статьи есть место где она отображается полностью. Еще есть лента статей, где отображаются краткая информация у всех, которая так же кэшируется вышеописанным способом. Если изменится одна статья, то изменится место, где она отображается полностью, а так же лента (потому что она в ней присутствует). Тогда мы создадим отдельный кэш для каждой из статьи и отдельный кэш для ленты:
Кэш ленты явно зависит от заблицы зависимостей (и неявно зависит от каждой отдельной статьи), т.е. времени последнего изменения таблицы со статьями в БД. Но отдельно взятая статья условно зависит от этого времени, т.е. она зависит от него при условии, что изменилась именно эта статья. Поэтому при показе отдельной статьи мы не будем использовать это время: если есть кэш статьи, то его покажем. Если нет — то построим статью и положим в кэш. Но, важное условие при этом — при редактировании отдельно взятой статьи мы должны удалить её кэш.
В итоге: При редактировании статьи мы обновляем время в таблице зависимостей и уничтожаем кэш этой статьи. При показе ленты решение об её обновлении принимается на основе таблицы зависимостей. При показе отдельной статьи если есть кэш, то он показывается. тем самым, при изменении статьи происходит перестройка кэша списка и этой статьи, но кэш других статей не затрагивается.
При кэшировании в файловую систему нельзя весь кэш пихать в одно место. То есть для каждых отдельных объектов лучше использовать свой директорий, например для страниц /cache/pages, для пользователей /cache/users и так далее. Это нужно, чтоб случайно данные не совпали. Допустим, у вас 2 разных сущности, с одинаковыми id. Так получилось, что надо сохнранить кэш обоих только по этому id. Хэш в обоих случаях будет одинаков, тем самым возникнет конфликт. Но если для каждой сущности будет выделен своё место, этого не будет.
При удалении элемента нужно не забывать удалять и сам кэш. Иначе со временем он распухнет из-за большого количества неактуальных данных. Как вариант – периодическое физическое удаление всего кэша.
Еще существует такая штука как «быстрый» кэш (FastCache, терминология моя). Идея FastCache заключается в том, что бы закэшировать наиболее часто используемые объекты наиболее быстрым образом, отметая все остальное. Например, можно положить полностью созданную главную страницу в память и отдавать её, если ничего не изменилось, незалогиненым пользователям. Основная нагрузка идет именно на главную страницу, поэтому это может сильно разгрузить ресурсы.
Как правильно заметили в одном из комментариев прошлой статьи – кэширование — это часть оптимизации сайта, которое особенно эффективно при высоконагруженных системах. Эффективность работы сайта не состоит из эффективности одного только кэша. Более того, иногда он может быть вообще лишним, поэтому стоит хорошо подумать, прежде чем «прикручивать кэш». Если, например, посещаемость сайта менее 1000 человек в день о кэшировании можно не думать. Хотя конечно, смотря какой сайт.
Спасибо за прочтение!
UPD: После многочисленных коментариев я сильно переработал статью, внес в неё больше конкретики и примеров, а так же убрал спорные моменты (например, касательно memcached). Спасибо всем, за конструктивную критику.
В данной статье я попытаюсь описать практические стороны кэширования, ориентированные, прежде всего, на сайты и системы управления контентом. Сразу предупреждаю, это мое личное мнение, которое не претендует на истину в последней инстанции. Большинство терминологии — моё, вы можете использовать его, если считаете нужным на своё усмотрение. Конструктивная критика приветствуется.
Итак.
Что нужно кэшировать
Как ни покажется странным, кэшировать целесообразно далеко не всё. Кэширующие механизмы сами по себе потребляют ресурсы, и если скажем, информация изменяется так же часто, как и выводится, то смысла кэшировать эту информацию никакого нет (например, системы статистики). Так же не стоит кэшировать процессы, которые и без того протекают быстро.
Например: Данные изменяются раз в 1 секунду. Происходит запрос на получение этих данных раз в 2 секунды. Данные кешируются 0,1 секунды, а отдаются из кеша 0,2 сек. Тогда при обращении к данным всегда будет получатся ситуация, когда нам потребуется перестроение данных — которая будет занимать 1 + 0,1 сек. А отдаваться данные из кеша практически не будут. Мы проигрываем эти 0,1 сек из-за кеша.
Первое, что должно быть кэшировано – информация, которая вычисляется крайне долго и ресурсоемко, и используется очень часто. Применимо к сайтам – это результаты выполнения модулей (или приложений), которые используют сложные запросы к базе данных. Кроме этого к ресурсоемким процессам относятся обращения к внешним ресурсам, использующим установку соединения (сокеты, curl и др.), а так же работу с большим объемом сложных данных (парсинг шаблонов, работа с изображениями и др.).
Куда нужно кэшировать
Далее в порядке убывания по скорости доступа к кэшу:
Память. Это memcached, APC, XCache и другие подобные технологии в том же духе. Они (как правило) предоставляют максимальную скорость доступа, но объем сильно ограничен. Память не подходит для больших данных, но хорошо подходит для сравнительно небольших объемов наиболее часто используемых данных, таких как скажем распарсеные шаблоны и др.
Например, у нас используется SAX-парсер. Он медленно парсит шаблоны. Сделаем следущее:
При запросе к шаблону сначала проверяется, находится ли он в памяти. Если да, то вытаскивается, делается unserialize и отдается. Если нет, мы создаем объект (парсим шаблон), сериализуем (serialize) его и помещаем в память (имя обозначим как хэш-код от пути файла). Осталось только решить, на каком основании будем обновлять данные кэша. Это можно сделать 2-мя способами:
1. Мы вообще не будем проверять изменения, а кэш будет существовать некоторый прмежуток времени, скажем, обновляться раз в час. Соответственно, при физическом изменении шаблона они вступят в силу максимум через час.
2. Мы будем контролировать изменения шаблона по времени его последнего сохранения. Для этого нам понадобиться еще одна переменная в памяти — время заненсения данных к кэш (можно назвать как имя переменной шаблона в памяти с префиксом time). Соответственно, мы должны будем её устанавливать равным текущему времени при сохранении объекта шаблона в памяти. Далее, при обращении к шаблону мы сначала сравниваем время изменения файла шаблона (filemtime) со временем закешированного в памяти. И если время изменения шаблона больше времени кэша, то выполняем его обновление. При таком подходе кэш может существовать вечно, если сам шаблон никогда не будет изменятся. Но как только он изменится, перестроится сам кэш.
Файловая система. Наиболее частоиспользуемый способ. Но и тут есть свои подводные камни. Доступ к файлам существенно замедляется, в директории их становится очень много (чем больше файлов, тем меньше скорость), а на некоторых файловых системах вообще существуют ограничения на количество (ext2 – 32768 файлов в директории). За этим надо строго следить. Например, нельзя для кэширования каких-то табличных данных сваливать их в одну директорию и делать названия равными первичным ключам. У вас такая схема просто когда-нибудь переполнится.
Вот так можно это сделать на php:
<?
function saveCache($name, $data)
{
$hash = sha1($name);
$chunks = str_split($hash, 4);
$cache_dir = CACHE_DIR.'/'.$chunks[0].'/'.$chunks[1];
if (!is_dir($cache_dir)) mkdir($cache_dir, 0775, true);
return file_put_contents($cache_dir.'/'.$hash, serialize($data));
}
?>
База данных. Тоже может быть использована для кэша. У базы данных есть сильное преимущество – выборка посредством SELECT. Если данных немного, но они зависят от огромного количество условий, то использование БД вполне оправдано, особенно если грамотно создать индексы в таблице. Напрмер, таблицу в БД можно использовать как хранилище результата выборки сложного запроса с объединением многих больших таблиц с применением большого количества условий. Саму выбоку можно поместить во временную таблицу, и выбирать данные уже из неё. Условия выборки будут тоже сложные, но многочилсенных JOIN уже не будет, что повысит скорость (особенно если используется ENGINE MEMORY применимо к MySQL).
Еще одно достоинство кэширование в БД – это упреждающая подготовка кеша. Скажем, можно первым и единственным запросом вытащить все данные для кэширования на конкретной странице и потом использовать уже их, если это нужно. Кэширование в БД конечно медленное, но грамотная организация может серьезно повысить эффективность использования кэша. И еще – существует SQLite, которая тоже хорошо подходит для этого. Особым эксклюзивом считается создание самой базы SQLite в memcached.
Использование БД для кэша мне представляется редким вариантом. Это больше возможность, нежели практическое применение. Просто, не стоит её упускать из вида.
Как нужно кэшировать
Для кэширования обычно используются хеширование строки, содержащей все параметры от которых зависит кэш. Если хотя бы какой-нибудь параметр изменился, то и сам хэш-код изменится. Для хранения в файловой системе от хеша «отрубаются» первые несколько символов и создаются соответствующие директории, чтоб не было переполнения файловой системе. Время изменения для файловой системы – это время модификации файла, для БД нужно отдельное поле, для памяти – отдельный параметр (см. пример выше).
Без хеш-кода у вас строка зависимостей может сильно распухнуть, тем более, если их много.
Развернутый пример:
Скажем, в базе 50000 статей. Запрос к базе на получение одной статьи работает долго, что не удивительно с таким объемом. Простой счучай — у нас нет JOIN других талиц.
Делаем следущее — прописываем некоторую зависимость в таблицу зависимостей. Таблица у нас может быть простым массивом, который сериализован и положен в файл. Это нужно для проверки актуальности кэша, что бы решить перестроить закешированные данные в связи с их изменением или можно использовать имеющиеся в кэше. При любом изменении нашей таблице в БД обновляем эту зависимость в таблице, т.е. устанавливаем время равное текущему. Объемы у нас большие, значит лучше использовать кеширование в файловую систему.
Кладем результат выполнения в кэш. При повторном запросе, сравниваем время изменения файла кэша с временем из таблицы зависимостей. Если время файла кэша меньше, перестраиваем — иначе отдаем то что в кэше.
Теперь. Если у нас в одной из 50000 статей мы изменили одну букву, то кэш дропнется для всех, что не является эффективным. Попробуем этого избежать:
Предположим, у нас для каждой статьи есть место где она отображается полностью. Еще есть лента статей, где отображаются краткая информация у всех, которая так же кэшируется вышеописанным способом. Если изменится одна статья, то изменится место, где она отображается полностью, а так же лента (потому что она в ней присутствует). Тогда мы создадим отдельный кэш для каждой из статьи и отдельный кэш для ленты:
Кэш ленты явно зависит от заблицы зависимостей (и неявно зависит от каждой отдельной статьи), т.е. времени последнего изменения таблицы со статьями в БД. Но отдельно взятая статья условно зависит от этого времени, т.е. она зависит от него при условии, что изменилась именно эта статья. Поэтому при показе отдельной статьи мы не будем использовать это время: если есть кэш статьи, то его покажем. Если нет — то построим статью и положим в кэш. Но, важное условие при этом — при редактировании отдельно взятой статьи мы должны удалить её кэш.
В итоге: При редактировании статьи мы обновляем время в таблице зависимостей и уничтожаем кэш этой статьи. При показе ленты решение об её обновлении принимается на основе таблицы зависимостей. При показе отдельной статьи если есть кэш, то он показывается. тем самым, при изменении статьи происходит перестройка кэша списка и этой статьи, но кэш других статей не затрагивается.
На что еще нужно обратить внимание
При кэшировании в файловую систему нельзя весь кэш пихать в одно место. То есть для каждых отдельных объектов лучше использовать свой директорий, например для страниц /cache/pages, для пользователей /cache/users и так далее. Это нужно, чтоб случайно данные не совпали. Допустим, у вас 2 разных сущности, с одинаковыми id. Так получилось, что надо сохнранить кэш обоих только по этому id. Хэш в обоих случаях будет одинаков, тем самым возникнет конфликт. Но если для каждой сущности будет выделен своё место, этого не будет.
При удалении элемента нужно не забывать удалять и сам кэш. Иначе со временем он распухнет из-за большого количества неактуальных данных. Как вариант – периодическое физическое удаление всего кэша.
Еще существует такая штука как «быстрый» кэш (FastCache, терминология моя). Идея FastCache заключается в том, что бы закэшировать наиболее часто используемые объекты наиболее быстрым образом, отметая все остальное. Например, можно положить полностью созданную главную страницу в память и отдавать её, если ничего не изменилось, незалогиненым пользователям. Основная нагрузка идет именно на главную страницу, поэтому это может сильно разгрузить ресурсы.
Заключение
Как правильно заметили в одном из комментариев прошлой статьи – кэширование — это часть оптимизации сайта, которое особенно эффективно при высоконагруженных системах. Эффективность работы сайта не состоит из эффективности одного только кэша. Более того, иногда он может быть вообще лишним, поэтому стоит хорошо подумать, прежде чем «прикручивать кэш». Если, например, посещаемость сайта менее 1000 человек в день о кэшировании можно не думать. Хотя конечно, смотря какой сайт.
Спасибо за прочтение!