Пример, показывающий, как kube-proxy играет с iptables

Сетевой прокси Kubernetes (он же kube-proxy) - это демон, работающий на каждом узле. Он в основном отражает службы, определенные в кластере, и управляет правилами балансировки нагрузки запросов к внутренним модулям службы.

Краткий пример. Допустим, у нас есть несколько модулей микросервиса API, работающих в нашем кластере, и эти реплики предоставляются сервисом. Когда запрос достигает виртуального IP-адреса службы, как он пересылается в один из базовых модулей? Ну ... просто используя правила, созданные kube-proxy. Хорошо, под капотом все не так просто, но мы получаем общую картину.

kube-proxy может работать в трех разных режимах:

  • iptables (режим по умолчанию)
  • ipvs
  • пользовательское пространство («устаревший» режим, больше не рекомендуется)

Хотя режим iptables вполне подходит для многих кластеров и рабочих нагрузок, ipvs может быть полезен, когда количество служб важно (более 1000). Действительно, поскольку правила iptables читаются последовательно, их использование может повлиять на производительность маршрутизации, если в кластере существует много сервисов.

Tigera (создатель и разработчик сетевого решения Calico) подробно описывает разницу между режимами iptables и ipvs в этой замечательной статье. Он также обеспечивает высокоуровневое сравнение этих двух режимов.

В этой статье мы сосредоточимся на режиме iptables (следующая статья будет посвящена режиму ipvs) и таким образом проиллюстрируем, как kube-proxy определяет правила iptables.

Для этого мы будем использовать двухузловой кластер, который я только что создал с помощью kubeadm:

$ kubectl get nodes
NAME    STATUS   ROLES                  AGE   VERSION
k8s-1   Ready    control-plane,master   57s   v1.20.0
k8s-2   Ready    <none>                 41s   v1.20.0

В следующей части мы развернем простое приложение, используя ресурс развертывания, и представим его через службу типа NodePort.

Развернуть образец приложения

Сначала мы создаем развертывание на основе призрачного образа (ghost - это бесплатная платформа для ведения блогов с открытым исходным кодом) и указываем две реплики:

$ kubectl create deploy ghost --image=ghost --replicas=2

Затем мы открываем модули с помощью службы типа NodePort:

$ kubectl expose deploy/ghost \
  --port 80 \
  --target-port 2368 \
  --type NodePort

Затем мы получаем информацию, относящуюся к только что созданному сервису:

$ kubectl describe svc ghost
Name:                     ghost
Namespace:                default
Labels:                   app=ghost
Annotations:              <none>
Selector:                 app=ghost
Type:                     NodePort
IP:                       10.98.141.188
Port:                     <unset>  80/TCP
TargetPort:               2368/TCP
NodePort:                 <unset>  30966/TCP
Endpoints:                10.44.0.3:2368,10.44.0.4:2368
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

Здесь важно отметить следующие важные моменты:

  • Виртуальный IP-адрес (VIP), выделенный службе: 10.98.141.188.
  • NodePort 30966 был выделен службе. Через этот порт мы можем получить доступ к призрачному веб-интерфейсу с любого узла кластера (узлы кластера, используемые в этом примере, имеют IP-адреса 192.168.64.35 и 192.168.64.36):

  • Свойство Endpoints показывает IP-адреса модулей, предоставляемых службой. Другими словами, каждый запрос, который попадает на виртуальный IP-адрес службы (10.98.141.188) на порт 80, будет перенаправлен на один из IP-адресов базовых модулей (10.44.0.3 или 10.44.0.4) на порту 2368 циклическим способом. .

Примечание. Конечные точки также можно получить с помощью стандартной команды kubectl get:

$ kubectl get endpoints
NAME         ENDPOINTS                       AGE
ghost        10.44.0.3:2368,10.44.0.4:2368   4m
kubernetes   192.168.64.35:6443              6m

Далее мы более подробно рассмотрим правила iptables, которые kube-proxy создал для маршрутизации запросов к внутренним модулям.

Подробнее об iptables

Каждый раз, когда создается / удаляется служба или изменяются конечные точки (например, если количество базовых модулей изменяется из-за масштабирования соответствующего развертывания), kube-proxy отвечает за обновление правил iptables на каждом узле кластера. Давайте посмотрим, как это делается с помощью службы, которую мы определили ранее.

Поскольку существует довольно много цепочек iptables, мы рассмотрим только основные из них, участвующие в маршрутизации запроса, который поступает на NodePort и перенаправляется в один из базовых модулей:

Во-первых, цепочка KUBE-NODEPORTS учитывает пакеты, поступающие на обслуживание типа NodePort.

Таким образом, каждый пакет, поступающий на порт 30966, сначала обрабатывается KUBE-MARK-MASQ, который маркирует пакет с помощью 0x4000.

Примечание. Эта отметка учитывается только тогда, когда для балансировки нагрузки используется режим IPVS (и, следовательно, iptables не выполняет этого).

Затем пакет обрабатывается цепочкой KUBE-SVC-4XJR4EADNBDQKTKS (на которую ссылается цепочка KUBE-NODEPORTS выше). Если мы присмотримся к нему поближе, то увидим две дополнительные цепочки iptables:

  • KUBE-SEP-7I5NH52DVZSA3QHP
  • KUBE-SEP-PSCUKR75MU2ULAEX

Из-за оператора statistic mode random probability 0.5 каждый пакет, попадающий в цепочку KUBE-SVC-4XJR4EADNBDQKTKS:

  • Обрабатывается KUBE-SEP-7I5NH52DVZSA3QHP 50% времени и, таким образом, игнорируется в 50% случаев.
  • Обрабатывается KUBE-SEP-PSCUKR75MU2ULAEX в 50% случаев (когда игнорируется первой цепочкой).

Если мы проверим обе цепочки, мы увидим, что они определяют маршрутизацию к одному из базовых модулей, запускающих приложение-призрак:

Затем, используя пару цепочек iptables, мы можем понять путь запроса от момента, когда он доходит до порта узла, до нижележащего модуля. Довольно круто, правда?

Заключение

В этой быстрой статье я надеюсь, что мне удалось прояснить, как kube-proxy работает под капотом при использовании режима iptables (по умолчанию). В следующей статье мы увидим, как выполняется маршрутизация, когда балансировка нагрузки выполняется в режиме ipvs.