Раздел 3 · Kubernetes — глава 3.5
Путь запроса: от браузера до пода и обратно
Любимый вопрос всех собеседований на позицию выше джуна: «что происходит, когда пользователь вбивает адрес в браузере». Ценность его в том, что за пятнадцать минут можно проверить сразу всё — DNS, TCP, TLS, балансировку, Kubernetes, — и увидеть, где у человека дыры. Эта глава — детальный проход по всей цепочке, и заодно сборка остальных глав в одну картину.
Карта целиком
Сначала общий вид, чтобы было куда возвращаться. Дальше каждый шаг разбираем отдельно.
§1Браузер разбирает URL
Шаг, который обычно проскакивают, а зря — там есть пара вещей, которые реально ломают продакшен.
Браузер разбирает https://shop.example.com/cart?id=42 на схему, хост, порт (по умолчанию 443), путь и параметры. Дальше — проверки до всякой сети:
- Кеш HSTS. Если домен когда-то отдал заголовок
Strict-Transport-Security, браузер сам перепишетhttp://вhttps://, ещё не сделав ни одного запроса. Отсюда классическая паника «я убрал редирект на HTTPS, а браузер всё равно идёт на 443» — надо чиститьchrome://net-internals/#hsts. - Кеш ответов. Свежий ответ с подходящим
Cache-Controlотдастся сразу, без сети вообще. - Пул соединений. Если к этому хосту уже открыто живое соединение — новое устанавливать не будут, всё поедет по нему. Это тот самый keep-alive, который ломает L4-балансировку (см. главу 3.1, §7).
§2DNS: как имя превращается в адрес
Браузеру нужен IP. Резолв идёт по цепочке кешей, и на каждом уровне может закончиться досрочно.
Что здесь важно знать сверх схемы:
TTL решает, как быстро поедет переключение. Записи кешируются на всех уровнях ровно столько, сколько сказано в TTL. Поэтому перед плановой сменой адреса TTL заранее опускают до минуты-двух, а после переключения возвращают обратно. И поэтому же DNS — плохой инструмент аварийного переключения: часть клиентов будет ходить на старый адрес ещё долго, а некоторые резолверы и приложения игнорируют TTL и кешируют дольше положенного (JVM исторически этим славилась).
Отрицательные ответы тоже кешируются. Если запросить имя до того, как запись создана, NXDOMAIN осядет в кешах на время из SOA-записи. Отсюда «я же создал запись, почему не резолвится» — потому что кто-то по дороге запомнил, что её нет.
Ответом может быть не один адрес. Несколько A-записей — примитивная балансировка; CNAME на домен провайдера — типично для CDN и облачных балансировщиков; anycast-адрес — один и тот же IP анонсируется из десятков точек мира, и маршрутизация сама приводит тебя к ближайшей.
В корпоративной сети почти всегда split-horizon: внутренний DNS отдаёт на то же имя внутренний адрес, а внешний — публичный. Это то, чем правильно лечится проблема hairpin NAT из главы 1.2.
$ dig +short shop.example.com
203.0.113.10
$ dig shop.example.com +trace # пройти всю цепочку от корня
$ dig @8.8.8.8 shop.example.com # спросить конкретный резолвер
$ dig shop.example.com +noall +answer # посмотреть TTL в ответе
shop.example.com. 287 IN A 203.0.113.10
↑ столько секунд осталось жить в кеше резолвера
§3TCP-соединение до этого адреса
Дальше — то, что подробно разобрано в главе 1.1: SYN, SYN-ACK, ACK, согласование MSS и масштабирования окна. Здесь важно добавить контекст пути.
Пакет от домашнего компьютера почти наверняка пройдёт через NAT на домашнем роутере, а часто и через второй NAT у провайдера. Сервер увидит не адрес пользователя, а адрес последнего NAT — что уже на этом этапе означает, что «настоящий IP клиента» становится вопросом договорённостей, а не фактом.
Полезный ориентир по времени: хендшейк стоит один круговой путь. Внутри города — единицы миллисекунд, между континентами — 100–200 мс. Дальше пойдёт TLS, и это ещё круги. Отсюда вся индустриальная борьба за keep-alive, TLS 1.3 и переиспользование сессий: сами данные передаются быстро, дорого именно установление соединения.
§4TLS: как договариваются о шифровании
После установки TCP браузер начинает TLS-хендшейк. Полностью его знать не обязательно, но три вещи спрашивают постоянно.
SNI — поле в самом первом пакете, где браузер открытым текстом называет запрашиваемый домен. Без него сервер не смог бы понять, какой сертификат предъявлять, если на одном IP висят сотни сайтов. Практическое следствие: маршрутизация по домену возможна до расшифровки — на этом работает режим passthrough у роутеров и балансировка TLS-трафика без владения ключами.
ALPN — здесь же решается, будет это HTTP/2 или HTTP/1.1. Важно, потому что HTTP/2 — это одно мультиплексированное соединение, и все выводы про балансировку из главы 3.1 начинают действовать именно с этого момента.
Проверка сертификата — три независимых условия: подпись выдана доверенным центром (цепочка до корня в хранилище браузера), имя в SAN совпадает с запрошенным доменом, срок действия не истёк. Отсюда самые частые продовые ошибки: отдали сертификат без промежуточного звена цепочки (в браузере работает, потому что он его дотягивает сам, а curl и Java — нет), не обновили вовремя, поставили сертификат не на то имя.
В TLS 1.3 хендшейк укладывается в один круговой путь вместо двух, а при переиспользовании сессии данные можно отправить вообще сразу. Плюс к TLS 1.3 нельзя применить старые атаки на слабые шифры — их из протокола просто выкинули.
$ openssl s_client -connect shop.example.com:443 -servername shop.example.com
↑ без -servername получишь
дефолтный сертификат
subject=CN = shop.example.com
issuer=C = US, O = Let's Encrypt, CN = R3
Verify return code: 0 (ok)
ALPN protocol: h2
# когда до срока обновления
$ echo | openssl s_client -connect shop.example.com:443 2>/dev/null \
| openssl x509 -noout -dates
§5Внешний балансировщик
Адрес, полученный из DNS, почти никогда не принадлежит поду или даже узлу кластера. Это адрес входной точки, и вариантов немного.
| Что стоит на входе | Как работает |
|---|---|
| Облачный балансировщик (ELB, ALB, и аналоги) | Создаётся автоматически при Service типа LoadBalancer. Бывает L4 (проксирует TCP) или L7 (разбирает HTTP сам) |
| MetalLB в bare-metal | Раздаёт адреса из пула и анонсирует их — либо gratuitous ARP в L2-режиме, либо BGP-анонсами. Тот самый ARP из главы 1.2 |
| HAProxy / nginx на отдельных машинах | Классика для on-prem: пара машин с плавающим VIP через keepalived, за ними — узлы кластера. Твой вариант по опыту |
| CDN перед всем этим | Ближайшая точка присутствия отдаёт статику из кеша, динамику проксирует к origin |
Два вопроса, которые тут задают:
Где обрывается TLS? Три варианта: на CDN (тогда до origin идёт отдельное соединение), на внешнем балансировщике (внутрь кластера — открытый HTTP), или он проходит балансировщик насквозь и обрывается уже на ingress-контроллере. От этого зависит, где лежат сертификаты и кто занимается их обновлением.
Что происходит с IP клиента? L7-балансировщик добавляет заголовок X-Forwarded-For. L4 никаких заголовков добавить не может — он не понимает HTTP, — поэтому для сохранения адреса используют proxy protocol: перед данными соединения передаётся маленький блок с настоящими адресами. Обе стороны должны быть настроены согласованно, иначе получишь либо потерянный IP, либо мусор в начале запроса.
Грабли
Здоровье проверяется здесь же. Балансировщик опрашивает бэкенды и выбрасывает мёртвые — но только если проверка настроена осмысленно. Проверка TCP-соединением на порт узла почти бесполезна: NodePort открыт на всех узлах и отвечает, даже когда подов приложения нет ни одного. Проверять надо HTTP-эндпоинт, который отвечает 200 только при реально работающем приложении. Это же — обязательное условие для externalTrafficPolicy: Local (см. главу 1.2, §9): без корректной проверки трафик поедет на узлы без подов и умрёт там.
§6Вход в кластер
Балансировщик выбрал узел и отправил соединение на его NodePort — скажем, 30443. Дальше начинается то, что разобрано в главе 3.1.
Пакет попадает в сетевой стек узла, где kube-proxy заранее разложил правила: трафик на этот порт надо направить в один из подов ingress-контроллера. Делается DNAT, трансляция запоминается в conntrack, и все последующие пакеты соединения идут туда же без повторного прохода по правилам.
Если выбранный под роутера живёт на другом узле, пакет пойдёт туда через сеть подов — и вот тут добавится SNAT, чтобы ответ вернулся тем же путём. Именно поэтому по умолчанию (externalTrafficPolicy: Cluster) приложение видит адрес узла, а не клиента.
§7Ingress-контроллер: первое место, где читают HTTP
До этого момента всё было четвёртым уровнем — адреса и порты, содержимое никого не интересовало. Ingress-контроллер (nginx, Envoy, Traefik, HAProxy) — первое звено, которое разбирает HTTP.
Что он делает по порядку:
- Обрывает TLS, если это его роль: по SNI находит нужный секрет с сертификатом и расшифровывает.
- Читает заголовок
Hostи путь, ищет подходящее правило среди всех объектов Ingress в кластере. Правила подбираются по точности: конкретный хост важнее wildcard, точный путь важнее префикса. - Применяет всё, что навешано аннотациями: редиректы, переписывание пути, ограничение скорости, авторизацию, таймауты, CORS.
- Выбирает бэкенд и отправляет запрос дальше.
И вот здесь — ключевое отличие от всего, что было раньше, которое стоит подчеркнуть на собеседовании:
Главное про этот шаг
Ingress-контроллер балансирует отдельные HTTP-запросы, а не соединения. Он держит собственный пул соединений к бэкендам и раскидывает по ним запросы честным round robin (или least connections — настраивается). Это ровно то, чего не умеет Service на уровне ядра. Поэтому единственный правильный ответ на вопрос «как размазать нагрузку от gRPC или от клиента с keep-alive» — поставить L7-прокси: он разрывает одно длинное клиентское соединение на отдельные запросы к разным подам.
Ещё одна деталь, которая отличает знающего человека: ingress-nginx по умолчанию не ходит через ClusterIP. Он читает EndpointSlice сервиса и обращается напрямую к IP подов, полностью минуя kube-proxy и iptables. Это даёт ему собственную балансировку, свои таймауты и плавное выведение подов из ротации. Так что «трафик идёт через Service» на этом участке — неточность: Service тут используется только как справочник адресов.
§8Последний участок: до пода приложения
Роутер отправляет запрос на IP пода — например, 10.244.3.17:8080. Пакет уходит в сеть подов, и дальше всё зависит от CNI:
- Роутинг (Calico с BGP, Cilium нативно): узлы знают маршруты до подсетей друг друга, пакет идёт как обычный маршрутизируемый трафик. Быстро, но требует, чтобы сеть между узлами умела эти маршруты.
- Туннель (VXLAN, Geneve, IPIP): пакет упаковывается в ещё один пакет, летит между узлами по обычной сети, распаковывается на той стороне. Работает где угодно, но добавляет заголовки — и вычитает их из MTU. Все ужасы из главы 1.1 живут именно здесь.
На целевом узле пакет попадает в veth-интерфейс пода, оттуда — в его сетевой namespace, ядро по номеру порта находит слушающий сокет, кладёт данные в буфер. Приложение делает read() и наконец получает HTTP-запрос — тот самый, который пользователь отправил секунду назад.
Стоит проговорить, что внутри пода никакого дополнительного проксирования нет — если только не используется service mesh. С sidecar-прокси (Istio, linkerd) добавляется ещё один шаг: iptables внутри пода заворачивают весь трафик в локальный Envoy, и уже он передаёт запрос приложению на localhost. Отсюда и накладные расходы mesh, и его возможности — mTLS, ретраи, детальные метрики.
§9Ответ идёт обратно
Обратный путь короче, потому что все решения уже приняты и записаны:
- Приложение пишет ответ в тот же сокет — соединение уже установлено, ничего выбирать не надо.
- На узле conntrack разворачивает трансляции обратно: адрес источника снова становится тем, к которому клиент подключался. Без conntrack ответ пришёл бы с адресом пода, и клиент бы его отбросил как чужой.
- Ingress-контроллер добавляет свои заголовки, при необходимости сжимает тело, шифрует и отдаёт клиенту.
- Соединение остаётся открытым (keep-alive), и следующий запрос браузера поедет по нему — уже без DNS, TCP и TLS. Первый запрос стоит дорого, остальные почти бесплатны.
Тут же живёт красивый дополнительный вопрос: «а почему после выкатки часть пользователей получает 502?» Ответ — из главы 3.1: под уже начал завершаться, а правила и пулы соединений о нём ещё не знают. Разница только в том, что теперь ты можешь показать, в каком именно месте цепочки это происходит.
§10Отладка по шагам
Главная практическая ценность всей цепочки — умение делить её пополам. Не «сайт не работает», а последовательная проверка: до какого места доходит.
| Шаг | Чем проверить | Симптом провала |
|---|---|---|
| DNS | dig shop.example.com | Пусто, NXDOMAIN или старый адрес |
| Доступность адреса | curl -v --resolve shop.example.com:443:203.0.113.10 https://shop.example.com | Проверяет сервер в обход DNS — разделяет «сломан DNS» и «сломан сервер» |
| TCP | nc -zv 203.0.113.10 443 | Refused — никто не слушает; таймаут — дропает файрвол |
| TLS | openssl s_client -connect ... -servername ... | Истёк, не то имя, нет промежуточного сертификата |
| Балансировщик | Его логи и статус бэкендов | Все бэкенды помечены нездоровыми |
| Вход в кластер | curl -H "Host: shop.example.com" http://<node-ip>:30080 | Работает мимо балансировщика — значит, дело в нём |
| Ingress | kubectl logs -n ingress-nginx ..., kubectl describe ing | 404 — не подошло правило; 503 — нет живых бэкендов |
| Service | kubectl get endpointslice | Пусто — самая частая причина 503. Не совпал селектор или поды не Ready |
| Под | kubectl exec -it другой-под -- curl 10.244.3.17:8080 | Проверка в обход всего: живо ли само приложение |
| Приложение | kubectl logs, kubectl describe pod | Падает, не слушает нужный порт, не проходит пробы |
Спросят
«Пользователь получает 502, с чего начнёшь?» — Не перечисляй причины наугад, назови метод: «Делил бы пополам. 502 отдаёт прокси, значит, до прокси дошли — проблема правее. Смотрю логи ingress: если пишет "no live upstreams" — иду в endpoints сервиса, там чаще всего пусто из-за селектора или readiness. Если endpoints на месте — курлю под напрямую из соседнего пода: отвечает или нет. Если отвечает, а через ingress нет — дело в самом ingress или в том, что поды в этот момент завершаются и правила не успевают обновиться.» Метод ценится выше, чем список причин.
§11Как это рассказывать
Рассказ на пять минут, без воды. Ориентир — вот такой.
«Браузер разбирает URL и сначала смотрит свои кеши — HSTS может сразу переписать схему на https, а живое соединение к этому хосту вообще избавит от установки нового. Дальше нужен адрес: кеш браузера, кеш ОС, /etc/hosts, потом рекурсивный резолвер, который при промахе идёт от корневых серверов к зоне .com и к авторитативному серверу домена. Ответ кешируется по TTL на всех уровнях — поэтому DNS плохо подходит для аварийного переключения.
С полученным адресом устанавливается TCP — три пакета, попутно согласуются MSS и масштабирование окна. Потом TLS: в ClientHello открытым текстом едет SNI с именем хоста, по нему сервер понимает, какой сертификат предъявить, и там же в ALPN выбирается HTTP/2 или HTTP/1.1. Браузер проверяет цепочку до доверенного корня, совпадение имени и срок.
Адрес из DNS — это обычно внешний балансировщик: облачный, MetalLB или пара HAProxy с плавающим VIP. Он выбирает живой узел кластера по health check и передаёт соединение на NodePort. На узле kube-proxy делает DNAT на под ingress-контроллера и запоминает трансляцию в conntrack; если под роутера на другом узле, добавится ещё и SNAT — и вот из-за этого по умолчанию теряется IP клиента, что чинится либо externalTrafficPolicy: Local, либо proxy protocol.
Ingress-контроллер — первое место, где вообще читают HTTP. Он смотрит Host и путь, находит правило, применяет аннотации и отправляет запрос в бэкенд. Важно, что он балансирует именно запросы, а не соединения, и обращается напрямую к IP подов из endpoints, минуя ClusterIP. Дальше пакет идёт по сети подов — либо маршрутизацией, либо туннелем с уменьшенным MTU — попадает в veth пода, ядро находит сокет по порту, приложение читает запрос.
Обратно проще: conntrack разворачивает все трансляции, ответ уходит клиенту, соединение остаётся открытым, и следующий запрос идёт по нему без DNS, TCP и TLS. А когда что-то ломается, вся ценность этой цепочки в том, что её можно делить пополам: dig, curl --resolve, openssl s_client, логи ingress, endpoints сервиса, curl до пода напрямую.»