Ctrl K
Урок

CDN и статика

Интуиция: пункт выдачи вместо центрального склада

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

CDN (Content Delivery Network) устроена так же. Это сеть краевых серверов (edge, PoP) по всему миру. На них лежат копии твоих файлов: JS, CSS, картинок, видео. Пользователь получает файл с ближайшего узла, а твой основной сервер (origin) отвечает только тогда, когда на краю копии нет.

Почему статику отдают именно с края

Статика — это файлы, одинаковые для всех: logo.png, app.js, превью видео. Её идеально кэшировать, и пользы от этого три:

  • Задержка. Скорость света не ускоришь. RTT из Москвы до сервера в Вирджинии около 120 мс, до узла CDN в Москве около 10 мс. TCP + TLS — это 2–3 RTT ещё до первого байта. Значит, разница в сотни миллисекунд на каждом соединении.
  • Нагрузка на origin. Большую часть запросов закрывает край, и до тебя доходит малая доля.
  • Трафик и устойчивость. Пиковые всплески (релиз, вирусная картинка) и даже DDoS ложатся на огромную распределённую сеть, а не на твои три сервера.

Схема

HTTPSHTTPSmiss: статикаmiss: статикаAPI-запросыПользователь (Москва)клиентПользователь (Токио)клиентCDN edge: МоскваCDNCDN edge: ТокиоCDNОбъектное хранилище (S3)хранилищеAPI (origin для динамики)сервисPostgreSQLБД
Статика и медиа отдаются с края; на origin уходят только промахи кэша и динамика

Как работает кэш на краю

  1. Пользователь запрашивает cdn.site.com/app.3f9a2c.js. DNS или anycast приводит его к ближайшему узлу.
  2. Cache hit: файл есть и не протух, узел сразу его отдаёт.
  3. Cache miss: узел идёт на origin (например, в S3), сохраняет копию и отдаёт её. Следующие пользователи в этом регионе уже получают hit.

Сколько хранить копию, решает TTL. Обычно его задаёт origin заголовками:

HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: public, max-age=31536000, immutable
ETag: "3f9a2c"

max-age=31536000 означает «храни год», immutable — «даже не проверяй». Для HTML ставят короткий TTL или no-cache, чтобы пользователи быстро видели новую версию страницы.

Инвалидация: версия в имени файла

Главная боль кэша — как обновить файл, который уже разложен по сотне узлов с TTL в год. Есть два пути:

Способ Как работает Минусы
Purge через API CDN «Удали /app.js на всех узлах» Медленно (секунды–минуты), часто платно, браузеры всё равно держат свою копию
Версия в имени app.3f9a2c.js, где хэш берётся из содержимого Нужна сборка, которая переписывает ссылки в HTML

Второй способ — стандарт индустрии. Файл с конкретным хэшем никогда не меняется, поэтому его можно кэшировать навечно. Новый релиз даёт новое имя (app.8b1d07.js), HTML начинает ссылаться на него, и старая копия просто перестаёт запрашиваться. Инвалидировать ничего не нужно: короткий TTL нужен только у HTML.

index.html (TTL 60 с)→app.8b1d07.js (TTL 1 год)→style.a1c4e2.css (TTL 1 год)→null
Короткий TTL нужен только у входной точки, всё остальное версионировано

Совет: на собесе формулируй так: «Статику версионируем хэшем в имени и кэшируем с immutable на год, а HTML отдаём с коротким TTL. Purge оставляем на экстренные случаи».

Посчитаем выгоду

Допустим, 10 млн просмотров страниц в день по 2 МБ статики, hit ratio на CDN 95%.

Величина Расчёт Итог
Трафик в день 10 млн × 2 МБ 20 ТБ
Средняя полоса 20 ТБ / 86 400 с ≈ 230 МБ/с ≈ 1,9 Гбит/с
Пик (×3) 1,9 × 3 ≈ 5,6 Гбит/с
До origin (5% miss) 5,6 × 0,05 ≈ 0,28 Гбит/с

Без CDN тебе пришлось бы держать почти 6 Гбит/с на своих серверах. С CDN хватит одного скромного бакета в S3.

CDN и динамический контент

Даже некэшируемые запросы выигрывают от CDN:

  • TLS terminates на краю. Дорогое рукопожатие идёт с близким узлом, а к origin у CDN уже открыты тёплые keep-alive соединения по быстрой магистрали.
  • Microcaching. Ответы вроде «топ товаров» или «ленты для гостей» можно кэшировать на 1–5 секунд. При 10 000 RPS на такой эндпоинт до origin дойдёт около одного запроса в секунду с каждого узла.
  • Кэш по ключу. Варьирование по Accept-Language или устройству через Vary или настраиваемый cache key.
  • Edge compute (Cloudflare Workers, Lambda@Edge): A/B-редиректы, проверка JWT, геоблок прямо на краю.
  • Защита: WAF, rate limiting и поглощение DDoS до того, как трафик дойдёт до тебя.

Персональные данные (корзина, профиль) помечай Cache-Control: private или no-store, чтобы они не попали в общий кэш.

Компромиссы, которые стоит назвать

  • Устаревание: пока не истёк TTL, пользователь может видеть старую версию. Это лечится версионированием.
  • Стоимость: платишь за исходящий трафик и запросы. На больших объёмах выгоднее мульти-CDN или собственные узлы, как у Netflix Open Connect.
  • Холодный кэш: редкие файлы (длинный хвост) часто дают miss. Помогает origin shield — промежуточный слой кэша, который схлопывает промахи с разных узлов в один запрос к origin.
  • Сложность отладки: «у меня работает, у пользователя в Токио нет» — потому что у них разные узлы.

Как это звучит на собесе

«Картинки и видео кладём в S3, перед ним ставим CDN. Статика версионирована хэшем и кэшируется на год, HTML живёт с TTL в минуту. Ожидаем hit ratio 95%+, так что origin получает около 5% трафика. Для API тоже используем CDN: TLS на краю, microcaching публичных эндпоинтов на пару секунд, WAF. Приватные ответы помечаем private, чтобы не утекли в общий кэш».

Частые ошибки

  • Обновлять файл под тем же именем и надеяться на purge.
  • Длинный TTL у index.html: после релиза пользователи сидят на старой версии.
  • Кэшировать ответ с cookie или персональными данными как public.
  • Забыть про origin shield при большом каталоге медиа: тысячи промахов бьют прямо в хранилище.
  • Говорить «CDN только для картинок» и упускать выгоду для динамики.

Если ты можешь нарисовать схему выше и объяснить, почему app.3f9a2c.js можно кэшировать навечно, тему CDN на собесе ты уже закрываешь уверенно.