Хабрахабр

[Перевод] Сети Kubernetes: Ingress

Сегодня мы публикуем перевод третьей части руководства по работе с сетями в Kubernetes. В первой части речь шла о подах, во второй — о сервисах, а сегодня мы поговорим о балансировке нагрузки и о ресурсах Kubernetes вида Ingress.

Маршрутизация — это не балансировка нагрузки

В предыдущем материале этой серии была рассмотрена конфигурация, состоящая из пары подов и сервиса, которому был назначен IP-адрес, называемый «IP кластера». Именно на этот адрес отправлялись запросы, предназначенные для подов. Здесь мы продолжим работу над нашей учебной системой, начав там, где закончили в прошлый раз. Вспомним о том, что кластерный IP-адрес сервиса, 10.3.241.152, принадлежит диапазону IP-адресов, отличному от того, который используется в сети подов, и от того, который используется в сети, в которой находятся узлы. Я назвал сеть, заданную этим адресным пространством, «сервисной сетью», хотя она вряд ли достойна особого названия, так как к этой сети не подключено никаких устройств, и её адресное пространство, фактически, полностью состоит из правил маршрутизации. Ранее было продемонстрировано то, как эта сеть реализована на основе компонента Kubernetes, который называется kube-proxy и взаимодействует с модулем ядра Linux netfilter для перехвата и перенаправления трафика, отправленного на IP кластера, на работающий под.

Схема сети

Так, соединения и запросы работают на 4 уровне модели OSI (tcp) или на 7 уровне (http, rpc, и так далее). До сих пор мы говорили о «соединениях» и «запросах» и даже использовали сложное для толкования понятие «трафик», но для того, чтобы понять особенности работы механизма Kubernetes Ingress, нам нужно использовать более точные термины. Все маршрутизаторы, включая netfilter, принимают решения, более или менее основываясь только на информации, содержащейся в пакете. Правила netfilter представляют собой правила маршрутизации, они работают с IP-пакетами на третьем уровне. Поэтому для того чтобы описать это поведение в терминах третьего уровня модели OSI, нужно сказать, что каждый пакет, предназначенный для сервиса, находящегося по адресу 10. В целом, их интересует то, откуда идёт пакет и куда он направляется. 241. 3. 152:80, который прибывает на интерфейс узла eth0, обрабатывается средствами netfilter, и, в соответствии с правилами, заданными для нашего сервиса, перенаправляется на IP-адрес работоспособного пода.

В результате эти внешние клиенты будут обращаться к IP-адресу и порту кластера, так как они являются «точкой доступа» ко всем тем механизмам, о которых мы до сих пор говорили. Кажется совершенно очевидным то, что любой механизм, который мы используем для того, чтобы позволить внешним клиентам обращаться к подам, должен использовать эту же инфраструктуру маршрутизации. Однако совсем неочевидно то, как сделать так, чтобы всё это работало. Они позволяют нам не беспокоиться о том, где именно выполняется под в некий определённый момент времени.

Ничто за пределами кластера не знает о том, что делать с адресами из диапазона, к которому принадлежит этот адрес. Кластерный IP сервиса достижим лишь с Ethernet-интерфейса узла. Как можно перенаправить трафик с общедоступного IP-адреса на адрес, который достижим только в том случае, если пакет уже пришёл в узел?

Если это сделать, можно будет обнаружить нечто такое, что, на первый взгляд, может показаться необычным: правила для сервиса не ограничены конкретной сетью-источником. Если мы попытаемся найти решение этой проблемы, то одним из дел, которые можно сделать в процессе поиска решения, будет исследование правил netfilter с использованием утилиты iptables. 3. Это значит, что любые пакеты, сгенерированные где угодно, которые прибывают на Ethernet-интерфейс узла и имеют адрес назначения 10. 152:80, будут признаны как соответствующие правилу и будут перенаправлены поду. 241. Можем ли мы просто дать клиентам IP кластера, возможно, привязав его к подходящему доменному имени, и затем настроить маршрут, позволяющий организовать доставку этих пакетов одному из узлов?

Внешний клиент и кластер

Клиенты обращаются к кластерному IP, пакеты следуют по маршруту, ведущему их к узлу, а затем перенаправляются поду. Если настроить всё именно так, то такая конструкция окажется рабочей. Первая заключается в том, что узлы, вообще-то, понятие эфемерное, они не особенно отличаются в этом плане от подов. В этот момент вам может показаться, что таким решением можно и ограничиться, но оно страдает некоторыми серьёзными проблемами. Маршрутизаторы работают на третьем уровне модели OSI и пакеты не могут отличить нормально работающие сервисы от тех, что работают неправильно. Они, конечно, немного ближе к материальному миру, чем поды, но они могут мигрировать на новые виртуальные машины, кластеры могут масштабироваться вверх или вниз, и так далее. Если узел оказывается недостижимым, маршрут станет нерабочим и будет таким оставаться, в большинстве случаев, немало времени. Они ожидают, что следующий переход в маршруте будет доступен и окажется стабильным. Если даже маршрут окажется устойчивым к сбоям, то такая схема приведёт к тому, что весь внешний трафик пойдёт через единственный узел, что, вероятно, оптимальным не является.

