Перейти к содержимому

Сплит трафика РФ / мир через GeoDNS и CDN

Сайт работает. Из России открывается быстро. Но:

  • Telegram не показывает превью ссылки. Кидаете ссылку в чат — вместо карточки с заголовком и картинкой просто синий текст. Иногда превью появляется через раз или только со второй попытки.
  • То же самое в других мессенджерах. WhatsApp, Slack, Discord, LinkedIn — карточка либо пустая, либо без картинки.
  • Видео и тяжёлые медиа не проигрываются у зарубежных зрителей или стартуют с задержкой в несколько секунд.
  • Зарубежные посетители жалуются на «тормозит», хотя в РФ метрики зелёные.
  • Google Search Console показывает высокое время ответа при обходе, хотя сервер не нагружен.

Общий признак: всё, что приходит из-за пределов РФ, работает хуже, чем то, что приходит изнутри.

Сайт лежит на одном сервере в России. Это нормально для аудитории в РФ и обязательно, если вы работаете с персональными данными по 152-ФЗ. Но каждый зарубежный запрос — это трансатлантический round-trip до Москвы и обратно.

Для человека это «медленно». Для бота — это отказ.

Боты превью ссылок не ждут долго. У краулера Telegram, Slack, Discord жёсткий таймаут на получение HTML: не уложились — карточка не строится, и повторной попытки чаще всего не будет. Складываются три задержки:

  1. TCP + TLS handshake через полмира — это уже 2–3 RTT до первого байта запроса;
  2. генерация страницы на origin, если она не в кэше;
  3. деградация транзита — зарубежные маршруты в РФ нестабильны, потери и всплески latency обычное дело.

При RTT 250–350 мс и холодном кэше вы легко упираетесь в секунду-полторы до первого байта. Бот отваливается. Пользователь видит голую ссылку.

Вторая часть проблемы — медиа. Картинка для og-превью и видео качаются с того же origin. Даже если HTML успел отдаться, картинка может не успеть.

Почему «просто поставить CDN на весь сайт» — плохое решение

Заголовок раздела «Почему «просто поставить CDN на весь сайт» — плохое решение»

Очевидный ход: завернуть весь домен в зарубежный CDN. Это чинит зарубежных посетителей и ломает основную аудиторию.

Диапазоны зарубежных CDN у российских операторов периодически деградируют или попадают под фильтрацию. Показательный пример — проблемы Cloudflare в РФ с июня 2025 года: соединения обрываются после первых 16 КБ ответа, то есть сайт «открывается наполовину».

Поставив весь трафик за зарубежный CDN, вы меняете понятную проблему (медленно для 5% аудитории) на непредсказуемую (сайт может лечь для 95%).

Идея простая: пусть DNS отвечает разными адресами в зависимости от того, откуда пришёл запрос.

запрос wpcraft.ru
┌────────┴────────┐
│ GeoDNS-зона │
└────────┬────────┘
┌──────────────────┴──────────────────┐
│ │
резолвер в РФ резолвер вне РФ
│ │
▼ ▼
A → origin в РФ Pull Zone → ближайший PoP CDN
(прямое соединение, (edge-кэш HTML и статики)
CDN в пути нет) │
│ │ промах кэша
▼ ▼
┌───────────────────────────────────────────────────┐
│ origin.example.ru → тот же сервер │
└───────────────────────────────────────────────────┘

Что получаем:

КтоМаршрутЭффект
Посетитель из РФпрямое соединение с originничего не изменилось, CDN не в критическом пути
Посетитель из-за рубежаближайший PoP, отдача из кэшаTTFB падает с ~1 с до десятков мс
Бот превью Telegram/Slackближайший PoPукладывается в таймаут, превью строится
Поисковый краулерближайший PoPбыстрый обход, меньше нагрузка на origin

Второй origin не нужен, данные никуда не переезжают, 152-ФЗ не затрагивается: сервер один и он в РФ, а edge кэширует только анонимные GET-ответы.

Мы используем для этого Bunny.net — у него есть и DNS с географическими record set, и CDN, и pay-as-you-go тариф без абонентки. Схема не привязана к вендору: любой GeoDNS + pull-CDN подойдёт.

Это первое, что нужно сделать, и самая частая ошибка, если этого не сделать.

