CDN, Edge Computing и глобальная доставка
Как доставить контент на другой континент быстрее, чем ваш сервер отвечает на localhost
#23.1 Введение: мир не сидит в вашем дата-центре
В 2012 году Pinterest запустился в Европе. Ну, то есть как запустился — сервера стояли в США, а европейские пользователи просто пытались открывать сайт через Атлантику. Среднее время загрузки страницы в Европе составляло 5 секунд. В Австралии — 7 секунд. Это не потому что код был плохой — это физика: сигнал идёт по оптоволокну со скоростью света, но даже свету нужно ~70ms на круг до Европы и ~150ms до Австралии.
К 2014 году Pinterest развернул CDN (Content Delivery Network) и сократил время загрузки европейских пользователей до 1.5 секунд. Не переписывая ни строчки кода. Не оптимизируя базу данных. Прото расположив контент ближе к пользователю.
Это и есть магия CDN. Не магия, а инженерное решение, которое 20 лет назад было «ништяком для гигантов», а сегодня — обязательный слой архитектуры любого мало-мальски серьёзного проекта.
В этой главе мы разберём, как работают CDN, когда они нужны, а когда — нет, что такое Edge Computing и почему это (возможно) следующий большой шаг, как маршрутизировать трафик по всему миру и что делать, если ваш WebSocket не хочет работать через Akamai.
#23.2 CDN архитектура: как контент оказывается рядом с пользователем
Давайте начнём с простого вопроса: что происходит, когда пользователь из Милана открывает ваш сайт, серверы которого стоят в Айове?
- DNS-запрос: Милан → DNS-сервер (~10ms)
- Установка TCP-соединения: Милан ↔ Айова (~140ms round-trip)
- TLS handshake: ещё ~140ms
- HTTP-запрос: Милан → Айова (~70ms)
- Сервер обрабатывает запрос (~100-300ms)
- Ответ обратно: Айова → Милан (~70ms)
- Браузер парсит HTML, находит CSS/JS/картинки — и повторяет шаги 2-6 для каждого ресурса
Итого: 500ms — 1.5 секунды только на сетевое взаимодействие, до того как браузер вообще начал что-то рендерить. И это если сервер ответил быстро.
CDN решает эту проблему радикально: не тащить контент к пользователю, а заранее размножить контент по всему миру.
#23.2.1 PoP — точки присутствия
CDN — это сеть серверов, разбросанных по всему миру. Каждый такой сервер называется Point of Presence (PoP). Не путайте с дата-центром: PoP — это обычно несколько стоек в colocation-центре крупного провайдера, иногда — просто сервер на пару дисков.
Когда CDN работает хорошо, у вас должно быть ощущение, что сервер стоит в соседней комнате. Cloudflare обещает ответ из ближайшего PoP за 10-30ms для 95% пользователей планеты. У них для этого 300+ городов. Akamai — 4000+ PoP (но это больше про enterprise). Fastly — 50+ PoP, но с упором на скорость обновления кэша.
Архитектура типичного PoP выглядит так:
Пользователь (Милан)
│
▼
┌──────────────────────────────┐
│ PoP (Милан) │
│ │
│ ┌────────────────────┐ │
│ │ Edge Cache │◄────►│ KVS (local cache index)
│ │ (SSD/NVMe) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Load Balancer │ │
│ └────────────────────┘ │
│ │
└──────────────────────────────┘
│
│ (если cache miss)
▼
┌──────────────────────────────────┐
│ Origin Server (AWS, Айова) │
└──────────────────────────────────┘#23.2.2 Origin Pull — как CDN заполняет кэш
Ваш настоящий сервер называется origin (или upstream). PoP не знает ваш контент по умолчанию — он его запрашивает у origin при первом обращении. Это называется origin pull.
Схема работы:
- Первый пользователь из Милана запрашивает
/images/cat.jpg - PoP в Милане проверяет: есть такой файл в кэше? Нет.
- PoP запрашивает файл у origin в Айове
- Origin отдаёт файл, PoP сохраняет его локально (на SSD в стойке)
- PoP отдаёт файл пользователю
- Второй пользователь из Рима запрашивает тот же файл — PoP в Милане (или ближайший к Риму) отдаёт из кэша, origin даже не дёргается
Критическая деталь: все PoP общаются с origin, не между собой. То есть если файл закэширован в Милане, а пользователь из Сиднея делает первый запрос — сиднейский PoP всё равно пойдёт к origin. Это называется pull-based CDN.
Есть альтернатива — push-based CDN: вы сами загружаете контент на все PoP. Удобно для больших файлов (видео, ISO-образы), но не для динамического контента. Используется реже.
#23.2.3 Что кэширует CDN, а что — нет
Вот шпаргалка:
| Тип контента | Кэшируется? | Комментарий |
|---|---|---|
| Статика (CSS, JS, изображения) | Да, агрессивно | TTL — от 1 дня до 1 года |
| HTML страницы | Зависит | Если динамика — нет, если SSG — да |
| API-ответы (JSON/XML) | Осторожно | Только если идемпотентные GET |
| POST/PUT/DELETE | Нет | CDN проксирует к origin |
| WebSocket | Нет (обычно) | Отдельная история, см. секцию 23.7 |
| Streaming-видео | Да | Специальные протоколы (HLS, DASH) |
| Large files (>100MB) | Да, с докачкой | Resume support обязателен |
#23.2.4 Cache hit ratio — главная метрика CDN
Cache hit ratio (CHR) — процент запросов, которые CDN обслужила из кэша, не обращаясь к origin.
- CHR = 95% — отлично
- CHR = 80% — нормально
- CHR = 50% и ниже — вы что-то делаете не так
Формула простая:
origin_load_reduction = 1 - (1 - CHR) × (latency_ratio)
Где latency_ratio = roundtrip_origin / roundtrip_cdnЕсли CHR = 90%, а latency_ratio = 5x (пользователь в 5 раз дальше от origin, чем от PoP), то вы сократили нагрузку на origin в 10 раз. Серверы могут спать спокойно.
Но CHR — штука коварная. Высокий CHR может скрывать проблему: а что, если вы кэшируете слишком агрессивно и пользователи видят устаревшие данные? Тогда CHR = 99% — не повод для гордости, а диагноз.
#23.3 Cache invalidation — две самые сложные проблемы в CS
Фил Карлтон, один из инженеров AWS, как-то сказал: «Есть только две сложные проблемы в computer science: инвалидация кэша, именование и off-by-one ошибки». С инвалидацией кэша он не шутил.
Проблема: вы закэшировали страницу товара на час. Админ поменял цену. Если CDN продолжает отдавать старую страницу — пользователь видит неверную цену. Если не кэшировать вообще — убиваете origin нагрузкой. Компромисс — это и есть искусство управления кэшем.
#23.3.1 TTL — самый простой (и самый тупой) способ
Time-To-Live (TTL) — время жизни кэша. CDN хранит объект и отдаёт его без проверки с origin, пока TTL не истёк.
Cache-Control: public, max-age=3600
└── хранить 1 часМинус очевиден: если данные изменились раньше — пользователи увидят старое.
Решение: выбирайте TTL не как «сколько данные актуальны», а как «сколько времени мы можем показывать устаревшие данные без серьёзных последствий». Для аватарки — 24 часа. Для цены товара — 0 секунд (кэшировать нельзя, см. ниже). Для статьи в блоге — 7 дней.
Есть техника stale-while-revalidate: вы отдаёте старый кэш (stale), но в фоне запрашиваете свежие данные:
Cache-Control: public, max-age=3600, stale-while-revalidate=300Это даёт вам «лучшее из двух миров»: пользователь получает ответ мгновенно (из кэша), а следующий запрос уже получит свежие данные. Особенно хорошо для страниц, которые генерируются долго.
#23.3.2 Cache tags — инвалидация по меткам
TTL — это пассивная инвалидация. Активная — когда вы сами говорите CDN «удалите этот объект из кэша». Самый грубый способ — purge (полная очистка кэша). Но purge всего сайта — это «отбеливание слона» (см. секцию 23.8).
Умный способ — cache tags (Surrogate-Key). Идея:
- Вы маркируете каждый ответ одним или несколькими тегами
- При изменении данных отправляете CDN команду: инвалидировать все объекты с тегом X
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, s-maxage=3600
Surrogate-Key: product_123 category_electronics store_ru
↑ ↑ ↑
тег товара тег категории тег магазинаТеперь, если цена товара 123 изменилась:
PURGE /api/cache?tags=product_123CDN удалит из кэша все ответы, содержащие этот тег. Не всю страницу целиком, не всё подряд — только то, что связано с изменившимся товаром.
Fastly называет это Surrogate-Keys, Cloudflare — Cache-Tags, но идея одна.
#23.3.3 Versioning — путь джедая
Лучшая инвалидация — та, которая не нужна. Если ваш ресурс (CSS, JS, изображение) меняет имя при каждом изменении, старый кэш никогда не вернёт устаревший ответ, потому что старого URL больше не существует.
<link rel="stylesheet" href="/styles/main.a1b2c3.css">
└── хэш содержимогоКогда вы меняете CSS, хэш меняется, URL становится новым, CDN кэширует новую версию параллельно со старой. Старая просто «умирает», когда на неё перестают ссылаться.
Для статики — это золотой стандарт. Для HTML/API — не работает (URL не может меняться при каждом изменении данных).
#23.3.4 Когда кэшировать нельзя, но очень хочется
Есть сценарии, где кэширование HTTP-ответов в принципе не подходит:
- Персонализированные страницы («Ваши заказы»)
- Real-time данные (курсы валют, котировки)
- API с частым изменением данных
Выходы:
-
CDN как proxy + server push (SSE): не кэшируем HTML, но кэшируем тяжёлые компоненты (шапка, подвал, каталог) через Edge Side Includes (ESI) — технология, при которой CDN собирает страницу из кусков, каждый со своим TTL.
-
Private CDN: поднимаете свой кэш в каждом регионе (например, Redis + Nginx) — дорого, но даёт полный контроль.
-
Accept stale data: Иногда лучше показать цену 5-минутной давности, чем не показать ничего. Особенно для read-heavy систем.
#23.4 Edge Computing — простите, CDN, вы больше не про статику
Лет десять назад CDN умела только одно: отдавать закэшированные файлы. Хочешь сделать ресайз картинки на лету? Генерировать HTML на PoP? Забудь — это к origin'у.
Потом появились Cloudflare Workers, Lambda@Edge, Fastly Compute@Edge, и концепция «CDN = кэш» умерла. Сегодня CDN — это ещё и распределённая вычислительная платформа. Ваш код работает в 300+ локациях одновременно, на расстоянии вытянутой руки от пользователя.
#23.4.1 Cloudflare Workers — JavaScript на границе сети
Cloudflare Workers — это V8-изоляты, запускаемые в PoP. Ваш код (JS, WASM) выполняется на каждом запросе, до того как запрос достигает кэша или origin.
// Cloudflare Worker — простой A/B тест на границе сети
addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Определяем пользователя (cookie или geolocation)
const country = event.request.cf?.country || 'unknown';
if (country === 'RU' && url.pathname === '/store') {
// Пользователи из РФ — на локальный store
url.hostname = 'ru.store.example.com';
event.respondWith(fetch(url));
} else if (Math.random() < 0.5) {
// 50% трафика — на новую версию
url.hostname = 'beta.store.example.com';
event.respondWith(fetch(url));
} else {
// Остальные — на стабильную
event.respondWith(fetch(event.request));
}
});Работает это так:
Запрос пользователя
│
▼
┌──────────────────────┐
│ PoP (Милан) │
│ │
│ Worker изоляция │ ← Ваш код стартует здесь
│ ┌──────────────┐ │
│ │ Перехват │ │
│ │ URL, headers │ │
│ │ cookies │ │
│ │ geo, device │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Решение │ │ ← A/B, персонализация,
│ │ (маршрут) │ │ авторизация, редиректы
│ └──────────────┘ │
│ │
└──────────────────────┘
│
▼
Origin или другой PoPКлючевые характеристики Workers (на момент написания книги):
- CPU time: до 30ms на запрос (для бесплатного плана), до 50s — для enterprise (Durable Objects)
- Memory: 128MB
- Subrequests: до 1000 на запрос
- Холодный старт: ~5ms (V8 snapshot, не Node.js)
- Цена: $0.15 за миллион запросов
Холодный старт в 5ms — это убийственная фича. Lambda@Edge стартует от 50ms (Node.js) до 200ms+ (Python) на холодном старте. Для Workers это практически не проблема — V8 snapshot загружается из памяти, не с диска.
#23.4.2 Lambda@Edge — AWS-версия
Lambda@Edge — это AWS Lambda, запущенная на CloudFront (CDN от AWS). Вы регистрируете Lambda-функцию на одно из четырёх событий:
- Viewer Request — запрос от пользователя (можно авторизовать, переписать URL)
- Origin Request — запрос от PoP к origin (можно подменить origin, добавить headers)
- Origin Response — ответ от origin к PoP (можно модифицировать, сжать)
- Viewer Response — ответ от PoP к пользователю (можно добавить заголовки кэширования, подписать cookies)
// Lambda@Edge: Viewer Request — проверка JWT на границе сети
'use strict';
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Защищённые пути
const protectedPaths = ['/account', '/checkout', '/api/user'];
const isProtected = protectedPaths.some(p => request.uri.startsWith(p));
if (!isProtected) {
return callback(null, request);
}
const cookie = headers.cookie?.[0]?.value || '';
const token = extractTokenFromCookie(cookie);
if (!token || !verifyToken(token)) {
const response = {
status: '302',
statusDescription: 'Found',
headers: {
location: [{
key: 'Location',
value: '/login?redirect=' + encodeURIComponent(request.uri),
}],
'cache-control': [{
key: 'Cache-Control',
value: 'no-store',
}],
},
};
return callback(null, response);
}
return callback(null, request);
};
function extractTokenFromCookie(cookie) {
// Разбираем cookie, ищем session_token
return null; // упрощено для примера
}
function verifyToken(token) {
// Проверяем JWT через shared secret
return false; // упрощено
}Разница между Workers и Lambda@Edge — философская:
- Workers — «код на границе сети». Вы пишете обработчик запроса, и он запускается на каждом PoP.
- Lambda@Edge — «Lambda на границе сети». Вы пишете Lambda-функцию, и AWS размещает её на PoP.
Workers быстрее стартуют, проще деплоятся, дешевле. Lambda@Edge даёт доступ к экосистеме AWS (RDS, S3, DynamoDB) и более привычный для многих разработчиков API.
#23.4.3 Когда имеет смысл считать на границе
Edge computing — это не замена вашему бэкенду. Это слой, который берёт на себя задачи, критичные к задержке:
- A/B тестирование на границе: разделение трафика без нагрузки на origin
- Гео-редиректы: перенаправление пользователя на региональный домен
- Authentication/Authorization: проверка JWT на границе, без проброса к origin
- Rate limiting: распределённый лимит запросов (с осторожностью — см. ниже)
- Image optimization: ресайз, webp/avif конвертация, сжатие — на границе, не на origin
- Content personalization: замена частей HTML/JSON в зависимости от гео, device, cookies
- API composition — aggregate: сборка ответа из нескольких origin-сервисов на границе (BFF на границе сети)
Где Edge Computing НЕ подходит:
- Тяжёлые вычисления (долгие CPU-задачи)
- Состояние, которое сложно синхронизировать между PoP
- Операции с высокой consistency (перевод денег)
- Задачи, требующие большого объёма данных в памяти (ML inference — пока нет, хотя уже делают)
#23.5 Geo-routing: как направить пользователя в правильный PoP
Ок, у нас есть 300+ PoP. Новый пользователь из Сан-Паулу открывает сайт. Как наш трафик попадает именно в PoP в Сан-Паулу, а не через Нью-Йорк?
Два основных механизма: DNS-based и Anycast.
#23.5.1 DNS-based routing — умный DNS
Старый, но рабочий способ. DNS-сервер CDN (или ваш Route53) смотрит на IP клиента, определяет его геолокацию и возвращает IP ближайшего PoP.
Запрос: A cdn.example.com
Resolver: сервер DNS клиента (например, Google 8.8.8.8)
Geo-IP: IP клиента → Сан-Паулу
Ответ: A 177.54.32.10 (PoP в Сан-Паулу)Плюсы:
- Простота реализации
- Полный контроль над маршрутизацией
- Можно реализовать на стандартном DNS (Route53, Google Cloud DNS)
Минусы:
- Кэширование DNS: многие клиенты (и публичные DNS-резолверы) кэшируют DNS-ответы на минуты-часы
- DNS-resolver может быть далеко от клиента: Google DNS в Сан-Паулу — ок, а если клиент использует корпоративный DNS в Хьюстоне?
- TTL играет против вас: короткий TTL (30 секунд) даёт гибкость, но нагружает DNS-инфраструктуру
#23.5.2 Anycast — один IP на всех
Anycast — это когда один и тот же IP-адрес объявляется из разных локаций. Маршрутизаторы BGP сами направляют клиента к ближайшей локации.
IP: 177.54.32.10
Объявляется из:
├── PoP Сан-Паулу (AS12345)
├── PoP Нью-Йорк (AS12345)
├── PoP Лондон (AS12345)
└── PoP Сингапур (AS12345)
Клиент в Сан-Паулу → BGP shortest path → PoP Сан-ПаулуБольшинство крупных CDN (Cloudflare, Fastly, Akamai) используют Anycast. Почему?
Плюсы Anycast:
- Клиенту не нужно делать DNS-запрос для каждого подключения — один IP на всё
- Автоматическая балансировка при отказе PoP: если PoP в Сан-Паулу упал, трафик автоматически пойдёт в Нью-Йорк (следующий shortest path по BGP)
- Никаких проблем с DNS-кэшированием
Минусы Anycast:
- BGP — это не ваша логика: вы не можете тонко управлять, какой трафик куда идёт. Только shortest AS path.
- Anycast плохо работает с протоколами, где сохраняется состояние (TCP connection pooling, WebSocket)
- Проблемы с BGP hijacking (кто-то объявляет ваш IP — и трафик уходит не туда)
#23.5.3 DNS + Anycast — комбинация
На практике большинство CDN используют гибрид:
- DNS возвращает IP, который объявлен через Anycast
- Клиент запрашивает
cdn.example.com, получает177.54.32.10 177.54.32.10— это Anycast IP, который ведёт в ближайший PoP
Это даёт и гибкость DNS (можно делать geo-based ответы для разных клиентов), и отказоустойчивость Anycast.
#23.5.4 Что важно понимать про geo-routing
Точность геолокации — это не GPS. Geo-IP базы (MaxMind, IP2Location) имеют точность:
- Страна: 99%
- Регион: 70-90%
- Город: 50-80%
- Координаты: ±50km
Если вам нужно точно определять регион пользователя для контентных ограничений (GDPR, цензура), опираться только на Geo-IP — опасно. Используйте как хинт, не как истину.
#23.6 Edge Use Cases — что реально делают на границе сети
Теория — хорошо, давайте посмотрим на реальные сценарии.
#23.6.1 Image optimization на границе
Это, пожалуй, самый популярный use case. Пользователь загружает фото 4000×3000, а на сайте оно нужно как превью 200×150. Вместо того чтобы ресайзить на origin (занимая CPU), вы делаете это на границе:
User upload → S3/Storage
│
▼
CDN + Image Optimization
├── device detection → формат (AVIF для iPhone, WebP для Chrome, PNG legacy)
├── viewport detection → размер (ширина экрана)
└── quality → 85% по умолчанию, adjustable
│
▼
ПользовательCloudflare Image Resizing, Imgix, Vercel - все делают это стандартно. Экономия трафика: до 80% без потери качества для пользователя.
#23.6.2 API Gateway + Rate Limiting на границе
Rate limiting на уровне origin — это уже поздно: запрос уже прошёл через сетевое взаимодействие, загрузил базу данных. Rate limiting на уровне CDN — это блокировка на PoP, до origin.
Пользователь → PoP → [Rate Limiter → Worker → Cache → ORIGIN]
Если лимит превышен:
Пользователь → PoP → 429 Too Many Requests
(origin не дёргали!)Cloudflare предоставляет встроенный Rate Limiting, Fastly — через VCL/Compute. Лимиты можно делать:
- По IP (просто, но не надёжно — за NAT сидит много людей)
- По API key (требует разбора запроса на edge)
- По гео + время (блокировать регионы с подозрительным трафиком)
Предупреждение: распределённый rate limiting — сложная задача. Workers в разных PoP не имеют общего состояния (если не использовать Durable Objects / Redis). Counter, увеличиваемый на каждом PoP — не консистентен. Решения:
- Centralized (DynamoDB/Redis): задержка из-за round-trip к дата-центру
- Consistent hashing + local counters: approximate, но быстро
- Sliding window logs с синхронизацией: сложно, но точно
#23.6.3 Multi-region origin failover
Если origin в Айове упал, CDN может автоматически переключить трафик на origin в Дублине:
// Cloudflare Worker — failover между origins
const ORIGINS = {
primary: 'https://origin-us.example.com',
fallback: 'https://origin-eu.example.com',
last: 'https://origin-ap.example.com',
};
async function fetchWithFailover(request) {
for (const [name, url] of Object.entries(ORIGINS)) {
try {
const req = new Request(url + request.url.pathname, request);
const resp = await fetch(req, { timeout: 5000 });
if (resp.status < 500) return resp; // Успех
} catch (e) {
console.log(`${name} failed: ${e.message}`);
// Пробуем следующий
}
}
return new Response('All origins unavailable', { status: 503 });
}#23.6.4 A/B тестирование без боли
Классическая проблема A/B тестов: как разделить трафик, чтобы один и тот же пользователь видел одну версию, а не прыгал туда-сюда?
Решение на edge — consistent hashing по cookie/ID:
function getStableVariant(userId) {
// 50/50 распределение, стабильное для одного userId
return hashCode(userId) % 100 < 50 ? 'control' : 'test';
}
addEventListener('fetch', event => {
const userId = getUserFromCookie(event.request);
const variant = getStableVariant(userId);
if (variant === 'test') {
// Добавляем заголовок для origin
event.request.headers.set('X-Variant', 'test');
}
event.respondWith(fetch(event.request));
});Origin видит заголовок X-Variant и отдаёт соответствующую версию страницы. Никакой дополнительной инфраструктуры для A/B тестов.
#23.7 WebSocket на границе: а вот и проблемы
WebSocket и CDN — сложная история. Протокол WebSocket устанавливает постоянное соединение между клиентом и сервером. Если вы используете обычный CDN с HTTP-кэшированием, WebSocket просто не пройдёт — CDN не знает, что делать с протоколом ws://.
#23.7.1 Sticky sessions — проклятие WebSocket'ов
Проблема: WebSocket-соединение устанавливается с конкретным экземпляром вашего сервера. Если вы запустили 10 инстансов, а ваш балансировщик/CDN не «приклеивает» клиента к тому же серверу — соединение будет разрываться.
Решение для HTTP/WS прокси: CDN (или ваш load balancer) запоминает, на какой сервер ушёл первый WebSocket-запрос (upgrade handshake), и все последующие сообщения по этому соединению направляет на тот же сервер.
Cloudflare WebSocket поддерживает проксирование WebSocket, но с ограничениями:
- Максимальное время соединения: 100 часов (против неограниченного для прямого сервера)
- Стандартные заголовки для origin ограничены (некоторые блокируются)
- WebSocket нельзя «кэшировать» — каждое соединение идёт напрямую к origin
#23.7.2 Edge pub/sub — WebSocket без origin
Современное решение — edge-native pub/sub. Вместо того чтобы проксировать WebSocket к origin, сообщения маршрутизируются внутри CDN.
Cloudflare Durable Objects + WebSocket позволяют запустить координатор соединений на границе:
Клиент A (Милан) ──┐
├── Durable Object (Лондон) ── pub/sub
Клиент B (Сидней) ─┘
├── сохраняет состояние
├── бродкастит всем подключённым WebSocket'ам
└── запущен на PoP, ближайшем к большинству клиентовКак это работает:
// Cloudflare Durable Object — чат-комната в PoP
export class ChatRoom {
constructor(state, env) {
this.sessions = new Map();
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/websocket') {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.sessions.set(server, { joinedAt: Date.now() });
server.accept();
server.addEventListener('message', event => {
// Бродкастим всем
for (const [ws] of this.sessions) {
ws.send(event.data);
}
});
server.addEventListener('close', () => {
this.sessions.delete(server);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
}
}Это радикально меняет архитектуру real-time приложений:
- Вы не держите origin-сервер для WebSocket'ов
- Состояние живёт на границе (но должно умещаться в 128MB)
- Задержка между отправкой и получением — 10-50ms (даже для пользователей на разных континентах)
- Origin занимается только бизнес-логикой, не соединениями
#23.7.3 Альтернативы: Push CDN и Server-Sent Events
Если WebSocket через CDN вызывает слишком много проблем, рассмотрите:
Server-Sent Events (SSE):
- Работает поверх HTTP, кэшируется CDN
- Проще, чем WebSocket
- Односторонняя связь (сервер → клиент)
- Хорошо для уведомлений, фидов, логов
Push CDN (Akamai Push, Fastly Fanout):
- CDN инициирует соединение к origin и держит «постоянный канал»
- При изменении данных на origin, CDN пушит обновление клиентам
- Чище, чем long polling, проще, чем WebSocket
#23.8 Кейс: Cloudflare — 300+ PoP и Workers
Cloudflare — пожалуй, самый известный пример edge-first архитектуры. Давайте посмотрим, как устроена их сеть.
#Масштаб
- Пиковая пропускная способность: >300 Tbps (2025)
- Количество PoP: >330 в 120+ странах
- Доля рынка: ~20% всех сайтов используют Cloudflare
- Latency: 95% пользователей — <30ms до PoP
#Архитектура сети
Cloudflare не строит собственные дата-центры. Они размещают серверы в colocation-центрах крупных телеком-провайдеров. Это даёт:
- Immediate peering — прямой обмен трафиком без посредников
- Низкую задержку — «первая миля» от пользователя до PoP минимизирована
- Быстрое масштабирование — не нужно строить инфраструктуру
Структура PoP:
┌─────────────── Cloudflare PoP ─────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Nginx │ │ Nginx │ │ Nginx │ │
│ │ (TLS) │ │ (TLS) │ │ (TLS) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └──────┬──────┘ │ │
│ ▼ │ │
│ ┌────────────────────┐ │ │
│ │ Cache Layer │ │ │
│ │ (SSD, RAM cache) │ │ │
│ └────────┬───────────┘ │ │
│ │ │ │
│ ┌────────▼───────────┐ │ │
│ │ Workers Runtime │ │ │
│ │ (V8 isolates) │ │ │
│ └────────────────────┘ │ │
│ │ │
│ Forward Proxy → Origin │ │
└──────────────────────────────────┘ │#Workers — революция в edge computing
Cloudflare запустила Workers в 2017 году. Ключевое архитектурное решение: вместо контейнеров (как Lambda@Edge) — V8 isolates.
Преимущество V8 изолятов перед контейнерами:
- Контейнер: холодный старт 100-500ms, ~50ms на warm start
- V8 isolate: холодный старт 5-10ms, 1-3ms на warm
Разница драматическая, потому что в CDN вы не знаете, какой PoP получит запрос. Нагревать миллионы изолятов на 300+ PoP — бессмысленно. Но 5ms на холодном старте позволяют запускать код на каждом запросе, не задумываясь.
Гранулярность:
- Один Worker обрабатывает один запрос
- Несколько Workers могут быть привязаны к разным роутам одного домена
- Workers можно chaining: запрос проходит через цепочку Workers (как middleware)
#Когда Cloudflare не подходит
Несмотря на все плюсы, есть сценарии, где Cloudflare — не лучший выбор:
- Enterprise-клиенты с требованиями on-premise: Cloudflare — это публичная сеть, вы не контролируете PoP
- GDPR/Sovereign data: данные не должны покидать регион — Cloudflare может направлять трафик через PoP в соседней стране
- Особые протоколы: WebSocket, gRPC-streaming — поддержка есть, но с ограничениями
- CPU-intensive Workers: лимиты Workers (30ms CPU/запрос) могут быть недостаточны
#23.9 Кейс: Vercel Edge Network
Vercel — компания, стоящая за Next.js, построила свою CDN (Edge Network) вокруг концепции Serverless + Edge = Global by default.
#Архитектура
Vercel не конкурирует с Cloudflare в объёме PoP (у них ~100 локаций). Их фокус — developer experience и плотная интеграция с фреймворком.
┌────────── Vercel Edge Network ──────────┐
│ │
│ ┌───────────────────────────────┐ │
│ │ Smart Routing Layer │ │
│ │ (предсказание региона) │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Edge Functions │ │
│ │ (V8, <50μs холодный старт) │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Static / ISR Cache │ │
│ │ (Stale-While-Revalidate) │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Serverless Functions │ │
│ │ (AWS Lambda, private infra) │ │
│ └───────────────────────────────┘ │
│ │
└───────────────────────────────────────────┘#Incremental Static Regeneration (ISR)
Главная инновация Vercel в контексте CDN — ISR. Это гибрид между статикой и динамикой:
- Страница генерируется как статический HTML при деплое
- CDN отдаёт её из кэша (мгновенно)
- Когда истекает revalidate-таймер, следующий запрос инициирует фоновую регенерацию
- Пользователь получает старую, но быструю версию
- Новая версия попадает в кэш и отдаётся следующим пользователям
// Next.js страница с ISR
export async function getStaticProps() {
const data = await fetchExpensiveApi();
return {
props: { data },
revalidate: 300, // 5 минут
};
}ISR решает проблему TTL-кэша: данные не старше 5 минут, но страница всегда отдаётся из кэша. Origin не нагружается на каждый запрос.
#Edge Functions + Next.js
Vercel интегрировала Edge Functions напрямую в Next.js. Middleware в Next.js — это Edge Function, запущенная на PoP:
// middleware.ts — запускается на каждом PoP Vercel
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
const url = request.nextUrl.clone();
// Гео-редирект для /store
if (url.pathname === '/store' && country !== 'US') {
url.pathname = `/store/${country.toLowerCase()}`;
return NextResponse.rewrite(url);
}
// A/B тест (50/50, стабильный по cookie)
const cookie = request.cookies.get('variant');
if (!cookie && Math.random() < 0.5) {
const response = NextResponse.next();
response.cookies.set('variant', 'b');
return response;
}
return NextResponse.next();
}#Почему Vercel показателен
Vercel показывает, куда движется CDN-индустрия: CDN перестаёт быть «сетью доставки» и становится «платформой для приложений». Вы не «подключаете CDN к сайту» — вы деплоите приложение на CDN, и она сама решает, что кэшировать, что генерировать на лету, а что выполнять на сервере.
#23.10 Не делай так: анти-паттерны CDN
#Анти-паттерн №1: CDN как чёрный ящик
Симптомы:
- Вы не знаете, что кэшируется, а что — нет
- При проблеме говорите «наверное, это CDN кэширует» — и не можете проверить
- Purge — единственный инструмент, который вы знаете
Почему это плохо:
- Вы не можете диагностировать проблемы с кэшированием
- Теряете контроль над производительностью
- Чистка всего кэша при каждом деплое — «лечим насморк ампутацией»
Как надо:
- Настройте мониторинг: cache hit ratio, origin traffic, stale serves
- Используйте заголовки
X-Cache: HIT/MISSдля диагностики - Знайте режимы кэширования вашей CDN (edge vs browser cache)
- Для отладки:
CF-Cache-Status(Cloudflare),X-Cache(Akamai),Fastly-Debug
#Анти-паттерн №2: Purge всего при каждом деплое
Симптомы:
- CI/CD пайплайн заканчивается командой
curl -X PURGE https://cdn.example.com/* - После деплоя — всплеск нагрузки на origin (все PoP одновременно пошли за свежими данными)
- 10 файлов изменилось, а вы очистили кэш от 10,000 файлов
Почему это плохо:
- Thundering herd на origin: все PoP одновременно запрашивают все ресурсы. Origin получает нагрузку в 10-100x выше обычной.
- Избыточная нагрузка на CDN: чистка кэша — не бесплатная операция, особенно если она затрагивает все PoP
- Время регенерации: пока кэш прогревается, пользователи видят либо ошибки, либо пустые страницы
Как надо:
- Cache tags/purge by key: инвалидируйте только то, что изменилось
- Global purge — только для экстренных случаев (утекли секреты, критический баг)
- Если уж делаете global purge — делайте постепенно (staggered purge) или грейте кэш заранее
#Анти-паттерн №3: Игнорирование geo-routing для динамики
Симптомы:
- CDN кэширует статику, но весь динамический трафик идёт в один origin-region
- Европейские API-запросы идут в Айову, хотя у вас есть origin во Франкфурте
- Вы настроили DNS-based routing, но не проверили, что TTL не закэширован на клиенте
Как надо:
- Multi-region origin: API в us-east-1 и eu-west-1
- Geo-aware origin pull: PoP в Европе идёт к origin в Европе
- DNS TTL согласован с частотой обновления карты PoP
#Анти-паттерн №4: WebSocket без sticky sessions
Симптомы:
- WebSocket-соединение обрывается каждые несколько минут
- Пользователи жалуются на «потерю соединения», вы думаете — проблема в коде
- Вы не используете sticky sessions, и трафик распределяется round-robin
Как надо:
- Включите sticky sessions (session affinity) на вашем балансировщике/CDN
- Или используйте edge-native pub/sub (Durable Objects, Fanout)
- Или откажитесь от WebSocket через CDN, если не критично
#Анти-паттерн №5: Предполагать, что «CDN всё починит»
Симптомы:
- Запускаете сайт, подключаете Cloudflare — и удивляетесь, что производительность не выросла
- Не оптимизировали origin (медленные запросы, большие HTML-страницы, нет сжатия)
- CDN не может «ускорить то, чего нет» — если у вас плохой origin, CDN тоже будет плохим
Как надо:
- CDN — это последний слой оптимизации, не первый
- Сначала: оптимизация origin (кэши в приложении, БД-запросы)
- Потом: HTTP-кэширование (заголовки Cache-Control)
- Потом: CDN
#23.11 Чек-лист для самопроверки
- Определён TTL для каждого типа контента (статика, HTML, API)
- Настроены заголовки Cache-Control (public/private, max-age, s-maxage, stale-while-revalidate)
- Используются cache tags / surrogate-keys для точечной инвалидации
- Статика версионируется через content-hash в URL
- Настроен мониторинг: cache hit ratio, origin latency, bandwidth saved
- Определены сценарии, где edge computing решает проблему, а не создаёт новую
- WebSocket — либо через sticky sessions, либо через edge-native pub/sub
- Purge кэша — точечный, а не глобальный (за исключением экстренных случаев)
- Региональные origin'ы соответствуют географии пользователей (если применимо)
- DNS TTL согласован с частотой обновления geo-карты
#23.12 Ключевые выводы
-
CDN — это не опция, а необходимость для любого проекта с глобальной аудиторией. Задержка в 100ms снижает конверсию на 2-7%, и CDN — самый дешёвый способ эту задержку сократить.
-
Кэш должен быть умным. TTL — минимально необходимый. Cache tags — для активного управления. Content-hash — для статики. Purge всего кэша — только в экстренных случаях.
-
Edge computing превращает CDN из пассивного кэша в активную платформу. A/B тесты, гео-редиректы, image optimization, rate limiting — всё это можно (и часто нужно) делать на границе сети.
-
Anycast + DNS — комбинация для маршрутизации. Anycast даёт отказоустойчивость и простоту, DNS — гибкость. Используйте оба.
-
WebSocket на CDN — сложно. Либо sticky sessions, либо edge-native pub/sub (Cloudflare Durable Objects, Fastly Fanout). Или SSE, если односторонней связи достаточно.
-
CDN не «чинит» плохой origin. Сначала оптимизируйте приложение и кэши на уровне бэкенда, потом подключайте CDN как финальный слой ускорения.
К следующей главе: Если CDN и Edge Computing — про распределение контента по миру, то следующая тема — про то, как ваша архитектура может выжить, когда часть мира отваливается. Circuit Breaker, Bulkhead, Retry — переходим к Resilience Patterns.