K8s Q A Services

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску


Services

Общая информация

Поды в кластере Kubernetes смертны - они создаются (рождаются), но когда под по какой-либо причине умирает, то он не воскресает. И хотя каждый под при создании получает свой собственный IP-адрес, этот адрес нельзя назвать постоянным и стабильным вследствие “смертности” подов.
К примеру, ReplicaSet может динамически изменять количество подов в кластере (путем масштабирования), при этом новые поды могут быть запущены на других узлах (нодах) кластера - в этом случае IP-адрес пода наверняка изменится.
Это порождает проблему: если некий набор подов (назовем их backends) предоставляют функционал другому набору подов (назовем их frontends) внутри кластера Kubernetes, то как frontend-поды узнают адреса backend-подов и взаимодействуют с ними?
Сервис в Kubernetes - это абстракция, определяющая логический набор подов и политику доступа к ним (иногда такой набор подов еще называют микросервисом). Как правило, этот набор подов определяется на основе меток (присваиваются в момент создания подов) и селекторов.
Например, backend - это 3 реплики подов, занимающихся обработкой изображений. Все три пода абсолютно идентичны с функциональной точки зрения (так как это реплики), поэтому frontend не должен беспокоиться на какой именно backend-под попадет запрос - это работа сервиса.
Простейший пример сервиса может выглядеть так:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

Согласно данной спецификации, будет создан объект “сервис” (service) с именем my-service, перенаправляющий запросы на порт 9376 каждого пода, у которого присутствует метка app=MyApp. Этому сервису также будет назначен отдельный IP-адрес.


Согласно указанному селектору, в кластере непрерывно будут проверяться поды на наличие метки app=MyApp, а результат такой проверки будет публиковаться (с помощью POST) в объект Endpoints с таким же именем - my-service.
Стоит отметить, что сервис может сопоставлять входящий порт (port:) с любым портом назначения (targetPort:). Если параметр targetPort не указан, то по умолчанию будет использоваться тот же порт, что и в параметре port.
Еще одна интересная особенность - targetPort может содержать строку с именем порта в подах (а сам номер порта, присвоенный этому имени, может быть разным для каждого пода).
Сервисы чаще всего используются для доступа к подам в кластере Kubernetes, но их также можно использовать и для доступа к другим типам бэкендов, например:

  • внешний кластер баз данных в production-окружении, локальная БД для тестового окружения; (КАК?)
  • совершенно иной сервис в другом неймспейсе или кластере;
  • часть бекендов, запущенных вне кластера Kubernetes (например, если вы только переносите свою инфраструктуру). (КАК?)

В любом из предложенных вариантов следует создать сервис без селекторов, например:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

и, так как селекторов нет, соотвествующий объект Endpoints не создастся автоматически. Его необходимо создать вручную, тем самым указав связь между сервисом и конечной точкой (бекендом), например так:

kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376

Примечание. IP-адрес в данном случае не может находиться в диапазонах 127.0.0.0/8, 169.254.0.0/16 и 224.0.0.0/24.
Для многих сервисов обычным делом является использование (открытие) нескольких портов. В таком случае, все что необходимо - присвоить имена всем портам, например:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377

В Kubernetes существует несколько вариантов предоставления доступа к сервисам, которые называются типы (ServiceTypes):

Типы Сервисов

(ТУТ НАДО ПОДРОБНЕЕ!!!!!!!)

ClusterIP

предоставляет доступ к сервису на внутреннем IP-адресе кластера (сервис доступен только внутри кластера). Тип ClusterIP используется по умолчанию;
Под капотом для каждого такого сервиса создаются правила в iptables которые и перенаправляют трафик - например (случайный сервис с живого кластера)

kubectl get svc -A -o wide --show-labels | grep tungstenfabric-operator-metrics
NAMESPACE NAME                            TYPE      CLUSTER-IP    EXTERNAL-IP PORT(S)   AGE  SELECTOR                     LABELS
tf        tungstenfabric-operator-metrics ClusterIP 10.232.100.45 <none>      8383/TCP  222d name=tungstenfabric-operator app.kubernetes.io/managed-by=Helm

Тут видно что

  • ClusterIP 10.232.100.45


kubectl -n tf get endpoints tungstenfabric-operator-metrics
NAME                              ENDPOINTS              AGE
tungstenfabric-operator-metrics   192.168.107.224:8383   222d
  • 192.168.107.224 - адрес единственного POD
kubectl  get pod -A -o wide | grep 192.168.107.224
tf                       tungstenfabric-operator-66b776cc76-c5xvt                2/2     Running     2 (24d ago)     37d     192.168.107.224   k8s-control-1   <none>           <none>

Этот под запущен на ноде k8s-control-1
На любой произвольной ноде можно видеть (iptables-save показывает правила из всех таблиц, в том числе nat, а не только filter)

