Строим свой CDN-сервер: один раз забрать, тысячу раз отдать

Опубликовано 8 мин. чтения

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 это машина для отдачи. Поэтому порядок важных свойств выглядит иначе, чем у сервера приложений:

  1. Трафик и канал. Здесь возникает вся нагрузка. Узел в работе регулярно передаёт несколько терабайт в месяц.
  2. Диск. NVMe заметно в выигрыше, потому что кеш состоит из множества мелких случайных обращений. Размер определяется тем, какая часть вашего содержимого горячая, а не тем, насколько велико содержимое целиком.
  3. Оперативная память. Операционная система использует свободную память как второй кеш перед диском. Больше памяти здесь напрямую означает больше скорости.
  4. Процессор. Самое некритичное, он нужен только для TLS, а с этим современные процессоры справляются попутно.

Ровно для этого случая сделаны наши серверы с безлимитным трафиком: настоящие 2 × 1 Гбит/с на каждый сервер без ограничения скорости, неограниченный объём по фиксированной цене за месяц, NVMe уровня центра обработки данных в RAID и защита от флуда на 3,2 Тбит/с с завода. Выдача занимает меньше 30 секунд, с полным доступом root, с IPv4 и сетью IPv6 /64. Так как расчёт идёт по предоплате и отказаться можно ежемесячно, вы можете начать с одного узла и в любой момент добавить вторую площадку, когда ваша публика начнёт распределяться.

Частые вопросы

Что именно делает CDN-сервер?
Он стоит между вашими посетителями и вашим сервером-источником. Первый запрос файла он передаёт источнику и при этом откладывает ответ себе. На все следующие запросы того же файла он отвечает сам, не беспокоя источник.
Насколько это реально разгружает?
Это зависит от доли попаданий. При 95 процентах ваш источник видит только каждый двадцатый запрос, то есть нагрузка падает до одной двадцатой. При 80 процентах это всё ещё одна пятая. Для статических файлов вроде изображений, скриптов, шрифтов и загрузок 95 процентов и больше это норма.
Нужен ли для этого коммерческий поставщик?
Не обязательно. Свой CDN-сервер оправдан прежде всего при больших файлах и большом объёме, потому что коммерческие поставщики считают деньги за каждый переданный гигабайт. На сервере с безлимитным трафиком сотый терабайт стоит столько же, сколько первый, а именно ничего дополнительно.
Какие файлы кешировать, а какие нет?
Всё, что выглядит одинаково для всех посетителей: изображения, видео, CSS, JavaScript, шрифты, PDF, установочные файлы. Не стоит кешировать персональные страницы, корзины, личные разделы и всё, что несёт сессионный cookie. Лучше всего это решение принимает сам источник через заголовок Cache-Control.
Как долго кешу держать файлы?
Для файлов с номером версии или контрольной суммой в имени спокойно год, они никогда не меняются. Для изображений и загрузок без версий обычны часы и дни. Для HTML, если вы его вообще кешируете, минуты. Важно не точное число, а наличие способа удалить отдельные файлы целенаправленно.
Как удалить файл из кеша?
Проще всего через имя файла: кто при каждом изменении выдаёт новое имя, тому удалять вообще не нужно. Если так не получается, в nginx помогает либо коммерческий модуль purge, либо свободный ngx_cache_purge. В крайнем случае вы удаляете файл кеша прямо в файловой системе, путь вычисляется из ключа кеша.
Что происходит, если сервер-источник выходит из строя?
При правильной настройке кеш продолжает отдавать, даже если записи уже просрочены. Об этом заботятся proxy_cache_use_stale с error и timeout, а также proxy_cache_background_update. Ваша страница остаётся доступной, пока источник обслуживают или перезапускают.
Работает ли это и с большими видеофайлами?
Да, но только с настройкой slice. Без неё кеш при каждой перемотке в видео заново загружает весь файл с источника. С slice nginx разбивает файл на блоки, например по одному мегабайту, и откладывает только те части, которые действительно запрашивались.
Сколько площадок мне нужно?
Начните с одной, и именно там, где находится большинство ваших посетителей. Вторая площадка оправдана только тогда, когда заметная часть публики сидит на другом континенте. Распределение идёт потом через DNS, каждый посетитель попадает на ближайший узел.
Как узнать, что кеш работает?
Выводите переменную upstream_cache_status в заголовке. При втором обращении к тому же файлу там должно стоять HIT. Если вы пишете это же значение ещё и в журнал, долю попаданий можно подсчитать в любой момент. Ниже 90 процентов на статических файлах обычно что-то не так с заголовками или с cookie.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic