====== Kubernetes: Services ====== Поды – это не постоянные сущности кластера. В любой момент времени вы можете добавить новый под или удалить не нужный. При перемещении пода между нодами кластера, под создается на новой ноде и удаляется на старой. При этом у пода меняется 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]]. Если в другом, то с указанием неймспейса: [[http://service-name.namespace.svc|http://service-name.namespace.svc]]. //Подробно о работе DNS в Kubernetes можно почитать тут: [[:devops:k8s:k8s_dns|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 Тут было много символов

Apache Tomcat/10.0.0-M10

Cписок сервисов в namespace default: # kubectl get svc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR kubernetes ClusterIP 10.233.0.1 443/TCP 173m tomcat-main ClusterIP 10.233.47.171 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 Тут было много символов

Apache Tomcat/10.0.0-M10

===== 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.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 для ////IPv////4, :: 1/128 для ////IPv////6) или локальными для ссылки (169.254.0.0/16 и 224.0.0.0/24 для ////IPv////4, ////fe////80 :: / 64 для ////IPv////6).// //IP////-адреса конечных точек не могут быть ////IP////-адресами других служб кластера k////ubernetes////, потому что ////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 443/TCP 73m **mail-ru** ClusterIP **10.233.99.119** 8080/TCP 21s ClusterIP 10.233.107.242 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 301 Moved Permanently

301 Moved Permanently


nginx/1.14.1
Работать с 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 301 Moved Permanently

301 Moved Permanently


nginx/1.14.1
dnstools# curl tomcat-main Тут было много символов

Apache Tomcat/10.0.0-M10

===== 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.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. ===== Headless Service ===== Выше мы запустили 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 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|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 mail.ru Для этого сервиса не создаётся 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 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.