И, на самом деле, нет надёжного способа сделать это, используя лишь маршрутизацию, без неких средств активного управления маршрутизатором. Как бы мы ни приводили клиентский трафик в систему, нам надо это делать так, чтобы это не зависело бы от состояния любого отдельно взятого узла кластера. Расширение сферы ответственности Kubernetes до управления внешним маршрутизатором, вероятно, не имело особого смысла для архитекторов системы, особенно учитывая то, что у нас уже есть доказавшие свою полезность инструменты для распределения клиентского трафика между множеством серверов. Собственно говоря, именно подобную роль, роль системы управления, kube-proxy играет по отношению к netfilter. Для того чтобы понять то, как в точности это происходит, нам нужно подняться с третьего уровня OSI и снова поговорить о соединениях. Они называются балансировщиками нагрузки, и неудивительно то, что именно они являются действительно надёжно работающим решением для Kubernetes Ingress.

По вышеописанным причинам мы не может просто создать стабильный статический маршрут между шлюзом-маршрутизатором и узлами, используя сеть, основанную на сервисах (IP кластера). Для того чтобы использовать балансировщик нагрузки для распределения клиентского трафика между узлами кластера, нам нужен общедоступный IP-адрес, к которому могут подключаться клиенты, а так же нам нужны адреса самих узлов, на которые балансировщик нагрузки может перенаправлять запросы.

100. Среди других адресов, с которыми можно работать, можно отметить лишь адреса сети, к которой подключены Ethernet-интерфейсы узлов, то есть, в этом примере, 10. 0/24. 0. Но если клиент хочет подключиться к нашему сервису по порту 80, то мы не можем просто отправить пакеты на этот порт сетевых интерфейсов узлов. Маршрутизатор уже знает о том, как направлять пакеты на эти интерфейсы, и соединения, отправленные с балансировщика нагрузки на маршрутизатор, попадут туда, куда должны попасть.

Балансировщик нагрузки, неудачная попытка обращения к порту 80 сетевого интерфейса узла

А именно, речь идёт о том, что нет процесса, ожидающего соединений по адресу 10. Причина, по которой сделать этого нельзя, совершенно очевидна. 0. 100. Они реагируют лишь на кластерный IP сети, основанной на сервисах, то есть на адрес 10. 3:80 (а если и есть, то это, точно, не тот процесс), и правила netfilter, которые, как мы надеялись, перехватят запрос и направят его поду, не сработают при таком адресе назначения. 241. 3. В результате эти пакеты, при их прибытии, не могут быть доставлены по адресу назначения, и ядро выдаст ответ ECONNREFUSED. 152:80. Для того чтобы решить эту проблему, можно создать между этими сетями мост. Это ставит нас в запутанное положение: с сетью, на перенаправление пакетов в которую настроен 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

100. На предыдущем рисунке клиент подключается к балансировщику нагрузки через общедоступный IP-адрес, балансировщик нагрузки выбирает узел и подключается к нему по адресу 10. 3:32213, kube-proxy принимает это соединение и перенаправляет его сервису, доступному по кластерному IP 10. 0. 241. 3. Здесь запрос успешно обрабатывается по правилам, заданным netfilter, и перенаправляется серверному поду на адрес 10. 152:80. 2. 0. Возможно, всё это может выглядеть немного сложным, и, в некоторой степени, так оно и есть, но нелегко придумать более простое решение, которое поддерживает все те замечательные возможности, которые дают нам поды и сети, основанные на сервисах. 2:8080.

Использование сервисов типа NodePort даёт клиентам доступ к сервисам с использованием нестандартного порта. Этот механизм, однако, не лишён собственных проблем. Но в некоторых сценариях, например, тогда, когда используется внешний балансировщик нагрузки платформы Google Cloud, может быть необходимым раскрыть NodePort клиентам. Часто проблемой это не является, так как балансировщик нагрузки может предоставить им обычный порт и скрыть NodePort от конечных пользователей. В большинстве случаев можно позволить Kubernetes выбирать номера портов случайным образом, но при необходимости их можно задавать самостоятельно. Надо отметить, что такие порты, кроме того, представляют собой ограниченные ресурсы, хотя 2768 портов, вероятно, достаточно даже для самых больших кластеров. Для того чтобы выяснить способы решения этих проблем, вы можете обратиться к этому материалу из документации Kubernetes. Ещё одна проблема заключается в некоторых ограничениях, касающихся сохранения IP-адресов источников в запросах.

Однако сами по себе они не представляют нам готового решения. Порты NodePorts — это фундаментальный механизм, благодаря которому весь внешний трафик попадает в кластер Kubernetes. По вышеозначенным причинам перед кластером, являются ли клиенты внутренними или внешними сущностями, находящимися в общедоступной сети, всегда требуется иметь некий балансировщик нагрузки.