iptables-save  | grep 10.232.100.45
-A KUBE-SERVICES -d 10.232.100.45/32 -p tcp -m comment --comment "tf/tungstenfabric-operator-metrics cluster IP" -m tcp --dport 8383 -j KUBE-SVC-I663JZWESJMFGRNU
-A KUBE-SVC-I663JZWESJMFGRNU ! -s 192.168.104.0/21 -d 10.232.100.45/32 -p tcp -m comment --comment "tf/tungstenfabric-operator-metrics cluster IP" -m tcp --dport 8383 -j KUBE-MARK-MASQ
  • -j KUBE-SVC-I663JZWESJMFGRNU - трафик перенаправлен в эту цепочку (-j ==jump)
iptables -t nat -nL KUBE-SVC-I663JZWESJMFGRNU -xv | column -t
Chain  KUBE-SVC-I663JZWESJMFGRNU  (1                         references)
pkts   bytes                      target                     prot         opt  in  out  source             destination
2      120                        KUBE-MARK-MASQ             tcp          --   *   *    !192.168.104.0/21  10.232.100.45  /*  tf/tungstenfabric-operator-metrics  cluster  IP                    */  tcp  dpt:8383
2      120                        KUBE-SEP-NGQNQI7MJHBZTPB6  all          --   *   *    0.0.0.0/0          0.0.0.0/0      /*  tf/tungstenfabric-operator-metrics  ->       192.168.107.224:8383  */

(про маркировку пакетов - ctmark = (ctmark AND NOT mask) XOR value ) https://itecnote.com/tecnote/linux-some-questions-about-set-xmark-in-iptables/

NodePort

предоставляет доступ к сервису на IP-адресе каждого узла (ноды) кластера, на статическом порту (из диапазона 30000-32767). Автоматически создастся и сервис типа ClusterIP, на который будут маршрутизироваться запросы с NodePort. Взаимодействовать с сервисом можно также из-за пределов кластера, используя в качестве адреса <NodeIP>:<NodePort>;

LoadBalancer

предоставляет доступ к сервису используя балансировщик (load balancer) облачного провайдера. При этом автоматически создаются сервисы типа NodePort и ClusterIP, на которые будут маршрутизироваться запросы с балансировщика;

ExternalName

особый случай - сопоставляет имя сервиса с содержимым поля externalName (например, foo.bar.example.com), возвращая CNAME запись. Никакого проксирования не происходит. (А через какой резолвер идет резолв?????)

Headless

Headless-сервис: это сервис, который не использует отдельный IP-адрес для маршрутизации запросов (ClusterIP: None). В этом случае под DNS-именем сервиса видны IP всех Pod, которые в этот сервис входят.
Headless-сервисы полезны, когда приложение само должно управлять тем, к какому Pod подключаться, например:

  • mongodb-клиент использует IP сервера, с которым он работает, для того, чтобы запросы для одного курсора шли на один хост (курсор «живёт» на mongos). В случае использования ClusterIP могут «теряться» курсоры даже для коротких запросов.
  • gRPC-клиенты держат по одному соединению с сервисами и сами управляют запросами, мультиплексируя запросы к одному серверу. В случае использования ClusterIP клиент может создать одно подключение и нагружать ровно один Pod сервера.

О проблемах можно читать тут: https://habr.com/ru/companies/joom/articles/563310/

Service и Endpoint - чуть подробнее

Еще одна интересная заметка которую я утащил сюда https://medium.com/@zzeett694/kubernetes-endpoints-9e3ee398c896

Как взаимодействуют сервисы и поды

Сервисы(services) кубрнетаса не взаимодействуют с Pod напрямую, помимо самого сервиса кубернетес создает так же абстракцию Endpoints которая является промежуточной в цепочке взаимодействия. Цепочка событий выглядит так:

Service -> Endpoints -> Pod

Для наглядности создадим один pod с простым web сервером возвращающим hostname pod-a и сервис с типом ClusterIP который с ним взаимодействует: Создание Deployment и сервиса для общения с ним

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: simple-web-app
        image: zet694/simple-web-app
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - name: http
          containerPort: 8888
---
apiVersion: v1
kind: Service
metadata:
  name: mysvc
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: http

Исследуем наш сервис:

$ kubectl describe svc mysvc 
Name:                  mysvc
Namespace:             default
Labels:                <none>
Annotations:           <none>
Selector:              app=myapp
Type:                  ClusterIP
IP Family Policy:      SingleStack
IP Families:           IPv4
IP:                    10.101.248.27
IPs:                   10.101.248.27
LoadBalancer Ingress:  localhost
Port:                  <unset>  80/TCP
TargetPort:            http/TCP
Endpoints:             10.1.1.99:8888
Session Affinity:      None
Events:                <none>

