Сегодня мы публикуем перевод третьей части руководства по работе с сетями в Kubernetes. В первой части речь шла о подах, во второй — о сервисах, а сегодня мы поговорим о балансировке нагрузки и о ресурсах Kubernetes вида Ingress.
Маршрутизация — это не балансировка нагрузки
В предыдущем материале этой серии была рассмотрена конфигурация, состоящая из пары подов и сервиса, которому был назначен IP-адрес, называемый «IP кластера». Именно на этот адрес отправлялись запросы, предназначенные для подов. Здесь мы продолжим работу над нашей учебной системой, начав там, где закончили в прошлый раз. Вспомним о том, что кластерный IP-адрес сервиса, 10.3.241.152
, принадлежит диапазону IP-адресов, отличному от того, который используется в сети подов, и от того, который используется в сети, в которой находятся узлы. Я назвал сеть, заданную этим адресным пространством, «сервисной сетью», хотя она вряд ли достойна особого названия, так как к этой сети не подключено никаких устройств, и её адресное пространство, фактически, полностью состоит из правил маршрутизации. Ранее было продемонстрировано то, как эта сеть реализована на основе компонента Kubernetes, который называется kube-proxy и взаимодействует с модулем ядра Linux netfilter для перехвата и перенаправления трафика, отправленного на IP кластера, на работающий под.
Схема сети
До сих пор мы говорили о «соединениях» и «запросах» и даже использовали сложное для толкования понятие «трафик», но для того, чтобы понять особенности работы механизма Kubernetes Ingress, нам нужно использовать более точные термины. Так, соединения и запросы работают на 4 уровне модели OSI (tcp) или на 7 уровне (http, rpc, и так далее). Правила netfilter представляют собой правила маршрутизации, они работают с IP-пакетами на третьем уровне. Все маршрутизаторы, включая netfilter, принимают решения, более или менее основываясь только на информации, содержащейся в пакете. В целом, их интересует то, откуда идёт пакет и куда он направляется. Поэтому для того чтобы описать это поведение в терминах третьего уровня модели OSI, нужно сказать, что каждый пакет, предназначенный для сервиса, находящегося по адресу 10.3.241.152:80
, который прибывает на интерфейс узла eth0
, обрабатывается средствами netfilter, и, в соответствии с правилами, заданными для нашего сервиса, перенаправляется на IP-адрес работоспособного пода.
Кажется совершенно очевидным то, что любой механизм, который мы используем для того, чтобы позволить внешним клиентам обращаться к подам, должен использовать эту же инфраструктуру маршрутизации. В результате эти внешние клиенты будут обращаться к IP-адресу и порту кластера, так как они являются «точкой доступа» ко всем тем механизмам, о которых мы до сих пор говорили. Они позволяют нам не беспокоиться о том, где именно выполняется под в некий определённый момент времени. Однако совсем неочевидно то, как сделать так, чтобы всё это работало.
Кластерный IP сервиса достижим лишь с Ethernet-интерфейса узла. Ничто за пределами кластера не знает о том, что делать с адресами из диапазона, к которому принадлежит этот адрес. Как можно перенаправить трафик с общедоступного IP-адреса на адрес, который достижим только в том случае, если пакет уже пришёл в узел?
Если мы попытаемся найти решение этой проблемы, то одним из дел, которые можно сделать в процессе поиска решения, будет исследование правил netfilter с использованием утилиты iptables. Если это сделать, можно будет обнаружить нечто такое, что, на первый взгляд, может показаться необычным: правила для сервиса не ограничены конкретной сетью-источником. Это значит, что любые пакеты, сгенерированные где угодно, которые прибывают на Ethernet-интерфейс узла и имеют адрес назначения 10.3.241.152:80
, будут признаны как соответствующие правилу и будут перенаправлены поду. Можем ли мы просто дать клиентам IP кластера, возможно, привязав его к подходящему доменному имени, и затем настроить маршрут, позволяющий организовать доставку этих пакетов одному из узлов?
Внешний клиент и кластер
Если настроить всё именно так, то такая конструкция окажется рабочей. Клиенты обращаются к кластерному IP, пакеты следуют по маршруту, ведущему их к узлу, а затем перенаправляются поду. В этот момент вам может показаться, что таким решением можно и ограничиться, но оно страдает некоторыми серьёзными проблемами. Первая заключается в том, что узлы, вообще-то, понятие эфемерное, они не особенно отличаются в этом плане от подов. Они, конечно, немного ближе к материальному миру, чем поды, но они могут мигрировать на новые виртуальные машины, кластеры могут масштабироваться вверх или вниз, и так далее. Маршрутизаторы работают на третьем уровне модели OSI и пакеты не могут отличить нормально работающие сервисы от тех, что работают неправильно. Они ожидают, что следующий переход в маршруте будет доступен и окажется стабильным. Если узел оказывается недостижимым, маршрут станет нерабочим и будет таким оставаться, в большинстве случаев, немало времени. Если даже маршрут окажется устойчивым к сбоям, то такая схема приведёт к тому, что весь внешний трафик пойдёт через единственный узел, что, вероятно, оптимальным не является.
Как бы мы ни приводили клиентский трафик в систему, нам надо это делать так, чтобы это не зависело бы от состояния любого отдельно взятого узла кластера. И, на самом деле, нет надёжного способа сделать это, используя лишь маршрутизацию, без неких средств активного управления маршрутизатором. Собственно говоря, именно подобную роль, роль системы управления, kube-proxy играет по отношению к netfilter. Расширение сферы ответственности Kubernetes до управления внешним маршрутизатором, вероятно, не имело особого смысла для архитекторов системы, особенно учитывая то, что у нас уже есть доказавшие свою полезность инструменты для распределения клиентского трафика между множеством серверов. Они называются балансировщиками нагрузки, и неудивительно то, что именно они являются действительно надёжно работающим решением для Kubernetes Ingress. Для того чтобы понять то, как в точности это происходит, нам нужно подняться с третьего уровня OSI и снова поговорить о соединениях.
Для того чтобы использовать балансировщик нагрузки для распределения клиентского трафика между узлами кластера, нам нужен общедоступный IP-адрес, к которому могут подключаться клиенты, а так же нам нужны адреса самих узлов, на которые балансировщик нагрузки может перенаправлять запросы. По вышеописанным причинам мы не может просто создать стабильный статический маршрут между шлюзом-маршрутизатором и узлами, используя сеть, основанную на сервисах (IP кластера).
Среди других адресов, с которыми можно работать, можно отметить лишь адреса сети, к которой подключены Ethernet-интерфейсы узлов, то есть, в этом примере, 10.100.0.0/24
. Маршрутизатор уже знает о том, как направлять пакеты на эти интерфейсы, и соединения, отправленные с балансировщика нагрузки на маршрутизатор, попадут туда, куда должны попасть. Но если клиент хочет подключиться к нашему сервису по порту 80, то мы не можем просто отправить пакеты на этот порт сетевых интерфейсов узлов.
Балансировщик нагрузки, неудачная попытка обращения к порту 80 сетевого интерфейса узла
Причина, по которой сделать этого нельзя, совершенно очевидна. А именно, речь идёт о том, что нет процесса, ожидающего соединений по адресу 10.100.0.3:80
(а если и есть, то это, точно, не тот процесс), и правила netfilter, которые, как мы надеялись, перехватят запрос и направят его поду, не сработают при таком адресе назначения. Они реагируют лишь на кластерный IP сети, основанной на сервисах, то есть на адрес 10.3.241.152:80
. В результате эти пакеты, при их прибытии, не могут быть доставлены по адресу назначения, и ядро выдаст ответ ECONNREFUSED
. Это ставит нас в запутанное положение: с сетью, на перенаправление пакетов в которую настроен netfilter, непросто работать при перенаправлении данных со шлюза на узлы, а сеть, для которой легко настроить маршрутизацию, это не та сеть, в которую netfilter перенаправляет пакеты. Для того чтобы решить эту проблему, можно создать между этими сетями мост. Именно это и делается в Kubernetes с использованием сервиса типа NodePort.
Сервисы типа NodePort
Тому сервису, который мы, для примера, создали в предыдущем материале, не назначен тип, поэтому он принял тип, назначаемый по умолчанию — ClusterIP
. Есть ещё два типа сервисов, которые отличаются дополнительными возможностями, и тот из них, который нас сейчас интересует — это NodePort
. Вот пример описания сервиса такого типа:
kind: Service
apiVersion: v1
metadata:
name: service-test
spec:
type: NodePort
selector:
app: service_test_pod
ports:
- port: 80
targetPort: http
Сервисы типа NodePort
— это сервисы типа ClusterIP
, обладающие дополнительной возможностью: доступ к ним можно получить как по IP-адресу, назначенному узлу, так и по адресу, назначенному кластеру в сети сервисов. Достигается это довольно простым способом: когда Kubernetes создаёт сервис NodePort
, kube-proxy выделяет порт в диапазоне 30000-32767 и открывает этот порт на интерфейсе eth0
каждого узла (отсюда и название типа сервиса — NodePort
). Соединения, выполняемые к этому порту (мы будем называть такие порты NodePort
), перенаправляются на кластерный IP сервиса. Если мы создадим сервис, описанный выше, и выполним команду kubectl get svc service-test
, мы сможем увидеть порт, назначенный ему.
$ kubectl get svc service-test
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service-test 10.3.241.152 <none> 80:32213/TCP 1m
В данном случае сервису назначен NodePort 32213
. Это означает, что мы теперь можем подключаться к сервису через любой узел в нашем экспериментальном кластере по адресу 10.100.0.2:32213
или по адресу 10.100.0.3:32213
. При этом трафик будет перенаправляться сервису.
После того, как эта часть системы заняла своё место, у нас есть все фрагменты конвейера для балансировки нагрузки, создаваемой запросами клиентов ко всем узлам кластера.
Сервис NodePort
На предыдущем рисунке клиент подключается к балансировщику нагрузки через общедоступный IP-адрес, балансировщик нагрузки выбирает узел и подключается к нему по адресу 10.100.0.3:32213
, kube-proxy принимает это соединение и перенаправляет его сервису, доступному по кластерному IP 10.3.241.152:80
. Здесь запрос успешно обрабатывается по правилам, заданным netfilter, и перенаправляется серверному поду на адрес 10.0.2.2:8080
. Возможно, всё это может выглядеть немного сложным, и, в некоторой степени, так оно и есть, но нелегко придумать более простое решение, которое поддерживает все те замечательные возможности, которые дают нам поды и сети, основанные на сервисах.
Этот механизм, однако, не лишён собственных проблем. Использование сервисов типа NodePort
даёт клиентам доступ к сервисам с использованием нестандартного порта. Часто проблемой это не является, так как балансировщик нагрузки может предоставить им обычный порт и скрыть NodePort
от конечных пользователей. Но в некоторых сценариях, например, тогда, когда используется внешний балансировщик нагрузки платформы Google Cloud, может быть необходимым раскрыть NodePort
клиентам. Надо отметить, что такие порты, кроме того, представляют собой ограниченные ресурсы, хотя 2768 портов, вероятно, достаточно даже для самых больших кластеров. В большинстве случаев можно позволить Kubernetes выбирать номера портов случайным образом, но при необходимости их можно задавать самостоятельно. Ещё одна проблема заключается в некоторых ограничениях, касающихся сохранения IP-адресов источников в запросах. Для того чтобы выяснить способы решения этих проблем, вы можете обратиться к этому материалу из документации Kubernetes.
Порты NodePorts
— это фундаментальный механизм, благодаря которому весь внешний трафик попадает в кластер Kubernetes. Однако сами по себе они не представляют нам готового решения. По вышеозначенным причинам перед кластером, являются ли клиенты внутренними или внешними сущностями, находящимися в общедоступной сети, всегда требуется иметь некий балансировщик нагрузки.
Архитекторы платформы, понимая это, предоставили два способа задания конфигурации балансировщика нагрузки из самой платформы Kubernetes. Давайте это обсудим.
Сервисы типа LoadBalancer и ресурсы вида Ingress
Сервисы типа LoadBalancer
и ресурсы вида Ingress
представляют собой одни из наиболее сложно устроенных механизмов Kubernetes. Мы, однако, не будем тратить на них слишком много времени, так как их использование не приводит к коренным изменениям во всём том, о чём мы до сих пор говорили. Весь внешний трафик, как и прежде, входит в кластер через NodePort
.
Архитекторы могли бы здесь и остановиться, позволив тем, кто создаёт кластеры, заботиться лишь об общедоступных IP-адресах и балансировщиках нагрузки. На самом деле, в определённых ситуациях, в таких, как запуск кластера на обычных серверах или в домашних условиях, именно так и поступают. Но в окружениях, которые поддерживают конфигурации сетевых ресурсов, управляемые через API, Kubernetes позволяет настроить всё, что нужно, в одном месте.
Первый подход к решению этой задачи, самый простой, заключается в использовании сервисов Kubernetes типа LoadBalancer
. Такие сервисы имеют все возможности сервисов типа NodePort
, и, в дополнение к этому, обладают возможностью создавать полные пути для входящего трафика, исходя из предположения о том, что кластер запущен в окружениях наподобие GCP или AWS, поддерживающих конфигурирование сетевых ресурсов через API.
kind: Service
apiVersion: v1
metadata:
name: service-test
spec:
type: LoadBalancer
selector:
app: service_test_pod
ports:
- port: 80
targetPort: http
Если мы удалим и повторно создадим сервис из нашего примера в Google Kubernetes Engine, то мы, вскоре после этого, используя команду kubectl get svc service-test
, сможем удостовериться в факте назначения внешнего IP.
$ kubectl get svc service-test
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
openvpn 10.3.241.52 35.184.97.156 80:32213/TCP 5m
Выше сказано, что удостовериться в факте назначения внешнего IP-адреса мы сможем «вскоре», несмотря на то, что назначение внешнего IP может занять несколько минут, что неудивительно, учитывая количество ресурсов, которые нужно привести в работоспособное состояние. На платформе GCP, например, для этого нужно, чтобы система создала внешний IP-адрес, правила перенаправления трафика, целевой прокси-сервер, бэкенд-сервис, и, возможно, экземпляр группы. После выделения внешнего IP-адреса к сервису можно подключиться через этот адрес, назначить ему доменное имя и сообщить клиентам. До тех пор, пока сервис не будет уничтожен и создан повторно (для того, чтобы это сделать, редко когда находится достойный повод), IP-адрес меняться не будет.
У сервисов типа LoadBalancer
есть некоторые ограничения. Такой сервис нельзя настроить на расшифровку HTTPS-трафика. Нельзя создавать виртуальные хосты или настраивать маршрутизацию, основанную на путях, поэтому нельзя, строя конфигурации, применимые на практике, использовать единственный балансировщик нагрузки со множеством сервисов. Эти ограничения привели к появлению в Kubernetes 1.1. особого ресурса, предназначенного для конфигурирования балансировщиков нагрузки. Речь идёт о ресурсе вида Ingress. Сервисы типа LoadBalancer
нацелены на расширение возможностей отдельно взятого сервиса по поддержке внешних клиентов. В отличие от них, ресурсы Ingress
— это особые ресурсы, которые позволяют гибко настраивать балансировщики нагрузки. API Ingress поддерживает расшифровку TLS-трафика, виртуальные хосты, маршрутизацию, основанную на путях. С помощью этого API балансировщик нагрузки легко можно настроить на работу с несколькими бэкенд-сервисами.
API ресурсов вида Ingress
слишком велико для того, чтобы обсуждать тут его особенности, оно, кроме того, не особенно влияет на то, как Ingress-ресурсы работают на сетевом уровне. Реализация этого ресурса следует обычному шаблону Kubernetes: имеется тип ресурса и контроллер для управления этим типом. Ресурсом в данном случае является ресурс Ingress
, описывающий запросы к сетевым ресурсам. Вот как может выглядеть описание ресурса Ingress
.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
annotations:
kubernetes.io/ingress.class: "gce"
spec:
tls:
- secretName: my-ssl-secret
rules:
- host: testhost.com
http:
paths:
- path: /*
backend:
serviceName: service-test
servicePort: 80
Контроллер Ingress ответственен за выполнение этих запросов путём приведения в нужное состояние других ресурсов. При использовании Ingress создаются сервисы типа NodePort
, после чего контроллеру Ingress позволяют принимать решения о том, как направить трафик к узлам. Существует реализация контроллера Ingress для балансировщиков нагрузки GCE, для балансировщиков AWS, для популярных прокси-серверов, таких как nginx и haproxy. Обратите внимание на то, что смешивание ресурсов Ingress и сервисов типа LoadBalancer
может привести к небольшим проблемам в некоторых окружениях. С ними несложно справиться, но, в целом, лучше всего просто использовать Ingress даже для простых сервисов.
HostPort и HostNetwork
То о чём сейчас пойдёт речь, а именно, HostPort
и HostNetwork
, можно отнести скорее к разряду интересных редкостей, а не к полезным инструментам. На самом деле, я берусь утверждать, что в 99.99% случаев их использование можно считать анти-паттерном, и любая система, в которой они используются, должна в обязательном порядке подвергаться проверке её архитектуры.
Я думал, что о них и вовсе не стоит говорить, но они представляют собой нечто вроде тех средств, которые используются ресурсами Ingress для обработки входящего трафика, поэтому я решил, что о них стоит, хотя бы кратко, упомянуть.
Для начала поговорим о HostPort
. Это — свойство контейнера (объявленное в структуре ContainerPort
). Когда в него записан некий номер порта, это приводит к открытию этого порта на узле и к его перенаправлению прямо к контейнеру. Тут нет механизмов проксирования, и порт открывается лишь на узлах, на которых выполняется контейнер. В ранние дни платформы, до появления в ней механизмов DaemonSet и StatefulSet, HostPort
представлял собой хитрость, позволяющую обеспечить запуск лишь одного контейнера некоего типа на любом узле. Например, я однажды использовал это для создания Elasticsearch-кластера, установив HostPort
в значение 9200
и указав столько реплик, сколько было узлов. Теперь подобный ход воспринимается как жуткий хак, и если только вы не занимаетесь реализацией системного компонента Kubernetes, вам вряд ли когда-нибудь понадобится пользоваться свойством контейнера HostPort
.
Свойство пода NostNetwork
, пожалуй, в контексте Kubernetes выглядит ещё более странно, чем HostPort
. Если это свойство установлено в значение true
, оно работает так же как аргумент -network=host
команды docker run
. Оно приводит к тому, что все контейнеры в поде будут использовать сетевое пространство имён узла. То есть у всех из них будет прямой доступ к интерфейсу eth0
и к открытым портам. Не думаю, что перебором будет совет никогда и ни при каких обстоятельствах этим не пользоваться. Если же вам эта возможность понадобится, то вы, весьма вероятно, занимаетесь разработкой платформы Kubernetes, и не нуждаетесь в каких-либо советах.
Итоги
Сегодня мы поговорили о маршрутизации в сетях Kubernetes, о балансировке нагрузки, об особенностях использования сервисов разных типов и ресурсов вида Ingress. Надеемся, серия статей, которая завершается этим материалом, помогла вам лучше разобраться в тонкостях Kubernetes.
Уважаемые читатели! Пользуетесь ли вы ресурсами Ingress?
Автор: ru_vds