CDN в системном дизайне: edge, origin shield, Cache-Control и инвалидация
Как устроена CDN: edge, PoP, origin и origin shield, push и pull, заголовки Cache-Control и ETag, версионирование ассетов, динамический контент и оценка трафика на system design интервью.
Коротко
- CDN — это географически распределённый HTTP-кэш. Пользователь получает ответ от ближайшей точки присутствия (PoP), а до вашего сервера (origin) доходит только промах.
- Pull — режим по умолчанию. Edge сам забирает файл с origin при первом запросе. Push нужен, когда контент крупный, заранее известный и его нельзя потерять, например видео.
- Поведением CDN управляет origin через заголовки.
Cache-Control,s-maxage,ETag,Varyрешают, что кэшировать, сколько и для кого. - Статику не инвалидируют, а версионируют. Хеш содержимого в имени файла плюс
max-age=31536000, immutable. Purge оставляют для HTML и аварий. - CDN не спасёт от медленного персонализированного API, записей и промахов по длинному хвосту. На интервью её ставят перед статикой, медиа и публичными страницами, а потом считают трафик и долю попаданий.
Общие стратегии кэша — cache-aside, write-through, stampede, горячие ключи — разобраны в статье о стратегиях кэширования. Здесь — только HTTP-кэш на краю сети.
Зачем вообще нужна CDN
Сервер стоит во Франкфурте, пользователь — в Новосибирске. Даже по идеальному кабелю свет идёт туда и обратно десятки миллисекунд, а реальный маршрут длиннее. Новое HTTPS-соединение требует нескольких таких кругов: TCP-рукопожатие, TLS, сам запрос. Страница с полусотней ресурсов превращается в секунды ожидания, которые не исправить ни быстрым кодом, ни мощным железом.
CDN решает три задачи одновременно:
- Задержка. Соединение заканчивается в городе пользователя, а не на другом континенте.
- Пропускная способность. Терабайты картинок и видео отдаёт сеть провайдера, а не ваши серверы и не ваш исходящий канал.
- Защита и сглаживание пиков. Сеть CDN гораздо шире любого одного дата-центра, поэтому она поглощает всплески и объёмные DDoS-атаки.
Побочные плюсы — терминация TLS ближе к клиенту, поддержка HTTP/2 и HTTP/3 без изменений на origin, сжатие и оптимизация изображений на краю.
Как устроена CDN
Edge, PoP и origin
- Origin — источник истины: ваш веб-сервер, объектное хранилище вроде S3, сервер упаковки видео.
- PoP (point of presence) — площадка CDN в конкретном городе или у конкретного оператора связи. Внутри — десятки или сотни серверов.
- Edge-сервер — сервер в PoP, который принимает соединение пользователя и отдаёт ответ из своего кэша.
Путь запроса выглядит так:
Браузер → ближайший PoP (edge) → [origin shield] → origin hit: ответ сразу промах edge промах shield
Если в кэше edge есть свежая копия — это hit, ответ уходит сразу. Если нет — miss: edge идёт дальше, получает ответ, сохраняет и отдаёт клиенту.
Как пользователь попадает в ближайший PoP
Два основных механизма:
- DNS-маршрутизация. Авторитетный DNS CDN смотрит, откуда пришёл запрос, и возвращает IP ближайшего или наименее загруженного PoP. Минус — он видит резолвер, а не пользователя. Расширение EDNS Client Subnet частично это исправляет.
- Anycast. Один и тот же IP анонсируется по BGP из многих PoP, и сеть сама доставляет пакет в «ближайший» по маршрутизации. Переключение при отказе PoP происходит на уровне BGP, без ожидания TTL в DNS.
Многие крупные CDN сочетают оба подхода. На интервью достаточно назвать оба и сказать, чем они отличаются при отказе. Подробнее о распределении трафика внутри одного дата-центра — в статье о балансировке нагрузки.
Origin shield: ещё один уровень кэша
У CDN могут быть сотни PoP. Если файл популярен везде, каждый PoP при первом промахе пойдёт в origin. Сто PoP — сто одинаковых запросов, а после истечения TTL — ещё сто.
Origin shield — выделенный промежуточный уровень (один PoP или региональный слой), через который проходят все промахи edge. Edge спрашивает shield, и только промах shield доходит до origin. Провайдеры называют это по-разному: shielding, tiered cache, regional edge cache — суть одна.
Небольшая симуляция показывает эффект. Тридцать PoP, 20 000 файлов с популярностью по закону Ципфа (несколько популярных, длинный хвост редких), TTL — час:
import random random.seed(42) N_OBJECTS = 20_000 # разных файлов N_POPS = 30 # точек присутствия N_REQUESTS = 300_000 TTL = 3_600 # секунд DURATION = 3_600 * 6 # шесть часов трафика
Популярность по Ципфу: k-й файл запрашивают пропорционально 1/k
weights = [1 / k for k in range(1, N_OBJECTS + 1)] objects = random.choices(range(N_OBJECTS), weights=weights, k=N_REQUESTS) times = sorted(random.uniform(0, DURATION) for _ in range(N_REQUESTS)) pops = [random.randrange(N_POPS) for _ in range(N_REQUESTS)]
def fresh(cache, key, now): """Есть ли в кэше свежая копия; если нет — кладём новую.""" expires = cache.get(key) if expires is not None and expires > now: return True cache[key] = now + TTL return False
def simulate(with_shield): edges = [dict() for _ in range(N_POPS)] shield = {} edge_hits = origin = 0 for obj, now, pop in zip(objects, times, pops): if fresh(edges[pop], obj, now): edge_hits += 1 elif not with_shield or not fresh(shield, obj, now): origin += 1 return edge_hits / N_REQUESTS, origin
for flag in (False, True): hit, origin = simulate(flag) name = "с shield " if flag else "без shield" print(f"{name}: hit ratio на edge {hit:.1%}, запросов в origin {origin}")
```text
без shield: hit ratio на edge 53.0%, запросов в origin 140922
с shield : hit ratio на edge 53.0%, запросов в origin 48702
Hit ratio на edge не изменился — shield ему и не помогает. Зато нагрузка на origin упала почти втрое: промахи разных PoP по одному файлу сошлись в одном месте. Цена — лишний сетевой переход на промахе и плата за сам shield у некоторых провайдеров. Shield располагают ближе к origin, чтобы этот переход был коротким.
Второй приём того же рода — склеивание запросов (request coalescing, collapsed forwarding). Если в PoP одновременно пришла тысяча запросов за файл, которого нет в кэше, в origin уходит один, остальные ждут его ответа. Это та же защита от stampede, что и single-flight в прикладном кэше, только встроенная в CDN.
Pull и push
| Pull (origin pull) | Push (storage) | |
|---|---|---|
| Как попадает на CDN | Edge забирает с origin при первом промахе | Вы заранее загружаете файлы в хранилище CDN |
| Первый запрос | Медленный: идёт до origin | Быстрый, если файл уже разложен |
| Обновление | Истечение TTL или purge | Перезаливка файла |
| Нужен origin | Да, всегда доступный | Нет, хранилище CDN и есть origin |
| Подходит для | Сайты, API, статика с частыми изменениями | Видео, дистрибутивы, крупные редко меняющиеся файлы |
| Риск | Холодный кэш после деплоя, нагрузка на origin | Нужно самим управлять загрузкой и удалением |
На практике граница размыта: «push» часто означает, что вы кладёте файлы в объектное хранилище, а CDN тянет из него в режиме pull. Для интервью важнее мысль: pull прост и сам подстраивается под популярность, push даёт контроль и предсказуемость для тяжёлого контента.
Ещё один вариант — прогрев кэша: после релиза заранее запросить самые популярные URL через нужные PoP, чтобы первые пользователи не попали на холодный кэш.
Заголовки: как origin управляет CDN
CDN — это общий (shared) HTTP-кэш, и правила его работы описаны в HTTP Caching, RFC 9111. Origin почти всё сообщает заголовками ответа.
Cache-Control
| Директива | Что значит |
|---|---|
max-age=N | Ответ свежий N секунд для любого кэша |
s-maxage=N | То же, но только для общих кэшей (CDN, прокси); перекрывает max-age |
public | Можно кэшировать в общих кэшах, даже если есть Authorization |
private | Только в браузере пользователя, CDN не хранит |
no-cache | Хранить можно, но перед каждой отдачей нужно перепроверить у origin |
no-store | Не сохранять вообще |
immutable | Содержимое по этому URL никогда не изменится, перепроверять не нужно |
stale-while-revalidate=N | N секунд после истечения можно отдавать старое и обновлять в фоне |
stale-if-error=N | Если origin отвечает ошибкой, N секунд можно отдавать старое |
Частая ошибка — путать no-cache и no-store. no-cache не запрещает кэш, а требует перепроверки. Для персональных данных нужен private или no-store.
Разделение max-age и s-maxage позволяет держать HTML в CDN минуту, а в браузере не держать вовсе:
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=30
Браузер каждый раз спрашивает CDN, CDN раз в минуту спрашивает origin, а в окне stale-while-revalidate пользователь не ждёт обновления. Некоторые CDN дополнительно понимают отдельные заголовки только для себя: Surrogate-Control или стандартизованный CDN-Cache-Control (RFC 9213). Поддержку конкретной директивы у своего провайдера стоит проверить в документации.
ETag и условные запросы
Когда копия устарела, не обязательно скачивать файл заново. Origin при первой отдаче ставит валидатор:
HTTP/1.1 200 OK ETag: "a1b2c3" Last-Modified: Tue, 03 Nov 2026 10:00:00 GMT
Кэш перепроверяет копию условным запросом:
GET /catalog.json If-None-Match: "a1b2c3"
Если ничего не изменилось, origin отвечает 304 Not Modified без тела, и кэш продлевает старую копию. Экономится трафик, но не круг до origin. Поэтому ETag — дополнение к max-age, а не замена.
Подвох: если у вас несколько серверов origin и ETag строится из inode или времени изменения файла на конкретной машине, у одинакового файла будут разные ETag. Строй ETag из хеша содержимого.
Ключ кэша и Vary
Кэш хранит ответ под ключом. По умолчанию это метод, хост и URL со строкой запроса. Отсюда две классические проблемы:
- Мусор в query string.
?utm_source=...делает каждый переход из рассылки уникальным ключом, и hit ratio падает. Лишние параметры исключают из ключа в настройках CDN. Vary. ЗаголовокVary: Accept-Encodingговорит, что gzip- и brotli-версии — разные ответы. Это нормально. АVary: User-AgentилиVary: Cookieдробит кэш на тысячи вариантов и фактически его выключает.
Отдельная опасность — cache poisoning и утечки. Если ответ зависит от заголовка, которого нет в ключе, первый запрос с «плохим» значением отравит кэш для всех. Если персональную страницу случайно отдали с public, CDN раздаст чужой профиль. Правило простое: всё, от чего зависит ответ, должно быть в ключе, а всё персональное — private.
Инвалидация и версионирование
Версионирование статики
Лучшая инвалидация — та, которая не нужна. Сборщики фронтенда добавляют хеш содержимого в имя файла:
import hashlib from pathlib import Path def hashed_name(path: Path) -> str: digest = hashlib.sha256(path.read_bytes()).hexdigest()[:10] return f"{path.stem}.{digest}{path.suffix}" css = Path("app.css") css.write_text("body { color: #222 }") print(hashed_name(css)) css.write_text("body { color: #111 }") print(hashed_name(css))
app.2d29e699c0.css app.7ef9a23474.css
Изменился файл — изменился URL. Значит, старую версию можно кэшировать вечно:
Cache-Control: public, max-age=31536000, immutable
А HTML, который ссылается на эти файлы, кэшируют коротко или с перепроверкой. После деплоя новый HTML ссылается на новые имена, и браузеры с CDN подтягивают их как новые объекты. Старые файлы не удаляют сразу: пользователи с открытой вкладкой ещё могут их запросить.
Хеш лучше версии вида ?v=12: он меняется только при реальном изменении содержимого, и неизменившиеся файлы остаются в кэше после релиза.
Purge
Для того, что нельзя версионировать — HTML, картинки по постоянным URL, ответы API, — у CDN есть API очистки:
- По URL или префиксу. Удалить
/products/42или всё под/products/. - По тегам (surrogate keys, cache tags). Origin помечает ответ заголовком со списком тегов, например
product-42 category-7. Изменился товар — очищаем всё, что с ним связано, одним вызовом. Доступность функции зависит от провайдера и тарифа. - Полная очистка. Аварийный вариант: весь трафик разом пойдёт в origin.
Purge не мгновенный: распространение по всем PoP занимает от долей секунды до минут в зависимости от провайдера. Некоторые CDN ограничивают частоту вызовов или берут плату за пути сверх лимита. Поэтому строить консистентность на purge нельзя: TTL всё равно задаёт верхнюю границу устаревания.
Soft purge помечает объект устаревшим вместо удаления. Вместе с stale-while-revalidate это защищает origin от волны промахов сразу после очистки.
Динамический контент
«CDN только для статики» — устаревшее представление. Вот что она даёт для динамики:
- Микрокэширование. Публичная страница каталога или лента новостей с
s-maxage=1–10. При тысяче запросов в секунду на страницу origin видит один запрос в несколько секунд — и пользователи получают данные возрастом в пару секунд. - Ускорение некэшируемых запросов. Даже при
no-storeсоединение с клиентом заканчивается в ближайшем PoP. До origin CDN держит заранее открытые и прогретые соединения по своей магистрали, поэтому TLS-рукопожатие и потери на «последней миле» не повторяются через весь мир. - Edge compute. Небольшой код на краю сети: редиректы, A/B-разметка, проверка JWT, подстановка заголовков, сборка страницы из кэшируемых кусков. Такие платформы есть у большинства крупных CDN. Ограничения — время выполнения, память, доступ к данным: база всё равно далеко.
- Раздельный кэш для анонимов и залогиненных. Анонимы получают кэшированную страницу, а персональные блоки (корзина, имя) подгружаются отдельным запросом с
private.
Для приватных файлов (купленные курсы, документы) используют подписанные URL или подписанные cookie: ссылка содержит срок жизни и подпись, edge проверяет её сам и отдаёт файл из кэша, не обращаясь к сервису авторизации.
Видео и крупные файлы
Видео — главный потребитель CDN. Современный стриминг (HLS, DASH) режет ролик на сегменты по несколько секунд в нескольких битрейтах. Каждый сегмент — обычный статический файл с неизменяемым URL, и кэшируется идеально. Манифест плейлиста для live-трансляции кэшируют на секунды.
Крупные файлы CDN обычно хранит и отдаёт кусками через Range-запросы, чтобы не тянуть весь гигабайт с origin ради первых мегабайт.
Когда CDN не поможет
- Персонализированный API. Ответ для каждого пользователя свой, hit ratio около нуля. Остаётся только выигрыш от терминации соединения. Ускорять нужно сам сервис и его кэш данных.
- Записи.
POSTиPUTпроходят насквозь. Если узкое место — запись в базу, нужны шардирование и репликация или очередь. - Длинный хвост. Миллионы редко запрашиваемых объектов (старые фото в соцсети) вытесняются из edge раньше, чем их запросят повторно. Помогает shield и многоуровневый кэш, но origin всё равно должен держать заметную нагрузку.
- Требование строгой свежести. Остаток на складе или баланс счёта нельзя показывать с задержкой TTL плюс время purge.
- Пользователи рядом с origin. Если все клиенты в одном городе с дата-центром, выигрыш по задержке мал. Остаются разгрузка канала и защита.
- Плохие заголовки.
Set-Cookieв ответе статики,Vary: *, случайные параметры в URL — и CDN честно проксирует всё в origin.
Ещё один риск — сама CDN становится единой точкой отказа. Крупные сбои у провайдеров случались, и на время инцидента «ложились» тысячи сайтов. Крупные компании используют несколько CDN с переключением через DNS — это дорого и требует одинаковой конфигурации у всех провайдеров.
Как оценивать CDN на интервью
Где CDN появляется на схеме
В задачах вроде «спроектируй Instagram, YouTube, новостной сайт» CDN ставят перед объектным хранилищем с медиа и перед публичными страницами. В сокращателе ссылок её можно поставить перед редиректами — с оговоркой, что тогда клики будут теряться для аналитики, и это надо решить отдельно.
Хороший ответ звучит так: «Медиа загружается в объектное хранилище, имена файлов содержат хеш, CDN тянет их в режиме pull с immutable. Перед хранилищем — origin shield. HTML и публичные ленты кэшируем на десятки секунд с stale-while-revalidate, персональное идёт мимо кэша».
Считаем трафик
Пример: 10 млн пользователей в день, каждый открывает 5 страниц, на страницу приходится 1,5 МБ статики и картинок.
- Объём в сутки: .
- Средний поток: .
- Пик при коэффициенте 3: около 21 Гбит/с. Такой канал на собственных серверах — дорогая инфраструктура.
- При hit ratio 95% по байтам origin отдаёт 5%: 3,75 ТБ в сутки, примерно 0,35 Гбит/с в среднем и около 1 Гбит/с в пике. Это уже обычная задача.
Здесь важна оговорка: hit ratio по запросам и по байтам — разные метрики. Мелкие иконки дают много попаданий, а крупные редкие видео — основной трафик в origin. Считать нагрузку на канал нужно по байтам.
Какие вопросы зададут дальше
- «Как обновить картинку, если пользователь сменил аватар?» Новый URL с версией или хешем, старый не трогаем. Purge — только если URL постоянный.
- «Как не отдать чужие данные из кэша?»
privateдля персонального, ключ кэша учитывает всё, от чего зависит ответ, авторизованные ответы по умолчанию не кэшируются. - «Что будет после деплоя с холодным кэшем?» Склеивание запросов, shield, прогрев популярных URL,
stale-while-revalidate. - «Что если CDN упадёт?» Origin должен пережить хотя бы часть трафика или стоять за второй CDN. Как минимум —
stale-if-errorна случай обратной ситуации, когда падает origin. - «Как защитить origin от обхода CDN?» Разрешить входящий трафик только с адресов CDN или проверять секретный заголовок, который добавляет CDN.
Частые ошибки
- Ставить
max-ageна HTML на сутки — после деплоя пользователи сутки видят старую версию со ссылками на удалённые файлы. - Версионировать через
?v=в URL при CDN, которая исключает query string из ключа. - Строить ETag из метаданных файла на конкретной машине.
- Отдавать
Set-Cookieвместе со статикой: многие CDN по умолчанию не кэшируют такие ответы. - Путать
no-cacheсno-storeиmax-ageсs-maxage. - Считать, что purge мгновенный и бесплатный.
- Оценивать выгоду по hit ratio запросов, а не байтов.
Что запомнить
- CDN — общий HTTP-кэш у пользователя: edge в PoP, за ним при необходимости shield, в конце origin.
- Pull — по умолчанию, push — для тяжёлого заранее известного контента.
- Свежесть задаёт
Cache-Control, перепроверку —ETag, варианты — ключ кэша иVary. - Статику версионируют хешем и кэшируют навсегда, HTML — коротко, purge — для исключений.
- Для динамики работают микрокэш,
stale-while-revalidate, ускорение соединений и edge compute, но не для персональных данных и записей. - На интервью называй, что кэшируется и на сколько, как обновляется, и считай трафик в байтах.