Значение поля endpoints содержит внутренний ip нашего единственного Pod который был выбран через селектор указанный в сервисе. Проверим это путем масштабирования количества Pod-ов до 3 экземпляров для этого используем команду:

$ kubectl scale deployment myapp --replicas=3
deployment.apps/myapp scaled

Теперь убедимся что колличество endpoints изменилось до 3 IP адрессов (ровно столько сколко Pod содержат label соотвествующий указаному в селекторе нашего сервиса):

$ kubectl describe svc mysvc
Name:                  mysvc
Namespace:             default
Labels:                <none>
Annotations:           <none>
Selector:              app=myapp
Type:                  ClusterIP
IP Family Policy:      SingleStack
IP Families:           IPv4
IP:                    10.101.248.27
IPs:                   10.101.248.27
LoadBalancer Ingress:  localhost
Port:                  <unset>  80/TCP
TargetPort:            http/TCP
Endpoints:             10.1.1.102:8888,10.1.1.103:8888,10.1.1.99:8888
Session Affinity:      None
Events:                <none>

Видим что количество Endpoints соответствует количеству Pod-ов, теперь убедимся что это действительно IP наших Pod-ов:

Проверяем внутренний IP наших Pod-ов

Вывод опущен - лень копировать картинку

Из вывода понятно что IP адреса Pod-ов совпадают с теми что указаны в Endpoints.Ресурс Endpoints является списком IP адрессов и портов предоставляющий доступ к службе, взаимодействовать с ним можно посредствам уже знакомых нам команд:

$ kubectl get endpoints
NAME         ENDPOINTS                                        AGE
mysvc        10.1.1.102:8888,10.1.1.103:8888,10.1.1.99:8888   48m

Из всего сказанного можно сделать вывод что при создании сервиса так же создается endpoints который автоматически добавляет и удаляет IP-адреса “подов” по меткам указанным в родительском сервисе.

Service без Selector

В случае если вы создаете сервис без указания селектора(selector) по которому должны будут обнаружены нужные поды, абстракция endpoint не создается так как кубернетес не знает куда направлять ваши запросы из сервиса, вы должны сами создать endpoints указав перечень необходимых IP адресов.
Внимание! Имя сервиса и имя endpoints должны совпадать.
Создадим сервис без указания selectors:

apiVersion: v1
kind: Service
metadata:
  name: mysvc-without-selector
spec:
  ports:
  - port: 80
    targetPort: http

Убедимся что сервис создан и не имеет endpoints:

$ kubectl describe svc mysvc-without-selector 
Name:              mysvc-without-selector
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.110.204.181
IPs:               10.110.204.181
Port:              <unset>  80/TCP
TargetPort:        http/TCP
Endpoints:         <none>
Session Affinity:  None
Events:            <none>

Теперь создадим endpoints с тем же именем что и наш сервис:

apiVersion: v1
kind: Endpoints
metadata:
  name: mysvc-without-selector
subsets:
  - addresses: 
    - ip: 185.199.110.153
    - ip: 77.88.55.80
    ports:
      - port: 80

Правила создания Endpoint:

  • Имя Endpoints и Service должны совпадать.
  • Должен быть указан один или более IP.
  • Должен быть указан порт.

Таким образом мы можем создать сервис с теми endpoints которые необходимы нам, например для взаимодействия с ресурсами который находятся за пределами кластера. Все запросы будут балансировать между указанным списком IP. Если вам необходимо использовать существующий сервис для взаимодействия с ресурсами за пределами кластера — удалите селектор и создайте endpoints с тем же именем.
Если точка назначения находится внутри вашего кластера, просто создайте селектор внутри службы и кубернетес будет автоматически определять IP. Главным преимуществом является то что адрес сервиса внутри приложения остается неизменным, но конечный пункт назначения вынесен на уровень инфрастуктуры.

Использование сервиса для внешних ресурсов

Указанный выше пример для использования сервиса как точку обмена с внешними сервисами имеющими DNS имя не рекомендуется использовать, существует более простой и понятный способ — ExternalName. Для этого достаточно создать сервис с типом ExternalName, указав в его поле DNS имя необходимого внешнего сервиса и порт. Вот небольшой пример:

Пример ExternalName сервиса

apiVersion: v1
kind: Service
metadata:
  name: external-svc
spec:
  type: ExternalName
  externalName: neverssl.com
  ports:
  - port: 80

Обрататите внимание что ExternalName сервис:

  • Реализуются исключительно на DNS уровне (создается CNAME запись).
  • Не имеет внутреннего IP.
  • Подключение к внешней службе происходит в обход внутреннего прокси.
  • CNAME указывает на полное имя, вместо IP адреса.