Поды – это не постоянные сущности кластера. В любой момент времени вы можете добавить новый под или удалить не нужный. При перемещении пода между нодами кластера, под создается на новой ноде и удаляется на старой. При этом у пода меняется IP адрес. Именно поэтому не стоит обращаться к поду по ip адресу.
В Kubernetes, для доступа к поду (наборам подов) используются сервисы (service). Сервис – это абстракция, определяющая набор подов и политику доступа к ним.
Предположим, что в системе есть приложение, производящее обработку запросов. Приложение работает без сохранения состояния и поэтому может легко горизонтально масштабироваться. Для обработки потока запросов нам потребовалось несколько экземпляров приложения (подов) и нам необходимо распределить нагрузку между ними.
Если бы мы не использовали Kubernetes, нам бы пришлось ставить перед приложениями какую-то программу, занимающуюся распределением запросов. Например nginx. И каждый раз при изменении количества приложений, при переезде приложения с одного сервера на другой перенастраивать nginx.
В Kubernetes заботу о распределении нагрузки или доступа к группе приложений ложится на сервис. При определении сервиса обычно достаточно указать селектор, определяющий выбор подов, на которые будут пересылаться запросы. Так же существует возможность определения сервисов без селекторов, но об этом мы поговорим позднее.
ClusterIP
Утилита 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://www.kryukov.biz/kubernetes/dns-i-kubernetes/
Приходящие запросы сервис будет пересылать на порт 8080 подам с метками (label) app: selector. Если в системе будет несколько подов с таким селектором, сервис будет перераспределять запросы между ними. По умолчанию по алгоритму round robbin.
В качестве значения targetPort можно использовать имена портов. Конечно, если вы его описали при определении пода. Это удобно, если вы ссылаетесь на поды, у которых определены разные номера портов, но под одним именем.
Рассмотрим пример. Deployment для сервера Tomcat. (Файлы манифестов, используемые ниже можно найти у меня на github)
---
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
Каким образом происходит связь между сервисом и подами? «За кадром» остался еще один элемент – 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.kryukov.local
targetRef:
kind: Pod
name: tomcat-56db4566fd-xdctz
namespace: default
…
- ip: 10.234.9.2
nodeName: worker2.kryukov.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>
EndpointSlice
У связки сервис + 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.kryukov.local
- addresses:
- 10.234.8.196
conditions:
ready: true
targetRef:
kind: Pod
name: tomcat-56db4566fd-xdctz
namespace: default
...
topology:
kubernetes.io/hostname: worker1.kryukov.local
ports:
- name: ""
port: 8080
protocol: TCP
Вывод команд я немного сократил.
Как видно из описания, EndpointSlice содержит набор портов, которые применяются ко всем endpoints. Мы можем в одном сервисе определить несколько портов. В результате на один сервис может быть создано несколько EndpointSlices.
Headless Service
Выше мы запустили Nexus в нашем кластере. В файле манифеста было определено два сервиса (почему там два сервиса? Смотрите виде на моём yuotube канале: Kubernetes, StatefulSet, devops [01], 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? Нет?
ExternalName
В предыдущих примерах у нас немного неудачно происходила ссылка на 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
External IPs
Не советую вам экспериментировать с полем 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
Сервисы типа 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.