Нельзя указывать pull zone на example.ru. Edge-нода при промахе кэша резолвит этот домен через публичный DNS — и, находясь за рубежом, получает «мировую» ветку, то есть саму себя. Петля. Сайт за границей ложится ровно в момент включения гео-сплита.

Решение — отдельная A-запись, которая не участвует в гео-сплите и всегда указывает на реальный сервер:

origin.example.ru. 300 IN A <IP сервера>

Pull zone смотрит на https://origin.example.ru, а заголовок Host подменяет на канонический example.ru, чтобы WordPress не начал генерировать ссылки на origin-хостнейм.

На этот хостнейм нужен валидный TLS-сертификат на стороне хостинга — иначе origin придётся опрашивать по HTTP, что для сайта с авторизацией недопустимо.

2. Настройки pull zone, которые ломают WordPress по умолчанию

Заголовок раздела «2. Настройки pull zone, которые ломают WordPress по умолчанию»

Bunny (и не он один) даёт дефолты, рассчитанные на раздачу статики. На динамическом WordPress каждый из них по отдельности достаточен, чтобы уронить сайт.

ПараметрДефолтНужноЧто ломается на дефолте
Origin URLдомен сайтаhttps://origin.example.ruпетля CDN-Loop
Origin Host Headerпустоexample.ruWordPress видит не тот домен, ломаются редиректы и канонические ссылки
Disable cookiesвключеновыключитьрежет Set-Cookie от origin — не работает вход и гостевая корзина
Ignore query stringsвключеновыключить?page=2, UTM и фильтры схлопываются в один ключ кэша — посетитель получает чужую страницу
Cache-Control overrideне задаватьзаголовки origin должны остаться единственным источником правды
TLS 1.0 / 1.1включеновыключитьлегаси-протоколы

Отдельно проверьте Verify origin SSL — его стоит включить после того, как на origin-хостнейме появится валидный сертификат.

Решение о кэшируемости принимаем по методу и пути, а не по кукам:

  • метод не GET/HEAD → не кэшируем;
  • путь начинается с /wp-admin, /wp-login.php, /wp-cron.php, /wp-json, /cart, /checkout, личный кабинет → не кэшируем;
  • всё остальное — по заголовкам origin.

Почему не по кукам: если сайт уже отдаёт залогиненным пользователям тот же публичный HTML, что и гостям (обычная практика для контентных сайтов с page cache), то bypass по wordpress_logged_in_* отправит каждого авторизованного на PHP и обнулит смысл кэша. Персональные страницы отделяются путём, а не кукой.

Важно: этот список путей обычно дублирует правила кэш-плагина на origin. Это единственная точка рассинхрона в схеме — меняете список на сервере, меняйте и на edge.

Edge должен слушать Cache-Control от origin, а не иметь собственный TTL. Тогда время жизни кэша задаётся в одном месте — в коде сайта.

Здесь ждёт неочевидная ловушка. Если на origin стоит кэш-плагин в режиме статических файлов (WP Super Cache в expert-режиме, LiteSpeed, любой вариант с отдачей .html через rewrite), то при попадании в кэш PHP не стартует, файл отдаёт веб-сервер напрямую — и заголовка Cache-Control в ответе нет, только ETag и Last-Modified.

То есть на большинстве ответов edge не получит никакой инструкции. Проверяется в одну команду:

Окно терминала
curl -I https://example.ru/ # попадание в статический кэш
curl -I 'https://example.ru/?nocache=1' # промах, отвечает PHP

Если в первом случае Cache-Control нет, а во втором есть — вы поймали именно это. Чинится на origin: добавить заголовок для отдачи из каталога статического кэша, вне маркеров, которые перезаписывает плагин.

<IfModule mod_headers.c>
Header always set Cache-Control "public, max-age=604800" "expr=%{REQUEST_FILENAME} =~ m#/wp-content/cache/supercache/#"
</IfModule>

Запасной вариант — задать TTL прямо на pull zone. Проще, но переопределяет заголовки глобально: страницы, которые кэшировать нельзя, тоже попадут под общее правило.

Edge будет держать страницу столько, сколько сказано в Cache-Control. Без принудительного сброса правка текста доедет до зарубежной аудитории через недели.

Минимальный mu-plugin на save_post, edited_term и обновление меню, дёргающий Purge API провайдера, закрывает вопрос. Разумный ориентир — обновление видно за границей в пределах минуты.

С мировой ветки origin увидит IP edge-ноды, а не посетителя. Клиентский адрес приезжает в X-Forwarded-For.

