Поговорим о проблемах кэширования

Существует две главные проблемы программирования: именование переменных и инвалидация кэша. Поговорим о второй. Но для начала разберёмся с самим термином — кэширование. Я нашёл много определений, но мне они не очень нравятся. Приведу моё:
Кэширование — от английского cacher (прятать, завёртывать), сохранение данных данных в другом месте или виде. Пункт, который меня смутил в других определениях — повсеместное упоминания ускорения. Но это не всегда так. Мы можем складывать данные и в более медленное хранилище, чтобы разгрузить основное. Тем самым уменьшив нагрузку на основной сторадж. Можем преобразовать данные в вид более удобный для парсинга как то xml или json. Можем разбрасывать данные по серверам, создавая дублирование, но при этом разгружая основное хранилище или перенося данные ближе к пользователю. Тем самым добавляем системе стабильность и удобство.


Теперь о проблемах. Чаще всего мы сталкиваемся с проблемой времени кэширования. В сложных системах несколько уровней кэшей и закэшировано много разных данных и часто время кэширования для них указывается одинаковое или кратное (60m, 15m). Соответственно инвалидация происходит одновременно и в рамках одного запроса генерируется куча новых кэшей и приложение падает. 
Рецепт очень прост — добавлять к времени рандомную дельту, чтобы блоки кэшировались на разные промежутки времени. Приведу пример, но можно сделать удобнее, встроив расчёты в саму функцию кэширования.

$cacheTime = 24 * 60 * 60; // кэшируем данные на целый день
$delta = 60; // дельта отклонение во времени при перегенерации кэша 
cacheSet('Лента новостей', $dataTimeline, ($cacheTime + rand(0, $delta)));
cacheSet('Топ 10 товаров', $dataTop10, ($cacheTime + rand(0, $delta)));
cacheSet('Категори таваров', $dataCategories, ($cacheTime + rand(0, $delta)));

Ещё один кейс — деплоймент нового кода. Зачастую он требует перегенерацию кэшей. Как правило у фреймворков есть консольные команды, которые прогревают кэш, плюс туда можно добавить свои, которые закэшируют основные данные, чтобы приложение не упало при первом же запросе.
Можно найти решения, которые обходят нужные урлы или проходят по карте сайта, вот первый пример из поиска https://gitlab.elias-haeussler.de/eliashaeussler/cache-warmup
Разумеется, это будет работать только если вы деплоите приложение отдельно, а не сразу на продакшене.

Новый кэш генерируется не мгновенно. Можно использовать стратегию двух ключей. Тяжёлые данные кэшируются дважды с разными ключами, второй на большее время. Соответственно при запросе мы проверяем основной ключ , если его нет просим сгенерировать новый кэш, а сами берём данные из бэкапа. Пример статьи статья по теме.

$key = 'ключ для кэша';
$keyBackup = 'ключ для бэкапа';
if (!($data = cacheGet($key))) {
    $data = cashGet($keyBackup); // взяли старые данные
    // асинхронно генерируем новые, чтобы не блокировать отдачу данных пользователю
    cacheSetAsync($key, fn() => generateNewData());
}
return $data;

Генерировать новые данные на лету вообще плохо, а ещё хуже, когда они генерируются многократно при параллельных запросах. Во избежания этого нужно использовать блокировки (пример на файлах), семафоры и т.д. в зависимости от ситуаций, только помните, что бэкэндов может быть несколько и тогда флаги лучше держать в redis или memcached. Лучше при этом использовать стратегию с кэшем бекапом, немного поменяв код выше.

$key = 'ключ для кэша';
$keyBackup = 'ключ для бэкапа';
if (!($data = cacheGet($key))) {
    $data = cashGet($keyBackup); // взяли старые данные
    if (!cacheHasLock($key)) {
        cacheSetLock($key);
        // асинхронно генерируем новые, чтобы не блокировать отдачу данных пользователю
        cacheSetAsync($key, fn() => generateNewData()); 
        // в конце generateNewData освобождаем блокировку cacheReleaseLock($key)
    }
}
return $data;

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

Кэшировать данные можно на разных уровнях кэша, где-то достаточно сохранить данные из базы, где-то модель целиком, где-то готовы html блок, а где-то страницу целиком. Важно не забывать о http кэшировании. Здесь можно почитать как это реализуется в Symfony, а так же какие заголовки посылать. Не стоит и забывать об инстументах для кэширования на уровнях серверов вроде Varnish.

Профилирование. Важно следить за кэшем, его объёмом, количеством попаданий-промахов, временем отклика. Бывает, что кэш лишь ворует ресурсы и даже негативно сказывается на производительности. Кэширование использует сеть, файловую систему, делает сериализацию и десериализацию — всё это не бесплатные операции, особенно на больших объектах.
Но не стоит основываться на искусственных тестах на машине разработчиков. Количество данных может отличаться, эти данные будут попадать в свой кэш файловой системы или базы данных, не будет конкуренции и вымывания, сервер будет находиться под нагрузкой.

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

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

Вам также может понравиться

About the Author: amdy

2 комментария

  1. > Кэширование использует сеть, файловую систему, делает сериализацию и десериализацию — всё это не бесплатные операции, особенно на больших объектах.

    не делаю сериализацию и десериализацию потому что храню Кеш в PHP Arrays файлах

    1. О, да… Хранить кэш в виде php файлов — это отличное решение, про которое вечно забывают.
      Собственно так статья и появилась. У symfony переводы работаю по такому принципу, но у нас запилили хранение в базе в обход хука с генерацией php-кэша симфони. На мой вопрос «какого х*я?» мне рассказали, что не парься, доктрина все кэширует. Пришлось рассказать про то как работает кэш и что это не совсем бесплатно.

      Ну и главное, помимо теории набросали сценарий в JMeter и получились ускорение более чем на 30% для всего ресурса. Я ожидал что будет плохо, но всё оказалось даже хуже.

Добавить комментарий для Sergey Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *