Пример, показывающий, как 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.