Строим свой CDN-сервер: один раз забрать, тысячу раз отдать
CDN-сервер забирает каждый файл с источника один раз и потом тысячу раз отдаёт его сам. Сервер-источник видит лишь малую долю запросов, а посетители получают короткое время загрузки. Вот как такой сервер собрать.
Каждый файл, который посетитель загружает с вашей страницы, ваш сервер-источник достаёт из файловой системы, упаковывает в ответ и отправляет. При десяти посетителях в день этого никто не замечает. При десяти тысячах одновременных посетителей именно это и есть причина, по которой страница становится медленной, хотя само приложение вообще ничем не занято.
CDN-сервер встаёт перед ним. Он забирает каждый файл один раз с источника и дальше отдаёт его сам, столько раз, сколько потребуется. Источник из-за этого видит лишь малую долю запросов, а посетители получают свои файлы с сервера, который не делает ничего, кроме отдачи. В этой статье разобрано, сколько это реально даёт, как поднять такой сервер на nginx и что важно в трафике и выборе площадки.
Что CDN-сервер делает на самом деле
Порядок работы укладывается в три предложения:
- Посетитель запрашивает файл у CDN-сервера.
- Если файл там уже лежит, он сразу уходит. Это попадание.
- Если файла там нет, CDN-сервер забирает его у источника, откладывает себе и отдаёт. Это промах, и происходит он на каждый файл только один раз за срок годности.
Принцип тот же, что и у стриминг-сервера: один раз забрать, много раз отдать. Разница только в том, что стриминг-сервер раздаёт идущий поток данных, а CDN-сервер готовые файлы.
Расчёт: насколько кеш разгружает
Решает доля попаданий, то есть часть запросов, на которые кеш отвечает сам. При миллионе запросов в день разгрузка выглядит так:
| Доля попаданий | Запросы к источнику | Разгрузка |
|---|---|---|
| 50 % | 500 000 | в 2 раза |
| 80 % | 200 000 | в 5 раз |
| 95 % | 50 000 | в 20 раз |
| 99 % | 10 000 | в 100 раз |
Для статических файлов 95 процентов и больше это обычное дело, ведь изображения, скрипты и шрифты меняются редко. Прыжок с 95 на 99 процентов при этом ценнее, чем можно предположить по числам: он уменьшает остаточную нагрузку ещё впятеро.
Одновременно смещается трафик. Источник передаёт уже почти ничего, зато CDN-сервер тем больше. При 500 000 обращениях к изображениям в день по 400 КБ каждое это 200 ГБ в сутки, то есть около 6 ТБ в месяц, и только на картинки. Поэтому у узла CDN важное число не процессор, а канал и объём.
Когда свой CDN-сервер оправдан
Коммерческие поставщики CDN считают деньги за каждый переданный гигабайт. Это удобно и при небольшом объёме дёшево. С определённого количества соотношение переворачивается:
| Случай | Свой CDN-сервер | Оплата за гигабайт |
|---|---|---|
| Немного ГБ в месяц | почти не оправдан | дешевле |
| Много ТБ в месяц | фиксированная цена за месяц | растёт линейно |
| Крупные загрузки, видеоархивы | явно в выигрыше | быстро дорожает |
| Полный контроль над правилами и протоколами | полный | ограниченный |
К этому добавляется пункт, который плохо выражается в числах: на своём узле вы сами решаете, что кешируется, как долго и когда исчезает. Вы не привязаны к правилам чужого поставщика и можете отдавать даже то, что в чужие схемы не укладывается.
Сборка на nginx в роли кеша
Установка
apt update
apt install -y nginx
Больше ничего не нужно, кеш встроен в nginx. Создайте каталог, в котором будут лежать файлы:
mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn
Настройка
Кеш один раз описывается в блоке http и потом используется в отдельных локациях:
proxy_cache_path /var/cache/cdn
levels=1:2
keys_zone=cdn:100m
max_size=200g
inactive=30d
use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
Значения по отдельности означают вот что: keys_zone=cdn:100m хватает примерно на 800 000 записей, max_size ограничивает место на диске, inactive=30d выбрасывает файлы, которые 30 дней никто не запрашивал, а use_temp_path=off избавляет от лишнего перекладывания между каталогами.
Ниже сам сервер:
server {
listen 443 ssl;
http2 on;
server_name cdn.example.com;
ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
location / {
proxy_pass https://origin.example.com;
proxy_set_header Host origin.example.com;
proxy_http_version 1.1;
proxy_cache cdn;
proxy_cache_valid 200 301 302 24h;
proxy_cache_valid 404 1m;
# несколько одновременных промахов сводим в один запрос
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
# при проблемах на источнике продолжаем отдавать
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# сессионные cookie не должны разрушить кеш
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Три строки из этого заслуживают особого внимания. proxy_cache_lock следит за тем, чтобы при новом файле к источнику обращались не сто посетителей одновременно, а только один, а остальные ждали. proxy_cache_use_stale поддерживает выдачу, пока источник перезапускается или отвечает медленно. А скрытие Set-Cookie не даёт одному единственному ответу с сессионным cookie сделать весь кеш бесполезным.
Проверяем, что это работает
nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
При первом обращении там стоит MISS, при втором HIT. Если и во второй раз стоит MISS, дело почти всегда в одном из трёх обычных подозреваемых: Set-Cookie в ответе, Cache-Control: no-store от источника или отсутствующее указание длины.
Что кешируется, а что нет
Чистое решение состоит в том, чтобы источник сам об этом сообщал. Он знает своё содержимое, а кеш нет. Для этого он выставляет подходящие заголовки:
| Содержимое | Заголовок на источнике |
|---|---|
| Файл с контрольной суммой в имени | Cache-Control: public, max-age=31536000, immutable |
| Изображение или загрузка без версии в имени | Cache-Control: public, max-age=86400 |
| HTML публичной страницы | Cache-Control: public, max-age=300 |
| Личный раздел, корзина | Cache-Control: private, no-store |
Если вы хотите решать в самом кеше, это делается через отдельные локации для каждого типа файлов:
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Cache-Status $upstream_cache_status always;
}
Как целенаправленно избавиться от содержимого
Самый простой путь состоит в том, чтобы вообще не приходилось удалять: кто каждому изменённому изображению даёт новое имя файла, например logo.a1b2c3.png, с этой задачей не сталкивается. Старый файл когда-нибудь сам выпадет из кеша.
Если так не получается, в nginx помогает свободный модуль ngx_cache_purge. Без дополнительного модуля путь можно вычислить из ключа кеша, потому что nginx использует его сумму MD5 в качестве имени файла:
KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"
Для повседневной работы обычно достаточно небольшого скрипта, который выполняет эти строки для списка путей.
Большие файлы и видео
При файлах больше нескольких сотен мегабайт вам нужно разбиение на блоки, иначе кеш при каждой перемотке в видео заново загружает весь файл:
location /video/ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 206 7d;
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$uri$slice_range";
}
Так nginx забирает только те блоки, которые действительно запрашиваются. Если зритель прыгает на середину фильма, по каналу идут несколько мегабайт вместо нескольких гигабайт.
TLS, HTTP/2 и HTTP/3
Узел CDN отдаёт много маленьких файлов во множество соединений, и именно здесь новые протоколы себя оправдывают. HTTP/2 включается строкой http2 on;. HTTP/3 даёт выигрыш прежде всего на мобильных соединениях, потому что при смене сети соединение не обрывается:
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
Не забудьте при этом открыть UDP 443 в файрволе. Это самая частая причина, по которой HTTP/3 настроен, но им всё равно никто не пользуется.
Несколько площадок
Один единственный узел даёт уже большую часть выигрыша, а именно разгрузку источника. Второй выигрыш, короткий путь до посетителя, появляется только с несколькими площадками.
Распределение идёт через DNS: одно и то же имя указывает, в зависимости от происхождения посетителя, на разные узлы. Для этого вам нужен поставщик DNS с географической привязкой. Все узлы получают одну и ту же конфигурацию и один и тот же источник, друг о друге они ничего не знают, и именно это делает схему такой устойчивой: если узел выходит из строя, вы убираете его из DNS, а остальные берут нагрузку на себя.
KernelHost предлагает площадки в Германии, на западном побережье США, на восточном побережье США, а также в Сингапуре и Южной Корее. Обзор с задержками вы найдёте на странице Локации серверов. Как правило: по одному узлу на каждый континент, на котором сидит заметная часть вашей публики.
Измерять, а не угадывать
Записывайте состояние кеша в журнал, тогда долю попаданий можно подсчитать в любой момент:
log_format cdn '$remote_addr $status $body_bytes_sent '
'$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;
Разобрать это можно одной строкой:
awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn
Если доля HIT на статических файлах ниже 90 процентов, искать причину почти всегда стоит: чаще всего это cookie в ответах, слишком короткий срок годности или запросы с меняющимися параметрами в конце адреса, которые для кеша каждый раз выглядят как новый файл.
Какой сервер подойдёт
Узел CDN это машина для отдачи. Поэтому порядок важных свойств выглядит иначе, чем у сервера приложений:
- Трафик и канал. Здесь возникает вся нагрузка. Узел в работе регулярно передаёт несколько терабайт в месяц.
- Диск. NVMe заметно в выигрыше, потому что кеш состоит из множества мелких случайных обращений. Размер определяется тем, какая часть вашего содержимого горячая, а не тем, насколько велико содержимое целиком.
- Оперативная память. Операционная система использует свободную память как второй кеш перед диском. Больше памяти здесь напрямую означает больше скорости.
- Процессор. Самое некритичное, он нужен только для TLS, а с этим современные процессоры справляются попутно.
Ровно для этого случая сделаны наши серверы с безлимитным трафиком: настоящие 2 × 1 Гбит/с на каждый сервер без ограничения скорости, неограниченный объём по фиксированной цене за месяц, NVMe уровня центра обработки данных в RAID и защита от флуда на 3,2 Тбит/с с завода. Выдача занимает меньше 30 секунд, с полным доступом root, с IPv4 и сетью IPv6 /64. Так как расчёт идёт по предоплате и отказаться можно ежемесячно, вы можете начать с одного узла и в любой момент добавить вторую площадку, когда ваша публика начнёт распределяться.
Частые вопросы
Что именно делает CDN-сервер?
Насколько это реально разгружает?
Нужен ли для этого коммерческий поставщик?
Какие файлы кешировать, а какие нет?
Как долго кешу держать файлы?
Как удалить файл из кеша?
Что происходит, если сервер-источник выходит из строя?
Работает ли это и с большими видеофайлами?
Сколько площадок мне нужно?
Как узнать, что кеш работает?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