Давайте это обсудим. Архитекторы платформы, понимая это, предоставили два способа задания конфигурации балансировщика нагрузки из самой платформы Kubernetes.

Сервисы типа LoadBalancer и ресурсы вида Ingress

Сервисы типа LoadBalancer и ресурсы вида Ingress представляют собой одни из наиболее сложно устроенных механизмов Kubernetes. Мы, однако, не будем тратить на них слишком много времени, так как их использование не приводит к коренным изменениям во всём том, о чём мы до сих пор говорили. Весь внешний трафик, как и прежде, входит в кластер через NodePort.

На самом деле, в определённых ситуациях, в таких, как запуск кластера на обычных серверах или в домашних условиях, именно так и поступают. Архитекторы могли бы здесь и остановиться, позволив тем, кто создаёт кластеры, заботиться лишь об общедоступных IP-адресах и балансировщиках нагрузки. Но в окружениях, которые поддерживают конфигурации сетевых ресурсов, управляемые через API, Kubernetes позволяет настроить всё, что нужно, в одном месте.

Такие сервисы имеют все возможности сервисов типа NodePort, и, в дополнение к этому, обладают возможностью создавать полные пути для входящего трафика, исходя из предположения о том, что кластер запущен в окружениях наподобие GCP или AWS, поддерживающих конфигурирование сетевых ресурсов через API. Первый подход к решению этой задачи, самый простой, заключается в использовании сервисов Kubernetes типа LoadBalancer.

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-адрес меняться не будет.

Такой сервис нельзя настроить на расшифровку HTTPS-трафика. У сервисов типа LoadBalancer есть некоторые ограничения. Эти ограничения привели к появлению в Kubernetes 1. Нельзя создавать виртуальные хосты или настраивать маршрутизацию, основанную на путях, поэтому нельзя, строя конфигурации, применимые на практике, использовать единственный балансировщик нагрузки со множеством сервисов. особого ресурса, предназначенного для конфигурирования балансировщиков нагрузки. 1. Сервисы типа LoadBalancer нацелены на расширение возможностей отдельно взятого сервиса по поддержке внешних клиентов. Речь идёт о ресурсе вида Ingress. API Ingress поддерживает расшифровку TLS-трафика, виртуальные хосты, маршрутизацию, основанную на путях. В отличие от них, ресурсы Ingress — это особые ресурсы, которые позволяют гибко настраивать балансировщики нагрузки. С помощью этого API балансировщик нагрузки легко можно настроить на работу с несколькими бэкенд-сервисами.

Реализация этого ресурса следует обычному шаблону Kubernetes: имеется тип ресурса и контроллер для управления этим типом. API ресурсов вида Ingress слишком велико для того, чтобы обсуждать тут его особенности, оно, кроме того, не особенно влияет на то, как Ingress-ресурсы работают на сетевом уровне. Вот как может выглядеть описание ресурса 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 для обработки входящего трафика, поэтому я решил, что о них стоит, хотя бы кратко, упомянуть.

Это — свойство контейнера (объявленное в структуре ContainerPort). Для начала поговорим о HostPort. Тут нет механизмов проксирования, и порт открывается лишь на узлах, на которых выполняется контейнер. Когда в него записан некий номер порта, это приводит к открытию этого порта на узле и к его перенаправлению прямо к контейнеру. Например, я однажды использовал это для создания Elasticsearch-кластера, установив HostPort в значение 9200 и указав столько реплик, сколько было узлов. В ранние дни платформы, до появления в ней механизмов DaemonSet и StatefulSet, HostPort представлял собой хитрость, позволяющую обеспечить запуск лишь одного контейнера некоего типа на любом узле. Теперь подобный ход воспринимается как жуткий хак, и если только вы не занимаетесь реализацией системного компонента Kubernetes, вам вряд ли когда-нибудь понадобится пользоваться свойством контейнера HostPort.

Если это свойство установлено в значение true, оно работает так же как аргумент -network=host команды docker run. Свойство пода NostNetwork, пожалуй, в контексте Kubernetes выглядит ещё более странно, чем HostPort. То есть у всех из них будет прямой доступ к интерфейсу eth0 и к открытым портам. Оно приводит к тому, что все контейнеры в поде будут использовать сетевое пространство имён узла. Если же вам эта возможность понадобится, то вы, весьма вероятно, занимаетесь разработкой платформы Kubernetes, и не нуждаетесь в каких-либо советах. Не думаю, что перебором будет совет никогда и ни при каких обстоятельствах этим не пользоваться.

Итоги

Сегодня мы поговорили о маршрутизации в сетях Kubernetes, о балансировке нагрузки, об особенностях использования сервисов разных типов и ресурсов вида Ingress. Надеемся, серия статей, которая завершается этим материалом, помогла вам лучше разобраться в тонкостях Kubernetes.

Уважаемые читатели! Пользуетесь ли вы ресурсами Ingress?

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»