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.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, потому что kubeproxy не поддерживает виртуальные 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.