Dog-pile эффект. Как отгонять стаи собак.

Dog-pile эффект — ситуация когда кэш протухает, а большое количество запросов генерирует высокую нагрузку на источник данных, из которых строиться кэш. Представьте, что вы кэшируете результат какого то тяжёлого запроса, например, список популярных статей. В какой-то момент времени кэш протухает, и его кто-то должен построить заново. В общем то пока все хорошо. Кроме случаев когда построение кэша тяжёлая операция, а запросов на него много. Например, запрос для генерации кэша занимает 1 секунду, а пользователи ломятся по 10 штуков в секунду. Соответственно, 9 пользователей (кроме первого) будут только зря нагружать базу. А при большом количестве запросов могут и полностью ее положить.

И пусть весь мир подождёт

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

Честные блокировки на генерацию

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

Лок файловой системы
есть косяки, но иногда работает :) . Подробности в описании функции flock().
Мьютекс в хранилище
Если мы используем memcache и генерируемый кэш имеет ключ «popular_articles», тогда наличие данных с ключем «popular_articles_lock», говорит о том, что наш кэш уже кто-то генерирует. Тоже самое справедливо и для других хранилищ.
IPC семафоры
Настоящие пацанские семафоры ;) Ни разу не использовал — руки не доходят.

Плюсы локов в том, что клиент сам решает, что делать в ситуации, когда ему нужны данные, а их кто-то генерирует. Например, если есть старые данные, то мы можем их отдать, а если данных нет, то либо подождать, либо честно вывести пользователю, что данных нет. На самом деле висящие в течении секунды пользователи совсем не есть гуд, но иногда можно допустить и такое. Так же не стоит забывать, что при генерации может возникнуть ошибка, и в этом случае лок может остаться висеть(в случае memcache можно ставить ttl на лок).

Организация «окна» для генерации

Способ был подсмотрен в исходниках какого-то фреймворка :) Суть его в том, что ttl поддерживается не самим хранилищем, а клиентом, в виде отдельного ключа, и при истечение данные не удаляются. Т.е. приходит первый запрос, видит что ttl истек, и начинает генерировать новое значение. Чтобы остальные запросы подумали, что все нормально, он продлевает ttl существующего кэша на какую-то заранее определённую величину. Если писатель умирает, то кэш быстро снова протухнет и кто-нибудь подхватит знамя генерации с трупа павшего товарища. Если же все нормально, то будет записан новое значение кэша и установлен новый ttl. Основным минусом такого подхода является то, что ttl нельзя хранить средствами самого кэш-storage, т.к. во всех самых известных хранилищах невозможно получить значение ttl и сами данные с истекшим ttl.

Я хочу вас всех, я хочу вас сразу!

Случай у нас тяжёлый, и пользователю будет скучно коротать 20 секунд рисуя матом надписи на пыльном столе. Я знаю про кого будут эти надписи. Главное откровение: чтобы избавиться от последствий многопоточности надо свести ее к одному потоку. Просто и со вкусом. Убираем всю генерацию кэша в оффлайн. ttl-ем в данном случае будет время перезапуска генерирующих кэш скриптов. Помимо быстрого ответа пользователю тут есть еще один плюс — наборы кэшей часто генерировать быстрее, чем каждый из них по отдельности, ибо можно хранить какие-то промежуточные данные. UPD: Вот ещё статья по теме.

Несколько комментариев

  1. Кирилл пишет:

    Разумеется нужно отдавать старый контент, но как быть во время __старта__ сайта, когда никакого кэша еще нет и в помине… Лично я для себя решил проблему разбив всю “выдачу” на очень маленькие блоки и кэшируя их в сериализованном виде в memcached. А потом это всё еще обернул кэшированием на уровне файловой системы. Первый пользователь выполняет 100% запросов, второй пользователь выполняет уже только 90% запросов, остальные 10% берет из мемкэша, третий выполняет 80% и т.д., а остальные глядишь уже из файлового кэша будут смотреть.

    Из-за того, что запросы реально маленькие – они не создают ощутимой нагрузки на сервер БД, которая могла бы его повесить. Для сбрасывания кэша использую флаги в том же мемкэше (правда флаги и прочую “управляющую” лабуду я храню не на общих мемкэш-серверах, а на отдельном – управляющем).

    Разумеется моё решение не всегда катит, иногда для отображения НУ ПРОСТО НЕОБХОДИМО выполнить какой-нибудь запрос, который будет выполняться 1-3 сек. Но если таких запросов реально много, то по-моему стоит посмотреть в сторону реструктуризации БД (скорее всего в ущерб нормализации).

    А так вообще спасибо, статья очень интересная.

  2. Станислав пишет:

    Ну тут все от случая зависит. Решений море: от выдачи “нет данных”, до асинхронности на ajax’е. Все зависит от требований по актуальности и доступности. Хотя, конечно, вы правы – лучше сделать “гадость” для малой доли пользователей, чем для всего сервера.

  3. gabaidulin пишет:

    Ну хз, если юзеров много, то это плохое решение, имхо, ибо на локах у вас например повиснут все fast-cgi процессы и пойдет 502…

    Лучше в этом случае отдавать старый контент имхо.

Оставить комментарий