Поды – это не постоянные сущности кластера. В любой момент времени вы можете добавить новый под или удалить не нужный. При перемещении пода между нодами кластера, под создается на новой ноде и удаляется на старой. При этом у пода меняется IP адрес. Именно поэтому не стоит обращаться к поду по ip адресу.
В Kubernetes, для доступа к поду (наборам подов) используются сервисы (service). Сервис – это абстракция, определяющая набор подов и политику доступа к ним.
Предположим, что в системе есть приложение, производящее обработку запросов. Приложение работает без сохранения состояния и поэтому может легко горизонтально масштабироваться. Для обработки потока запросов нам потребовалось несколько экземпляров приложения (подов) и нам необходимо распределить нагрузку между ними.
Если бы мы не использовали Kubernetes, нам бы пришлось ставить перед приложениями какую-то программу, занимающуюся распределением запросов. Например nginx. И каждый раз при изменении количества приложений, при переезде приложения с одного сервера на другой перенастраивать nginx.
В Kubernetes заботу о распределении нагрузки или доступа к группе приложений ложится на сервис. При определении сервиса обычно достаточно указать селектор, определяющий выбор подов, на которые будут пересылаться запросы. Так же существует возможность определения сервисов без селекторов, но об этом мы поговорим позднее.
Утилита kubectl может создать сервис используя аргументы командной строки. Но мы будем пользоваться старыми добрыми yaml файлами.
--- apiVersion: v1 kind: Service metadata: name: service-name spec: selector: app: pod-selector ports: - protocol: TCP port: 80 targetPort: 8080
В приведённом выше примере создаётся сервис с именем service-name. Kubernetes присваивает сервису ip адрес и добавляет его данные в DNS сервер. После этого вы можете обратиться к сервису по его имени. В нашем случае сервис принимает запросы на 80-м порту и, если мы находимся в том же неймспейсе, мы можем обратиться к нему http://service-name. Если в другом, то с указанием неймспейса: http://service-name.namespace.svc. Подробно о работе DNS в Kubernetes можно почитать тут: https://fatalex.cifro.netdoku.php/devops/k8s/k8s_dns
Приходящие запросы сервис будет пересылать на порт 8080 подам с метками (label) app: selector. Если в системе будет несколько подов с таким селектором, сервис будет перераспределять запросы между ними. По умолчанию по алгоритму round robbin.
В качестве значения targetPort можно использовать имена портов. Конечно, если вы его описали при определении пода. Это удобно, если вы ссылаетесь на поды, у которых определены разные номера портов, но под одним именем.
Рассмотрим пример. Deployment для сервера Tomcat.
--- apiVersion: apps/v1 kind: Deployment metadata: name: tomcat labels: app: tomcat spec: replicas: 2 revisionHistoryLimit: 3 selector: matchLabels: app: tomcat template: metadata: labels: app: tomcat spec: containers: - name: tomcat image: tomcat:10-jdk15 imagePullPolicy: IfNotPresent resources: requests: cpu: "0.2" memory: "200Mi" limits: cpu: "0.5" memory: "500Mi" ports: - containerPort: 8080 name: tomcat protocol: TCP
Файл 01- deployment . yaml .
В deployment указано наличие двух реплик (подов) приложения. Объявляется порт приложения 8080, с названием tomcat. Приложению присваивается метка: app: tomcat.
Сервис, предоставляющий доступы к этим подам можно объявить следующим образом:
--- apiVersion: v1 kind: Service metadata: name: tomcat-main spec: selector: app: tomcat ports: - protocol: TCP port: 80 targetPort: tomcat
Файл 02- service . yaml
В разделе selector мы указываем метку приложения, на которые мы будем ссылаться. В разделе ports говорим, что к сервису нужно обращаться на 80-й порт. Запрос будет переслан приложению на порт, имеющий имя tomcat.
Применим файлы манифеста.
kubectl apply -f 01-deployment.yaml kubectl apply -f 02-service.yaml
Примечание. Кластер kuberntes устанавливался со следующими параметрами сети:
networking: dnsDomain: cluster.local podSubnet: 10.234.0.0/18 serviceSubnet: 10.233.0.0/18
Посмотрим информацию о запущенных подах. Вывод программы немного обрезан.
# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP tomcat-56db4566fd-8ffjg 1/1 Running 0 2m40s 10.234.9.2 tomcat-56db4566fd-xdctz 1/1 Running 0 2m40s 10.234.8.196
Если обратиться к любому поду напрямую, мы увидим ответ сервера tomcat.
# curl 10.234.9.2:8080 <!doctype html> Тут было много символов <h3>Apache Tomcat/10.0.0-M10</h3></body></html>
Cписок сервисов в namespace default:
# kubectl get svc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR kubernetes ClusterIP 10.233.0.1 <none> 443/TCP 173m <none> tomcat-main ClusterIP 10.233.47.171 <none> 80/TCP 114s app=tomcat
Посмотрим подробнее на сервис.
# kubectl get svc tomcat-main -o yaml
apiVersion: v1 kind: Service … name: tomcat-main namespace: default … spec: clusterIP: 10.233.47.171 ports: - port: 80 protocol: TCP targetPort: tomcat selector: app: tomcat sessionAffinity: None type: ClusterIP
Система выделила сервису виртуальный ip адрес:
clusterIP: 10.233.47.171
Адрес виртуальный. Это значит, что вы не найдете на машине Linux интерфейса с таким ip. Следует отметить, что за выдачу ip для сервисов отвечает kube-proxy, а не модуль IPAM драйвера сети Kubernetes.
Обратите внимание, что ip адреса сервисам выдаются из диапазона, определенного при помощи serviceSubnet, заданном при установке кластера.
В принципе, ip адрес можно определять сразу в файле манифеста. Если это ip занят – то вы получите сообщение об ошибке. Но нет особого смысла заниматься явным указанием ip, поскольку к сервису мы всегда будем обращаться по имени, а не по ip.
Так же следует обратить внимание на:
type: ClusterIP
По умолчанию создаются сервисы типа ClusterIP.
После определения сервиса мы можем обратиться к нему и получить ответ одного из tomcat. Да, тут мы обращаемся к сервису по его ip адресу, но только потому, что делаем это в консоли машины Linux, а не внутри какого-либо пода Kubernetes. Linux машина не использует DNS Kubernetes и её клиент не может разрешать внутренние имена Kubernetes в ip адреса.
# curl 10.233.47.171 <!doctype html> Тут было много символов <h3>Apache Tomcat/10.0.0-M10</h3></body></html>
Каким образом происходит связь между сервисом и подами? «За кадром» остался еще один элемент – endpoint.
# kubectl get ep NAME ENDPOINTS AGE kubernetes 192.168.218.171:6443,192.168.218.172:6443,192.168.218.173:6443 174m tomcat-main 10.234.8.196:8080,10.234.9.2:8080 3m17s
Посмотрим на него подробнее.
# kubectl get ep tomcat-main -o yaml
apiVersion: v1 kind: Endpoints metadata: … name: tomcat-main namespace: default … subsets: - addresses: - ip: 10.234.8.196 nodeName: worker1.cluster.local targetRef: kind: Pod name: tomcat-56db4566fd-xdctz namespace: default … - ip: 10.234.9.2 nodeName: worker2.cluster.local targetRef: kind: Pod name: tomcat-56db4566fd-8ffjg namespace: default … ports: - port: 8080 protocol: TCP
В случае сервисов, использующих селектор, такие endpoints создаются автоматически. Система сама находит ip адреса подов, имеющих соответствующие метки, и формирует записи в endpoint.
Как такие связи будут выглядеть с точки зрения Linux машины, зависит от режима работы kube-proxy. Ведь именно он управляет сервисами. Обычно используют iptables или ipvs. С точки зрения быстродействия предпочтительнее использовать режим ipvs.
# ipvsadm -L -n | grep -A 2 10.233.47.171 TCP 10.233.47.171:80 rr -> 10.234.8.196:8080 Masq 1 0 0 -> 10.234.9.2:8080 Masq 1 0 0
По своей сути перед нами nat преобразование.
Сервисы без селекторов обычно используются для обращения за пределы кластера по ip адресу к какому либо приложению.
В качестве примера возьмем mail.ru. Конечно, лучше в качестве примера использовать какую либо базу данных, но у таковой базы не оказалось под рукой, поэтому будем тренироваться на кошках.
# host mail.ru mail.ru has address 217.69.139.200 mail.ru has address 94.100.180.200 mail.ru has address 217.69.139.202 mail.ru has address 94.100.180.201
Определение сервиса:
--- apiVersion: v1 kind: Service metadata: name: mail-ru spec: ports: - protocol: TCP port: 8080 targetPort: 80
Файл 03-service-mail-ru.yaml
Порт 8080 у сервиса был поставлен в качестве эксперимента. Можно оставить его значение равным 80.
Определение endpoint. Имя сервиса и endpoint должны совпадать.
--- apiVersion: v1 kind: Endpoints metadata: name: mail-ru subsets: - addresses: - ip: 217.69.139.200 - ip: 94.100.180.200 - ip: 217.69.139.202 - ip: 94.100.180.201 ports: - port: 80
Файл 04-endpoint-mail-ru.yaml
IP-адреса конечных точек не должны быть: loopback (127.0.0.0/8 для IPv4, :: 1/128 для IPv6) или локальными для ссылки (169.254.0.0/16 и 224.0.0.0/24 для IPv4, fe80 :: / 64 для IPv6).
IP-адреса конечных точек не могут быть IP-адресами других служб кластера kubernetes, потому что kube—proxy не поддерживает виртуальные IP-адреса в качестве пункта назначения.
# kubectl apply -f 03-service-mail-ru.yaml # kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.233.64.1 <none> 443/TCP 73m **mail-ru** ClusterIP **10.233.99.119** <none> 8080/TCP 21s ClusterIP 10.233.107.242 <none> 80/TCP 61m # kubectl apply -f 04-endpoint-mail-ru.yaml # kubectl get ep NAME ENDPOINTS AGE kubernetes 192.168.218.171:6443 73m **mail-ru** 217.69.139.200:80,94.100.180.200:80,217.69.139.202:80 + 1 more... 24s tomcat-main 10.233.8.193:8080,10.233.9.1:8080 61m # ipvsadm -L -n | grep -A 4 10.233.99.119 TCP 10.233.99.119:8080 rr -> 94.100.180.200:80 Masq 1 0 0 -> 94.100.180.201:80 Masq 1 0 0 -> 217.69.139.200:80 Masq 1 0 0 -> 217.69.139.202:80 Masq 1 0 0
Проверим, можем ли мы обращаться к новому сервису:
# curl 10.233.99.119:8080 <html> <head><title>301 Moved Permanently</title></head> <body bgcolor="white"> <center><h1>301 Moved Permanently</h1></center> <hr><center>nginx/1.14.1</center> </body> </html>
Работать с ip адресами не удобно. Запустим в кластере какой ни будь под и обратимся к сервисам по их именам.
# kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# curl mail-ru:8080 <html> <head><title>301 Moved Permanently</title></head> <body bgcolor="white"> <center><h1>301 Moved Permanently</h1></center> <hr><center>nginx/1.14.1</center> </body> </html> dnstools# curl tomcat-main <!doctype html> Тут было много символов <h3>Apache Tomcat/10.0.0-M10</h3></body></html>
У связки сервис + endpoint существовала большая проблема: все endpoint кластера – это объекты API. Все объекты API хранятся в базе данных etcd. И самое страшное, что записи объектов endpoints хранились в базе в одном ресурсе. При увеличении количества endpoints (читаем подов), ресурс приобретал просто огромные размеры. Представьте себе что в одной записи в БД хранятся ip адреса почти всех подов кластера!
Проблемы возникли в больших кластерах. При изменении endpoint, приходилось перечитывать весь объект из базы, что приводило к большому сетевому трафику.
Начиная с версии 1.17 в Kubernetes добавили EndpointSlice. При помощи него объект содержащий endpoint системы разбили на куски (слайсы). Теперь endpoints хранятся в EndpointSlice. Разбиение происходит автоматически. По умолчанию в одном slice хранится около 100 endpoints.
# kubectl get endpointslice NAME ADDRESSTYPE PORTS ENDPOINTS kubernetes IPv4 6443 192.168.218.171,192.168.218.172,192.168.218.173 mail-ru-fq7jd IPv4 80 217.69.139.200,217.69.139.202,94.100.180.200 + 1 more... tomcat-main-jf6dq IPv4 8080 10.234.9.2,10.234.8.196 # kubectl get endpointslice tomcat-main-jf6dq -o yaml
addressType: IPv4 apiVersion: discovery.k8s.io/v1beta1 kind: EndpointSlice metadata: ... generateName: tomcat-main- generation: 1 labels: endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io kubernetes.io/service-name: tomcat-main ... name: tomcat-main-jf6dq namespace: default ownerReferences: - apiVersion: v1 blockOwnerDeletion: true controller: true kind: Service name: tomcat-main ... endpoints: - addresses: - 10.234.9.2 conditions: ready: true targetRef: kind: Pod name: tomcat-56db4566fd-8ffjg namespace: default ... topology: kubernetes.io/hostname: worker2.cluster.local - addresses: - 10.234.8.196 conditions: ready: true targetRef: kind: Pod name: tomcat-56db4566fd-xdctz namespace: default ... topology: kubernetes.io/hostname: worker1.cluster.local ports: - name: "" port: 8080 protocol: TCP''
Вывод команд я немного сократил.
Как видно из описания, EndpointSlice содержит набор портов, которые применяются ко всем endpoints. Мы можем в одном сервисе определить несколько портов. В результате на один сервис может быть создано несколько EndpointSlices.
Выше мы запустили Nexus в нашем кластере. В файле манифеста было определено два сервиса. Нас интересует сервис под названием nexus.
--- apiVersion: v1 kind: Service metadata: name: nexus labels: app: nexus section: tools spec: ports: - name: http-main port: 8081 protocol: TCP targetPort: 8081 selector: app: nexus section: tools clusterIP: None
Обратите внимание на последнюю строку: clusterIP: None. Мы говорим системе, что у данного сервиса не будет виртуального ip адреса. Таким образом, мы определяем «безголовый» сервис.
Вот так он отображается в командной строке при запросе списка сервисов.
# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) ... nexus ClusterIP None <none> 8081/TCP ...
В системе для данного сервиса создаётся endpoint. Без этого никак.
# kubectl get endpoints NAME ENDPOINTS ... nexus 10.234.9.3:8081 ...
Что же такое headless сервис? Это просто запись А в системе DNS. Т.е. имя сервиса преобразуется не в виртуальный ip, его (ip) у нас нет, а сразу в ip пода.
Увеличим количество подов nexus:
# kubectl scale --replicas=2 statefulset nexus
Немного подождем. Записи в системе DNS появляются с небольшой задержкой.
Посмотрим список endpoints.
# kubectl get endpoints NAME ENDPOINTS ... nexus 10.234.8.199:8081,10.234.9.3:8081 ...
Запустим мой любимый dnstools и пошлем запрос к DNS кластера.
# kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# dig nexus.default.svc.cluster.local ... ;; ANSWER SECTION: nexus.default.svc.cluster.local. 5 IN A 10.234.9.3 nexus.default.svc.cluster.local. 5 IN A 10.234.8.199 ...
Мы видим, что у нас появились две записи типа А, указывающие на ip адреса подов. Поскольку не создаётся виртуальный ip сервиса, т.е. не создаётся NAT преобразование. Мы можем по имени сервиса напрямую обратиться к поду (подам) без затрат времени на лишние преобразования.
Таким образом, если работе вашего приложения противопоказаны NAT преобразования – используйте headless сервисы. Правда равномерного распределения нагрузки вы не получите, DNS не умеет этого делать. Но чем-то приходится жертвовать. Или haproxy? Нет?
В предыдущих примерах у нас немного неудачно происходила ссылка на mail.ru. В общем то была поставлена простая задача: обратиться внутри кластера к mail.ru не по его имени, а при помощи сервиса mail-ru. Что-то типа: http://mail-ru
Решим задачу правильно. Сначала удалите старый сервис.
# kubectl delete svc mail-ru
Применим манифест из файла 06-external-name.yaml.
apiVersion: v1 kind: Service metadata: name: mail-ru spec: type: ExternalName externalName: mail.ru
Сервис типа ExternalName добавляет запись типа CNAME во внутренний DNS сервер Kubernetes.
Что у нас получается?
# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) mail-ru ExternalName <none> mail.ru <none>
Для этого сервиса не создаётся endpoint. Поэтому сразу переходим к запросам к DNS.
# kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools dnstools# dig mail-ru.default.svc.cluster.local ;; ANSWER SECTION: mail-ru.default.svc.cluster.local. 5 IN CNAME mail.ru. mail.ru. 5 IN A 217.69.139.202 mail.ru. 5 IN A 94.100.180.200 mail.ru. 5 IN A 94.100.180.201 mail.ru. 5 IN A 217.69.139.200 dnstools# ping mail-ru PING mail-ru (94.100.180.200): 56 data bytes 64 bytes from 94.100.180.200: seq=0 ttl=55 time=47.865 ms 64 bytes from 94.100.180.200: seq=1 ttl=55 time=47.892 ms
Не советую вам экспериментировать с полем externalIPs. Внимательно прочитайте что будет написано ниже. Если решитесь на эксперименты — не подставляете ip адреса, на которых висит API сервер кластера. Используйте multimaster установку кластера, если ошибётесь, можно переключиться на другую ноду и исправить ошибку. И не говорите, что я вас не предупреждал.
Почти в любом определение сервиса можно добавить поле externalIPs, в котором можно указать ip машины кластера. При обращении на этот ip и указанный в сервисе порт, запрос будет переброшен на соответствующий сервис.
В качестве примера добавьте алиас на сетевой интерфейс. Например вот так:
# ifconfig ens33:ext 192.168.218.178
оставим один под nexus.
# kubectl scale --replicas=1 statefulset nexus
Применим манифест из файла 07-external-ip.yaml.
--- apiVersion: v1 kind: Service metadata: name: external-svc labels: app: nexus section: tools spec: ports: - name: http-main port: 8888 protocol: TCP targetPort: 8081 selector: app: nexus section: tools externalIPs: - 192.168.218.178
Посмотрим список сервисов.
# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) external-svc ClusterIP 10.233.55.223 **192.168.218.178** 8888/TCP
Посмотрим таблицу преобразований.
# ipvsadm -L -n | grep -A 1 8888 TCP 192.168.218.178:8888 rr -> 10.234.8.203:8081 Masq 1 0 0 -- TCP 10.233.55.223:8888 rr -> 10.234.8.203:8081 Masq 1 0
Попробуем обратиться по указанному ip.
# curl 192.168.218.178:8888
Тут будет большой ответ nexus
Очень похоже на NodePort. Но только похоже.
Сервисы типа NodePort открывают порт на каждой ноде кластера на сетевых интерфейсах хоста. Все запросы, приходящие на этот порт, будут пересылаться на endpoints, связанные с данным сервисом.
Диапазон портов, который можно использовать в NodePort — 30000-32767. Но его можно изменить при конфигурации кластера.
Пример сервиса типа NodePort.
--- apiVersion: v1 kind: Service metadata: labels: app: tomcat name: tomcat-nodeport spec: type: NodePort # externalTrafficPolicy: Local selector: app: tomcat ports: - protocol: TCP port: 80 targetPort: tomcat # nodePort: 30080
При описании сервиса необходимо явно указать его тип. Если не указать значение порта, при помощи параметра spec.ports.nodePort, порт присваивается автоматически из стандартного диапазона.
# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) tomcat-nodeport NodePort 10.233.43.49 <none> 80:31504/TC
Порт открывается на всех нодах кластера.
Посмотрим ноду из control plane кластера.
root@control1 # ipvsadm -L -n | grep -A 2 31504 TCP 169.254.25.10:31504 rr -> 10.234.8.205:8080 Masq 1 0 0 -> 10.234.9.8:8080 Masq 1 0 0 TCP 192.168.218.171:31504 rr -> 10.234.8.205:8080 Masq 1 0 0 -> 10.234.9.8:8080 Masq 1 0 0 -- TCP 10.234.56.192:31504 rr -> 10.234.8.205:8080 Masq 1 0 0 -> 10.234.9.8:8080 Masq 1 0 0 TCP 127.0.0.1:31504 rr -> 10.234.8.205:8080 Masq 1 0 0 -> 10.234.9.8:8080 Masq 1 0 0
На остальных нодах кластера ситуация с портом 31504 будет аналогичной.
Существует возможность открыть порт только на тех нодах, на которых запущены поды на которые ссылается данный сервер. Для этого в описании сервиса необходимо добавить параметр externalTrafficPolicy: Local.