Пинг был 12 мс, а соединение вставало 3 секунды: виноват оказался IPv6
Тикет выглядел абсурдно: ping до сервера стабильно показывал 12 мс, мониторинг был зелёный, нагрузка на CPU и диск — в пределах нормы, а часть пользователей жаловалась, что сайт «думает» секунды три перед тем, как вообще начать грузиться. Не пять раз из ста, не только у одного клиента — воспроизводимо, но не у всех. Разбор этого случая занял больше времени, чем хотелось бы, именно потому, что первые полчаса ушли не туда — на сервер и приложение, хотя дело было в паре строк DNS-записей и до конца не настроенном IPv6.
Содержание
- Симптом: идеальный ping и подвисающий коннект
- Подозреваемый номер один: сервер, приложение — и совпадение, которое им не было
- curl -4 против curl -6: момент истины
- Почему именно Happy Eyeballs и почему именно секунды, а не мгновенно
- Корень проблемы: AAAA-запись обогнала настройку IPv6
- Что делать: починить IPv6 до конца или убрать AAAA-запись
- Мониторинг: проверяйте IPv4 и IPv6 по отдельности
Симптом: идеальный ping и подвисающий коннект
Жалоба формулировалась примерно так: «страница открывается с задержкой», без деталей. Первая проверка — обычная, ручная:
ping example.com
64 bytes from 203.0.113.10: icmp_seq=1 ttl=55 time=12.4 ms
64 bytes from 203.0.113.10: icmp_seq=2 ttl=55 time=11.9 ms
64 bytes from 203.0.113.10: icmp_seq=3 ttl=55 time=12.7 ms
Идеальная картина. Дальше — curl -w с разбивкой таймингов, чтобы увидеть, на каком этапе именно уходит время:
curl -o /dev/null -s -w \
"dns: %{time_namelookup}\nconnect: %{time_connect}\nttfb: %{time_starttransfer}\ntotal: %{time_total}\n" \
https://example.com
И вот тут появлялась странность: time_namelookup — доли секунды, а time_connect — около 3 секунд, притом что сам ping до того же хоста отвечал за 12 мс. То есть проблема не в DNS-резолвинге, не в TLS-хэндшейке (он идёт уже после connect) и не в бэкенде — задержка возникала строго на этапе установки TCP-соединения, до того как сервер вообще успевал что-то ответить.
Первая мысль в такой ситуации обычно — «сервер под нагрузкой не успевает принять соединение» или «где-то в цепочке балансировщик/файрвол держит SYN-пакет». Но это не подтвердилось: ping меряет RTT до того же IP по тому же физическому пути (в большинстве случаев), и если бы сеть между вами и сервером реально деградировала, задержка была бы видна и в ICMP-ответах. Она не была видна — потому что, как выяснилось позже, ping без ключей на большинстве систем использует IPv4, а проблемный трафик шёл по другому протоколу.
Подозреваемый номер один: сервер, приложение — и совпадение, которое им не было
Первым делом проверили то, что проверяют всегда: логи nginx, состояние worker-процессов, очередь backlog на прослушивающем сокете, метрики соединений в ss -s. Ничего аномального — активных соединений в пределах обычного, ошибок 502/504 в логах нет, отказов из-за SYN flood тоже. Смотрели dmesg на предмет переполнения очереди net.core.somaxconn — тоже мимо.
Проверили приложение — не копится ли где-то блокирующий вызов перед тем, как сокет начинает accept(). Гипотеза правдоподобная, но она не объясняла главного: если бы проблема была на стороне сервера, она проявлялась бы у всех клиентов одинаково. А она проявлялась выборочно.
Здесь стоит отметить методическую ошибку, в которую легко провалиться: раз ping в порядке, соблазн исключить сеть целиком и копать только в сторону приложения. Это ловушка — ping проверяет только ICMP по одному конкретному протоколу (обычно IPv4) и не говорит ничего о судьбе TCP-трафика по IPv6, если он вообще уходит по другому маршруту. Здесь пригодится и более широкий взгляд на то, как измерить реальный ping до сервера методикой, не ограничиваясь одной командой без ключей.
Собрали жалобы по клиентам и заметили закономерность: задержка воспроизводилась не у всех, а у пользователей конкретных провайдеров и мобильных операторов — как правило, у тех, кто реально предоставляет IPv6-связность конечным абонентам. У пользователей с чисто IPv4-подключением (или с IPv6, отключённым на роутере) страница открывалась мгновенно.
Это уже прямой намёк на протокольную природу проблемы. Если бы дело было в самом сервере или в приложении, географическая и «операторская» избирательность не имела бы смысла — сервер не знает и не должен знать, какой стек использует клиент, разница должна была бы проявляться у всех одинаково. А она зависела именно от того, добирается ли конкретный клиент до сервера по IPv6 или сразу идёт по IPv4.
Дальше — контрольная проверка на своей стороне: посмотреть, какие DNS-записи вообще существуют у домена.
dig example.com A +short
203.0.113.10
dig example.com AAAA +short
2001:db8:1234::10
Обе записи были на месте: и A (IPv4), и AAAA (IPv6). Это само по себе не проблема — dual-stack конфигурация обычна и правильна, когда обе записи реально рабочие. Вопрос был в другом: рабочая ли вторая.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверcurl -4 против curl -6: момент истины
Самая показательная проверка в этом расследовании — сравнить поведение по двум протоколам явно, без надежды на то, что ОС сама выберет правильный:
curl -4 -o /dev/null -s -w "IPv4 connect: %{time_connect}s total: %{time_total}s\n" https://example.com
IPv4 connect: 0.014s total: 0.087s
curl -6 -o /dev/null -s -w "IPv6 connect: %{time_connect}s total: %{time_total}s\n" https://example.com
IPv6 connect: 3.001s total: 3.312s
Вот и всё расследование в одной паре команд. По IPv4 сервер отвечал за миллисекунды. По IPv6 соединение упиралось в таймаут где-то около тех же трёх секунд, на которые жаловались пользователи. Проверка curl -6 -v показала, что TCP SYN на IPv6-адрес уходит, но ответа (SYN-ACK) не приходит — пакеты либо терялись на маршруте, либо гасились на сервере файрволом, не настроенным на IPv6-трафик.
Дополнительно стоило сравнить трассировку по обоим протоколам — traceroute example.com против traceroute -6 example.com (или mtr -6) — чтобы увидеть, на каком хопе теряется IPv6-путь: уходит ли он вообще за пределы локальной сети, или обрывается сразу на аплинке. В этом случае трасса по IPv6 просто не доходила до сервера — пакеты уходили в пустоту где-то на промежуточном узле.
Почему именно Happy Eyeballs и почему именно секунды, а не мгновенно
Механизм, который создавал ощущение «зависания сайта» у части пользователей, называется Happy Eyeballs и описан в RFC 8305. Смысл в следующем: когда у хоста есть и A, и AAAA записи, современные операционные системы и браузеры по умолчанию предпочитают IPv6 (он считается более «современным» и часто действительно быстрее при прочих равных). Клиент запускает попытку подключения по IPv6 и, не дожидаясь однозначного провала, параллельно или с небольшой задержкой запускает попытку по IPv4 — и использует то соединение, которое установится первым.
Ключевое слово — «не дожидаясь однозначного провала». Быстрый и однозначный отказ (например, ICMP "destination unreachable" или мгновенный RST) Happy Eyeballs обрабатывает почти незаметно для пользователя — соединение сразу переключается на IPv4. Проблема начинается тогда, когда IPv6-путь не отказывает явно, а просто молчит: пакет уходит, ответа нет, и клиенту приходится ждать таймаута TCP-подключения по IPv6, прежде чем откатиться на IPv4 (или — в зависимости от реализации — прежде чем вообще начать пробовать IPv4 отдельно, если параллельный запуск не сработал так, как задумано).
Именно поэтому в реальности речь идёт не о теоретических ~250 мс, заложенных RFC 8305 для гонки между попытками, а о заметно больших паузах: конкретное число зависит от реализации стека, настроек таймаутов у клиента и от того, насколько именно «сломан» IPv6-путь (полный blackhole без ответа обычно даёт более долгий таймаут, чем явный отказ). Разные ОС, браузеры и мобильные приложения реализуют Happy Eyeballs не одинаково агрессивно — где-то откат происходит быстрее, где-то медленнее, поэтому одна и та же поломка IPv6 у разных пользователей ощущается по-разному.
Отдельно стоит подчеркнуть то, что и сбило расследование в начале: ping example.com без ключей на большинстве систем по умолчанию использует IPv4, если у хоста есть обе записи (поведение зависит от конкретной ОС и её настроек getaddrinfo, но de facto так происходит часто). Именно поэтому ping был идеальным — он просто не проверял тот путь, который был сломан. Чтобы увидеть проблему через ping, нужно было явно запустить ping -6 example.com — и вот тогда стало бы видно то же самое молчание, что и в curl -6.
Корень проблемы: AAAA-запись обогнала настройку IPv6
Когда стали разбираться, откуда вообще взялась AAAA-запись, оказалось, что она была добавлена раньше — панелью хостинг-провайдера или скриптом первоначальной настройки сервера, автоматически, вместе с выдачей IPv6-адреса. Технически сервер действительно получил IPv6-адрес на интерфейс, и адрес даже отвечал на ICMP echo в некоторых внутренних проверках. Но полноценно рабочим IPv6-стек не был по паре причин, которые встречаются в паре или по отдельности:
- Файрвол настроен только для IPv4. Правила
iptablesбыли аккуратно прописаны, а параллельные правила вip6tables(или вnftablesс отдельной таблицей для inet6) — нет. В результате входящие TCP SYN на 443/80 по IPv6 просто отбрасывались политикойDROPпо умолчанию, либо вообще не имели явногоACCEPTи попадали под запрещающее правило в конце цепочки. - Маршрутизация по IPv6 неполная. У сервера был локальный IPv6-адрес, но отсутствовал корректный маршрут по умолчанию (
default via ... dev eth0для inet6) либо провайдер выдавал IPv6-связность нестабильно — часть транзитных сетей на пути реально плохо маршрутизировала IPv6-трафик до этого сервера, несмотря на то что сам адрес был технически доступен из части точек интернета.
И тот, и другой сценарий дают одинаковую внешнюю картину: адрес существует, он даже иногда отвечает на что-то простое, но реальный прикладной трафик (TCP-хэндшейк на веб-порт) до него не доходит стабильно. Это состояние хуже, чем полное отсутствие IPv6: если бы AAAA-записи не было вовсе, все клиенты сразу шли бы по IPv4, и никакой задержки не возникало бы вообще. Наполовину рабочий IPv6 создаёт иллюзию «поддержки протокола», а на деле маскирует деградацию под таймауты, которые тяжело диагностировать без явного разделения по протоколам.
Разбор смежной темы про переход на IPv6 и что при этом обычно ломается стоит держать под рукой при любой миграции: список типичных мест, где включённый на бумаге IPv6 на практике не настроен до конца, обычно один и тот же — firewall, маршрутизация, а иногда и логирование/geo-фильтрация, которая писалась в расчёте только на IPv4-адреса.
Что делать: починить IPv6 до конца или убрать AAAA-запись
Из разбора следует простой практический вывод, и вариантов ровно два — третьего в виде «оставить как есть и подождать» быть не должно, потому что «наполовину работающий» протокол активно вредит части аудитории прямо сейчас.
Вариант 1. Довести IPv6 до полностью рабочего состояния. Если IPv6-адрес и AAAA-запись нужны (например, часть аудитории реально приходит по IPv6 напрямую, без Happy Eyeballs-отката, или это требование инфраструктуры), тогда нужно закрыть весь список, а не только DNS:
# Проверить, что default route для IPv6 вообще есть
ip -6 route show default
# Проверить правила ip6tables так же внимательно, как ipv4
ip6tables -L -n -v --line-numbers
# Явно разрешить нужные порты по IPv6, если правил не было
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
И обязательно — тест именно по IPv6 отдельно, не полагаясь на то, что «раз IPv4 работает, значит и всё остальное настроено»:
curl -6 -v https://example.com
ping6 example.com
mtr -6 example.com
Вариант 2. Убрать AAAA-запись, пока IPv6 не готов. Если приводить IPv6 в порядок прямо сейчас нет ресурсов или необходимости, честнее и безопаснее для пользователей — временно удалить AAAA-запись из зоны и оставить только A. Это возвращает всех клиентов на гарантированно рабочий IPv4-путь без всякого Happy Eyeballs-таймаута:
; было
example.com. IN A 203.0.113.10
example.com. IN AAAA 2001:db8:1234::10
; стало (закомментировано/удалено до готовности)
example.com. IN A 203.0.113.10
Это не «откат назад» и не признание поражения — это осознанный выбор в пользу предсказуемого поведения для всех пользователей, вместо гибридного состояния, которое работает быстро для одной части аудитории и на несколько секунд дольше для другой без явной причины на первый взгляд.
Мониторинг: проверяйте IPv4 и IPv6 по отдельности
Главный практический урок этого случая — нельзя полагаться на то, что операционная система сама разберётся и выберет рабочий протокол, а обычный ping или базовый uptime-чек по умолчанию покажут полную картину. Если у домена есть обе записи, обе должны проверяться отдельно и постоянно, особенно сразу после любых изменений в сетевой конфигурации сервера (смена провайдера, добавление интерфейса, правка firewall, миграция на новый хостинг).
Минимальный набор проверок, которые стоит гонять по расписанию, а не только руками при жалобе:
# HTTP-доступность по обоим протоколам
curl -4 -sf -o /dev/null -w "%{http_code} %{time_total}\n" https://example.com
curl -6 -sf -o /dev/null -w "%{http_code} %{time_total}\n" https://example.com
# ICMP по обоим протоколам
ping -c 3 example.com
ping6 -c 3 example.com
Хорошая практика — завести оба чека (по A и по AAAA-адресу отдельно) как два разных пункта в системе мониторинга доступности, а не один общий по имени хоста, чтобы алерт по одному протоколу не терялся на фоне того, что второй продолжает отвечать нормально. По теме отдельного построения таких проверок пригодится обзор инструментов для мониторинга доступности сайта — принцип разделения проверок по протоколу туда добавляется без особых сложностей, важно только не забыть явно указать версию IP в конфигурации чека, а не полагаться на автоматический выбор.
Похожая по симптомам, но другая по природе проблема разобрана в статье про потерю пакетов, которую не видно в ping: там задержки тоже не ловятся стандартной проверкой, но причина в частичной деградации канала, а не в выборе протокола. Для IPv6-кейса решает явное разделение -4/-6 в проверках, для скрытой потери пакетов — более длительные серии и статистика по jitter, а не единичные пинги.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если ping отвечает быстро, значит с сетью точно всё в порядке?
Нет. Обычный ping без ключей чаще всего идёт по IPv4 и ничего не говорит о состоянии IPv6-пути, если у домена есть отдельная AAAA-запись. Проверяйте оба протокола явно: ping и ping6, либо curl -4 и curl -6.
Зачем вообще нужна AAAA-запись, если она создаёт такие проблемы?
Сама по себе AAAA-запись не проблема — проблема в том, что IPv6-стек за ней был настроен не полностью (файрвол, маршрутизация). Полностью рабочий dual-stack не создаёт таких задержек, потому что и IPv4, и IPv6 путь одинаково доступны.
Можно ли отключить Happy Eyeballs у клиентов, чтобы не ждать таймаут?
Технически некоторые клиенты и приложения позволяют форсировать протокол (например, тот же curl -4), но это решение на стороне каждого отдельного клиента, а не на стороне сервера. Управлять массово поведением чужих браузеров и ОС нельзя — чинить нужно источник, то есть сам сервер и его AAAA-запись.
Сколько именно ждёт клиент при сломанном IPv6 — всегда одни и те же секунды?
Нет фиксированного числа: точная длительность зависит от реализации Happy Eyeballs в конкретной ОС/браузере/приложении, от настроек TCP-таймаутов и от того, отвечает ли сломанный путь молчанием (обычно дольше) или явным отказом (обычно почти мгновенно). Ориентируйтесь на диапазон, а не на конкретную цифру, и всегда проверяйте свой случай curl-ом с обоими флагами.
Что безопаснее в долгосрочной перспективе — держать AAAA без полной настройки IPv6 или убрать её совсем?
Убрать, пока IPv6 не настроен целиком. Наполовину рабочий протокол хуже его отсутствия: часть пользователей платит скрытым таймаутом за то, что формально выглядит как «поддержка современного стандарта».
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →