Такой сбой часто можно распознать ещё до разговора о новом железе. Трафик обычный. Redis доступен. У горячего ключа заканчивается TTL, или выкладка намеренно удаляет группу ключей. На короткое время многие запросы проходят мимо кеша. Каждый такой промах запускает дорогую работу: тяжёлый SQL-запрос, построение отчёта, рендер с несколькими внешними вызовами. Очередь в базе растёт, приложение греет процессор, и команда решает, что машине просто не хватает мощности.

Потом тот же сценарий повторяется уже на более крупном инстансе, потому что причина была не в размере сервера. Это и есть cache stampede, или наплыв промахов. Один промах почти безвреден, если новое значение пересчитывает один запрос, а остальные либо ждут, либо ещё немного получают прежнюю версию. Когда одновременно приходит пятьдесят промахов, вы оплачиваете пятьдесят одинаковых пересчётов. Дополнительная память для Redis не собирает их в один. Дополнительный CPU лишь даёт каждому чуть больше пространства, но не убирает дублирование. И память, и процессор могут понадобиться позже, но до проверки одного конкретного истечения это всё ещё догадка.

1. Назовите ключ, время жизни и что именно запускает промах

Начинайте не с абстрактного слова «кеш», а с конкретного шаблона ключа. На страницу. На аккаунт. На отчёт. На пользователя. Запишите TTL в секундах и укажите, кто его выставляет. Если много ключей истекают в одну и ту же минуту, вы сами назначили наплыв по расписанию. Если выкладка стирает их вместе, эффект тот же. Ключ без TTL означает другую проблему: Redis растёт, потому что значения не исчезают. Это уже не наплыв, а удержание данных без границы.

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

2. Смотрите на одно истечение, а не на средний график

Средние значения здесь почти ничего не объясняют. Большинство минут проходит на попаданиях в кеш, поэтому p50 может выглядеть здоровым. Настоящий скачок живёт в коротком окне, когда ключ только что исчез. Возьмите один горячий ключ на копии с реалистичным объёмом данных и дайте ему истечь или удалите его один раз. Зафиксируйте три числа: сколько запросов промахнулось, сколько пересчётов реально стартовало и сколько длился один пересчёт.

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

Отдельно стоит записать, исчезло ли старое значение до того, как новое успели положить обратно. Именно этот разрыв создаёт опасное окно. Если система может ещё немного отдавать прежнюю версию, пока новая считается, число настоящих промахов резко падает. Во многих стеках каждое истечение TTL трактуют как жёсткий промах, но это не закон Redis, а выбор реализации.

3. Пусть пересчёт выигрывает один, а замок живёт недолго

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

У замка должен быть собственный TTL. Если воркер упадёт, удерживая замок без срока, наплыв превратится в простой: никто не пересчитает значение, и никто другой не сможет попробовать. Длительность замка должна быть больше нормального времени пересчёта и меньше того промежутка, в течение которого вы готовы отдавать устаревшие данные или возвращать ошибку. Если одна и та же задача приехала дважды, результат всё равно должен записаться один раз. Это та же идемпотентность, которая нужна и в очередях. Если тяжёлая работа уже не помещается в HTTP-запрос, её можно вынести в очередь, как описано в материале когда работу стоит вынести в очередь. Но сама очередь не лечит наплыв: пятьдесят задач на один ключ — это всё тот же наплыв, просто в другом месте.

Рядом с замком почти всегда нужен разброс TTL. Если сто ключей живут одинаково долго, они истекут вместе даже при идеальной блокировке. Добавьте случайный диапазон ко времени жизни, чтобы истечения разошлись по времени. Вторая полезная техника — раннее обновление, когда новое значение готовится ещё до исчезновения старого. Читатели продолжают получать прежнюю версию, а запись выполняет один процесс. Для этого не нужен новый продукт кеширования. Нужны ключ, замок и ясное решение, допускаете ли вы немного устаревшие данные.

4. Когда сервер побольше действительно уместен

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

Но даже тогда сначала запишите причину вытеснения и шаблон ключа. Фраза «Redis забит» без шаблона часто заканчивается тем, что безграничный набор ключей просто переезжает на машину крупнее. Не покупайте сервер, чтобы пережить массовое удаление ключей. Выкладка, которая сносит горячий набор, создаст наплыв на любом размере сервера, пока вы не уберёте это удаление или не сведёте пересчёт к одному исполнителю. И не покупайте сервер вместо чтения запроса, который запускается на промахе. Пятьдесят быстрых запросов — это всё равно пятьдесят запросов, а один медленный, умноженный на пятьдесят, лишь громче повторяет ту же проблему.

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

Когда стоит читать путь промаха вместе

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

Услуги покрывают именно такой разбор: ключ, промах и запрос или задача за ним. Избранные работы показывают время рядом с конкретной операцией, а не как лозунг про масштаб. Если пишете, пришлите шаблон ключа, TTL и результат одного принудительного истечения на копии. Пароли от боевого доступа в первое сообщение не добавляйте.