Часть V: Высоконагруженные системы
Глава 23

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 архитектура: как контент оказывается рядом с пользователем

Давайте начнём с простого вопроса: что происходит, когда пользователь из Милана открывает ваш сайт, серверы которого стоят в Айове?

  1. DNS-запрос: Милан → DNS-сервер (~10ms)
  2. Установка TCP-соединения: Милан ↔ Айова (~140ms round-trip)
  3. TLS handshake: ещё ~140ms
  4. HTTP-запрос: Милан → Айова (~70ms)
  5. Сервер обрабатывает запрос (~100-300ms)
  6. Ответ обратно: Айова → Милан (~70ms)
  7. Браузер парсит 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.

Схема работы:

  1. Первый пользователь из Милана запрашивает /images/cat.jpg
  2. PoP в Милане проверяет: есть такой файл в кэше? Нет.
  3. PoP запрашивает файл у origin в Айове
  4. Origin отдаёт файл, PoP сохраняет его локально (на SSD в стойке)
  5. PoP отдаёт файл пользователю
  6. Второй пользователь из Рима запрашивает тот же файл — 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_123

CDN удалит из кэша все ответы, содержащие этот тег. Не всю страницу целиком, не всё подряд — только то, что связано с изменившимся товаром.

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 с частым изменением данных

Выходы:

  1. CDN как proxy + server push (SSE): не кэшируем HTML, но кэшируем тяжёлые компоненты (шапка, подвал, каталог) через Edge Side Includes (ESI) — технология, при которой CDN собирает страницу из кусков, каждый со своим TTL.

  2. Private CDN: поднимаете свой кэш в каждом регионе (например, Redis + Nginx) — дорого, но даёт полный контроль.

  3. 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-функцию на одно из четырёх событий:

  1. Viewer Request — запрос от пользователя (можно авторизовать, переписать URL)
  2. Origin Request — запрос от PoP к origin (можно подменить origin, добавить headers)
  3. Origin Response — ответ от origin к PoP (можно модифицировать, сжать)
  4. 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. Это гибрид между статикой и динамикой:

  1. Страница генерируется как статический HTML при деплое
  2. CDN отдаёт её из кэша (мгновенно)
  3. Когда истекает revalidate-таймер, следующий запрос инициирует фоновую регенерацию
  4. Пользователь получает старую, но быструю версию
  5. Новая версия попадает в кэш и отдаётся следующим пользователям
// 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 Ключевые выводы

  1. CDN — это не опция, а необходимость для любого проекта с глобальной аудиторией. Задержка в 100ms снижает конверсию на 2-7%, и CDN — самый дешёвый способ эту задержку сократить.

  2. Кэш должен быть умным. TTL — минимально необходимый. Cache tags — для активного управления. Content-hash — для статики. Purge всего кэша — только в экстренных случаях.

  3. Edge computing превращает CDN из пассивного кэша в активную платформу. A/B тесты, гео-редиректы, image optimization, rate limiting — всё это можно (и часто нужно) делать на границе сети.

  4. Anycast + DNS — комбинация для маршрутизации. Anycast даёт отказоустойчивость и простоту, DNS — гибкость. Используйте оба.

  5. WebSocket на CDN — сложно. Либо sticky sessions, либо edge-native pub/sub (Cloudflare Durable Objects, Fastly Fanout). Или SSE, если односторонней связи достаточно.

  6. CDN не «чинит» плохой origin. Сначала оптимизируйте приложение и кэши на уровне бэкенда, потом подключайте CDN как финальный слой ускорения.


К следующей главе

К следующей главе: Если CDN и Edge Computing — про распределение контента по миру, то следующая тема — про то, как ваша архитектура может выжить, когда часть мира отваливается. Circuit Breaker, Bulkhead, Retry — переходим к Resilience Patterns.