ISR-штормы и cold starts: самоDDoS, который вы назначили себе сами
Деплой, сбросивший ISR-кеш, превращает каталог в одну синхронную перегенерацию по вашему же origin. Митигации с механикой: персистентный cacheHandler, джиттер окон, очередь с капом — плюс анатомия cold start и почему p99 взлетает в час деплоя.
Вторник, 08:57: выкатывается рутинный деплой. 09:00:00 ровно: маркетинговое письмо о снижении цен падает в 1,2 миллиона ящиков — точно по расписанию. К 09:00:40 трафик в 40 раз выше ночной впадины, и два отказа, похожие на один, складываются: деплой привёз свежие контейнеры с пустым ISR-кешем, так что каждая страница товара — это MISS, рендерящийся об Postgres; а serverless-флот за ночь отмасштабировался почти в ноль, и всплеск приземляется на холодные инстансы, платящие 4 секунды инициализации до первого байта. p95 пробивает 8 секунд и держится четыре минуты, пока база делает полную перегенерацию каталога, под которую её никто не выделял. Никто никого не атаковал. Обе половины инцидента команда назначила себе сама: деплой стёр кеш, а письмо синхронизировало спрос. Этот урок — анатомия того утра.
Анатомия ISR-шторма
К концу урока вы будете точно знать, какое решение команды породило 8-секундный p95, — и какое одно инфраструктурное изменение не даст ему повториться при следующей рассылке. ISR (Incremental Static Regeneration — инкрементальная статическая регенерация) в установившемся режиме — машина размазывания нагрузки. Каждая запись несёт собственное окно revalidate, отсчитанное от момента генерации; запрос после окна мгновенно получает устаревшую копию и запускает одну фоновую регенерацию. Нагрузка на origin — один рендер на страницу за окно, размазанный трафиком по времени. Каждый шторм — одно и то же событие: что-то ломает размазывание и синхронизирует инвалидации.
Триггер первый: деплой сбрасывает кеш. На своём хостинге Full Route Cache и Data Cache по умолчанию живут в файловой системе в .next/cache внутри контейнера — раскатка заменяет контейнеры, и каждый новый стартует пустым. На managed-платформах новый деплоймент засеян только пререндерами времени сборки; все on-demand-записи, накопленные с прошлого деплоя, исчезают. В обоих случаях длинный хвост, который вы грели днями, испаряется за одну раскатку. Триггер второй: широкая ревалидация. CMS при массовой публикации шлёт вебхук на каждую запись, и обработчик зовёт revalidatePath для каждой; или кто-то «чинит устаревание» вызовом revalidatePath('/', 'layout'), инвалидирующим все маршруты приложения разом. Триггер третий: выровненные окна — после любого синхронного стирания все записи регенерируются в пределах нескольких минут, их окна теперь истекают вместе, и шторм повторяется каждое окно, пока трафик их не рассинхронизирует.
Арифметика и превращает это из «медленно» в инцидент. 30 000 страниц каталога по 4 запроса, перегенерированные за десять минут, — это 120 000 запросов к базе, выделенной под установившийся ручеёк: самоDDoS, где ботнет — ваш собственный путь рендеринга. Хуже: после стирания устаревшей копии нет, подушка stale-while-revalidate исчезла — первые запросы блокируются на рендере вместо старой страницы, поэтому шторм виден как пользовательская задержка, а не только как нагрузка на БД. С репликами всё умножается: три пода с тремя приватными файловыми кешами — это до трёх независимых рендеров на путь плюс мерцание контента, пока балансировщик чередует под, успевший перегенерировать, с подом, ещё отдающим старую копию. Механика ISR — в уроке о стратегиях рендеринга в начале трека; поверхность инвалидации по тегам — в юните про слой данных; этот урок — о том, что бывает, когда обоими управляют в масштабе флота.
Self-hosted Next.js, 3 реплики, файловый кеш по умолчанию, 30k ISR-страниц. Раскатка завершилась в 08:57, всплеск трафика пришёл в 09:00. Что видит база и что видят пользователи?
Митигации, меняющие механизм
Прежде чем тянуться к «добавьте подов», спросите себя: поды с приватными кешами делят нагрузку или умножают рендеры? Каждая настоящая митигация убирает одно из предусловий шторма; ни одна из них не «добавьте подов» (поды с приватными кешами делают хуже).
Вынести кеш из процесса. Кастомный cacheHandler в next.config переносит Full Route Cache и Data Cache в Redis или другое общее хранилище. Умирают сразу два предусловия: кеш переживает деплои (новые контейнеры подключаются к тому же хранилищу, длинный хвост остаётся тёплым) и реплики делят записи (одна регенерация на путь, без мерцания). Каверза корректности, которая кусает команды: если деплой меняет форму страницы, старые записи теперь неверны — включайте build id в ключ или ревалидируйте изменённые разделы на деплое через очередь ниже, осознанно, вместо надежды на случайное полное стирание.
Джиттер окон. revalidate: 3600 на каждой странице означает: одно синхронное стирание выравнивает все истечения навсегда. Выводите окно из пути — 3600 плюс хеш слага, размазанный на ±600 секунд, — и истечения больше не синхронизируются. Самая дешёвая митигация списка: одна функция, ноль инфраструктуры.
Очередь и кап регенерации. Массовые вебхуки не должны звать revalidatePath инлайн, по разу на событие. Складывайте слаги в очередь, дедуплицируйте и дайте воркеру разгребать её с конкурентностью, рассчитанной из свободного запаса базы: если у Postgres есть 500 QPS запаса, а рендер стоит 4 запроса, кап — около 100 одновременных регенераций, и массовая публикация на 5 000 SKU занимает спокойные несколько минут вместо одной горячей.
Отдавать устаревшее, пока штормит. CDN впереди со stale-while-revalidate и stale-if-error — внешний щит: во время шторма пользователи получают страницу ограниченной свежести вместо 8-секундного рендера. Назовите бюджет устаревания вслух: «во время штормов регенерации цены могут отставать до 10 минут» — это продуктовое решение, и его письменная фиксация делает митигацию легитимной.
Cold starts: на что на самом деле уходит инициализация
Cold start — всё между «инстанса не существует» и «ваш обработчик выполняется»: развернуть и распаковать бандл, распарсить и скомпилировать JavaScript — доминирующее, пропорциональное размеру слагаемое; десятки мегабайт зависимостей стоят секунды CPU до всякой логики запроса, — поднять фреймворк (манифесты маршрутов, инструментация), затем набрать соединения: TLS плюс рукопожатие с базой — 50–300 мс на инстанс, оплаченные до первого запроса. Поджарый маршрут укладывает инициализацию в низкие сотни миллисекунд; маршрут, тянущий тяжёлый ORM, AWS SDK и markdown-конвейер, платит 2–5 секунд.
Cold starts невидимы на p50 и владеют вашим p99, и кучкуются они ровно в двух предсказуемых местах. Час деплоя: раскатка заменяет каждый инстанс — весь флот холоден одновременно, и p99 на несколько минут становится гистограммой инициализации. Переход «впадина — всплеск»: ночное масштабирование в ноль означает, что всплеск 09:00 приземляется на ноль тёплых инстансов, а раз всплеск конкурентный, платформа поднимает N инстансов параллельно — N пользователей разом платят полный cold start; это и есть 8-секундный p95 из пролога. Честный список митигаций: меньшие бандлы маршрутов (главный рычаг — мерьте размер на маршрут, lazy-импортируйте тяжёлые пути, держите гигантские SDK вне общих модулей), provisioned concurrency (пол из всегда тёплых инстансов; реально работает и стоит как маленький постоянный флот — оцените против SLO, а не делайте вид, что бесплатно) и региональное размещение рядом с данными (cold start вдали от Postgres доплачивает межрегиональный RTT, а потом его платит и каждый тёплый запрос). Меряйте cold starts как полноправную метрику: долю инвокаций с фазой инициализации плюс гистограмму её длительности — алерт только на p99-задержку расскажет о шторме после пользователей.
Маркетинг настаивает на письме в 09:00 после прошломесячных 8 с p95. Бюджета хватает ровно на одну митигацию до следующей рассылки. Какая бьёт в настоящий механизм всплеска?
- 01Назовите три триггера, синхронизирующих ISR-инвалидации, и митигацию, убирающую каждый.
- 02На что на самом деле уходит cold start и почему p99 взлетает ровно в час деплоя и на всплесках 09:00?
ISR в установившемся режиме — размазывание нагрузки: каждая запись регенерируется раз в окно, в фоне, позади устаревшей копии. Каждый шторм — событие синхронизации, ломающее размазывание. Деплои сбрасывают кеш — файловый .next/cache умирает с контейнером, managed-платформы оставляют только пререндеры сборки, — прогретый длинный хвост испаряется вместе со stale-подушкой: первые запросы блокируются на полных рендерах, а 30k страниц на 4 запроса, сжатые в минуты, — это самоDDoS. Широкие revalidatePath и лавины вебхуков делают то же по запросу, а после любого стирания окна revalidate истекают в ногу, пока что-то их не рассинхронизирует. Реплики с приватными кешами умножают рендеры и заставляют контент мерцать. Митигируйте удалением предусловий: персистентный cacheHandler вне процесса делает кеш переживающим деплои и общим для подов (ключуйте с build id, чтобы деплой, меняющий форму страницы, не отдавал старые структуры); джиттер окон от пути — истечения не выравниваются; массовая ревалидация — через дедуплицирующую очередь с капом конкурентности из свободного запаса БД; впереди — stale-while-revalidate и stale-if-error на CDN с явно названным бюджетом устаревания. Cold start (холодный старт) — вторая половина боли часа деплоя: инициализация тратит парсинг-компиляцию пропорционально размеру бандла, бут фреймворка и 50–300 мс набора соединений — невидимо на p50, владеет p99, кучкуется, когда весь флот холодеет разом (раскатки) или когда переход «впадина — всплеск» форсирует N параллельных инициализаций (письмо в 09:00). Сначала ужимайте бандлы маршрутов, provisioned concurrency (пул всегда тёплых инстансов) покупайте осознанно против SLO, размещайте вычисления рядом с данными и следите за долей cold starts и гистограммами инициализации — чтобы увидеть шторм раньше пользователей. Теперь, когда будете планировать деплой перед маркетинговой рассылкой, проверьте два вопроса: переживает ли кеш раскатку и готовы ли тёплые инстансы к всплеску?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.