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: Вот ещё статья по теме.
Разумеется нужно отдавать старый контент, но как быть во время __старта__ сайта, когда никакого кэша еще нет и в помине… Лично я для себя решил проблему разбив всю “выдачу” на очень маленькие блоки и кэшируя их в сериализованном виде в memcached. А потом это всё еще обернул кэшированием на уровне файловой системы. Первый пользователь выполняет 100% запросов, второй пользователь выполняет уже только 90% запросов, остальные 10% берет из мемкэша, третий выполняет 80% и т.д., а остальные глядишь уже из файлового кэша будут смотреть.
Из-за того, что запросы реально маленькие – они не создают ощутимой нагрузки на сервер БД, которая могла бы его повесить. Для сбрасывания кэша использую флаги в том же мемкэше (правда флаги и прочую “управляющую” лабуду я храню не на общих мемкэш-серверах, а на отдельном – управляющем).
Разумеется моё решение не всегда катит, иногда для отображения НУ ПРОСТО НЕОБХОДИМО выполнить какой-нибудь запрос, который будет выполняться 1-3 сек. Но если таких запросов реально много, то по-моему стоит посмотреть в сторону реструктуризации БД (скорее всего в ущерб нормализации).
А так вообще спасибо, статья очень интересная.
Ну тут все от случая зависит. Решений море: от выдачи “нет данных”, до асинхронности на ajax’е. Все зависит от требований по актуальности и доступности. Хотя, конечно, вы правы – лучше сделать “гадость” для малой доли пользователей, чем для всего сервера.
Ну хз, если юзеров много, то это плохое решение, имхо, ибо на локах у вас например повиснут все fast-cgi процессы и пойдет 502…
Лучше в этом случае отдавать старый контент имхо.