Доверять этому заголовку можно только для запросов из диапазонов вашего CDN. Иначе любой желающий подставит произвольный IP в комментарии, логи и антиспам.

Географические DNS-записи работают по принципу «ближайшая точка», а не по правилу «страна == RU». Поэтому мало завести две ветки — нужны геоякоря:

  • ветка РФ — якорь в Москве;
  • ветка мира — несколько якорей: Франкфурт, Нью-Йорк, Сингапур, Сан-Паулу.

Без мировых якорей посетители из Финляндии, Польши, Турции окажутся географически «ближе» к Москве и уйдут на origin в обход CDN. Абсолютной точности здесь не будет — это принципиальное ограничение GeoDNS, и его надо принять заранее.

Порядок, который экономит нервы:

  1. Создать зону у нового DNS-провайдера как точную копию текущей — гео-сплит выключен, все ветки отдают origin.
  2. Сверить записи один в один, запрашивая напрямую у новых NS.
  3. Сменить NS у регистратора.
  4. Дождаться распространения.
  5. И только потом включать гео-сплит.

Два момента, о которых узнают поздно:

TTL делегирования в зоне .RU — 345 600 секунд (4 суток), он задан реестром и не понижается:

Окно терминала
dig NS example.ru @a.dns.ripn.net

Значит после смены NS резолверы до четырёх суток продолжат ходить на старого провайдера. Столько же займёт и откат. Старую зону всё это время держим живой и неизменной — это и есть кнопка отката.

CAA-записи могут не переехать. Cloudflare, например, синтезирует их на лету и не хранит в зоне: в экспорте их нет, а после переезда они просто исчезнут. Как минимум нужно завести:

example.ru. 300 IN CAA 0 issue "letsencrypt.org"

Без этого CDN не выпустит сертификат для вашего домена, а хостинг — для origin-хостнейма.

CDN обычно валидирует ваш домен по HTTP-01, а для этого домен уже должен указывать на pull zone. Возникает окно: NS переехали, сертификата ещё нет, а поставить домен на CDN без сертификата — TLS-ошибка у всех посетителей.

Чище выпустить сертификат заранее через DNS-01 (валидация TXT-записью, домен при этом никуда указывать не должен) и загрузить его в pull zone вручную. Окна без сертификата не возникает вовсе.

Резолв из разных точек — из РФ должен приходить origin, извне адреса CDN:

Окно терминала
dig example.ru @77.88.8.8 # резолвер в РФ → IP origin
dig example.ru @8.8.8.8 # резолвер вовне → IP PoP CDN
dig origin.example.ru # везде → IP origin

Кэш отрабатывает — второе обращение из-за рубежа должно дать HIT:

Окно терминала
curl -sI https://example.ru/ | grep -i 'cdn-cache\|cache-control'

Исключения не кэшируются:

Окно терминала
for p in /cart /checkout /wp-json/wp/v2/posts /wp-login.php; do
printf '%-28s ' "$p"
curl -sI "https://example.ru$p" | grep -i 'cdn-cache' || echo 'нет заголовка'
done

Превью ссылки. Самая честная проверка исходной проблемы — кинуть ссылку в Telegram и посмотреть, собралась ли карточка. Проверять на ссылке, которую раньше не отправляли: у мессенджеров есть собственный кэш превью, и старая ссылка может отдаться из него.

Функциональные сценарии, которые ломаются чаще всего:

  • вход в админку из-за рубежа;
  • гостевая корзина переживает переход между страницами;
  • пагинация и фильтры отдают разные страницы, а не одну и ту же;
  • публикация записи видна за границей в пределах минуты;
  • в логах origin виден реальный IP, а не адрес edge-ноды.

Схема не бесплатная с точки зрения сложности:

  • Два слоя кэша с частично дублирующейся конфигурацией — правила на origin и на edge надо держать синхронными вручную.
  • Гео-мисроутинг приграничных стран — часть европейской аудитории может уходить на origin в обход CDN.
  • Откат не мгновенный — 4 суток TTL делегирования в .RU.
  • Диагностика усложняется — на вопрос «почему у клиента не открывается» теперь два разных ответа в зависимости от его геолокации.

Если зарубежная аудитория и превью ссылок для вас не критичны — не начинайте. Если критичны — это дешевле, чем второй origin за рубежом с репликацией данных.