Хабрахабр

[Перевод] Эксперименты с kube-proxy и недоступностью узла в Kubernetes

Прим. перев.: В этой статье, написанной техническим консультантом и сертифицированным администратором Kubernetes из Великобритании — Daniele Polencic, — наглядно показывается и рассказывается о том, какую роль играет kube-proxy в доставке пользовательских запросов до подов и что происходит, когда на одном из узлов кластера возникают проблемы.

Узел может располагаться как на физической или виртуальной машине, так и в AWS EC2 или Google Compute Engine, а наличие множества таких площадок означает возможность эффективного запуска и масштабирования приложения. Код приложений, развёрнутых в Kubernetes, запускается на одном или более рабочих узлов. Например, если кластер состоит из трёх узлов и вы решаете отмасштабировать приложение на четыре реплики, Kubernetes равномерно распределит их среди узлов следующим образом:

Если один узел окажется недоступным, приложение продолжит работать на двух других. Такая архитектура хорошо справляется и с падениями. А в это время Kubernetes переназначит четвёртую реплику на другой (доступный) узел.

Например, уменьшим число реплик приложения до двух: Более того, даже если все узлы окажутся изолированными, они всё равно смогут обслуживать запросы.

Поскольку каждый узел может обслуживать приложение, как же третий (Node 3) узнает, что на нём не запущено приложение и ему следует перенаправить трафик на один из других узлов?

Его можно сравнить с администратором отеля, сидящим за стойкой регистрации. У Kubernetes есть бинарник kube-proxy, запускаемый на каждом узле и ответственный за маршрутизацию трафика на конкретный под. Kube-proxy принимает весь трафик, приходящий на узел, и пересылает на правильный под.

Но откуда kube-proxy знает, где расположены все поды?

Он не знает.

А kube-proxy проверяет эти правила и приводит их в действие. А вот знает обо всём главный (master) узел, который отвечает за создание списка всех правил маршрутизации. В простом сценарии, описанном выше, список правил сводится к следующему:

  • Первая реплика приложения доступна на узле 1 (Node 1).
  • Вторая реплика приложения доступна на узле 2 (Node 2).

Совершенно не важно, с какого узла приходит трафик: kube-proxy знает, куда необходимо перенаправить трафик в соответствии с этим списком правил.

Но что происходит, когда kube-proxy падает?

И что, если список правил пропадёт?

Что происходит, когда нет правил, куда направлять трафик?

У Manabu Sakai были такие же вопросы. И он решил разобраться.

Предположим, у вас кластер из двух узлов в GCP:

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
node1 Ready <none> 17h v1.8.8-gke.0
node2 Ready <none> 18h v1.8.8-gke.0

И вы разворачиваете приложение Manabu:

$ kubectl create -f https://raw.githubusercontent.com/manabusakai/k8s-hello-world/master/kubernetes/deployment.yml
$ kubectl create -f https://raw.githubusercontent.com/manabusakai/k8s-hello-world/master/kubernetes/service.yml

Это простое приложение, которое выводит на веб-странице имя хоста текущего пода.

Масштабируем его (Deployment) до десяти реплик:

$ kubectl scale --replicas 10 deployment/k8s-hello-world

Десять реплик равномерно распределяются по двум узлам (node1 и node2):

$ kubectl get pods
NAME READY STATUS NODE
k8s-hello-world-55f48f8c94-7shq5 1/1 Running node1
k8s-hello-world-55f48f8c94-9w5tj 1/1 Running node1
k8s-hello-world-55f48f8c94-cdc64 1/1 Running node2
k8s-hello-world-55f48f8c94-lkdvj 1/1 Running node2
k8s-hello-world-55f48f8c94-npkn6 1/1 Running node1
k8s-hello-world-55f48f8c94-ppsqk 1/1 Running node2
k8s-hello-world-55f48f8c94-sc9pf 1/1 Running node1
k8s-hello-world-55f48f8c94-tjg4n 1/1 Running node2
k8s-hello-world-55f48f8c94-vrkr9 1/1 Running node1
k8s-hello-world-55f48f8c94-xzvlc 1/1 Running node2

Создаётся Service для балансировки нагрузки от запросов по десяти репликам:

$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-hello-world NodePort 100.69.211.31 <none> 8080:30000/TCP 3h
kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 18h

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

Но как трафик маршрутизируется с порта 30000 до пода?

За настройку правил по направлению входящего трафика с порта 30000 до одного из десяти подов отвечает kube-proxy.

Попробуйте отправить запрос на порт 30000 одного из узлов:

$ curl <node ip>:30000

Примечание: IP-адрес узла можно получить командой kubectl get nodes -o wide.

via <hostname>. Приложение отвечает «Hello world!» и именем хоста контейнера, на котором оно запущено: Hello world!

Причина — kube-proxy работает как балансировщик нагрузки, проверяет маршрутизацию и распределяет трафик по десяти подам. Если повторно запрашивать тот же URL, иногда будет появляться такой же ответ, а иногда он будет меняться.

Что интересно, совершенно всё равно, к какому узлу вы обращаетесь: ответ будет приходить с любого пода — даже с тех, которые размещены на других узлах (не тех, к которым вы обратились).

Так и получается финальная схема прохождения запросов: Для окончательной конфигурации потребуется задействовать внешний балансировщик нагрузки, который распределит трафик по узлам (на порт 30000).

Внесём ясность во всю эту схему — резюмируем принцип её работы: То есть балансировщик нагрузки перенаправляет входящий трафик из интернета на один из двух узлов.

  1. Приходящий из интернета трафик направляется на основной балансировщик нагрузки.
  2. Этот балансировщик направляет трафик на порт 30000 одного из двух узлов.
  3. Правила, установленные kube-proxy, перенаправляют трафик с узла на под.
  4. Трафик попадает на под.

Вот и вся схема!

Настало время всё сломать

Теперь, когда мы знаем, как всё взаимодействует, давайте вернёмся к изначальному вопросу. Что случится, если мы изменим правила маршрутизации? Будет ли по-прежнему работать кластер? Будут ли поды обслуживать запросы?

Для последнего достаточно написать цикл, который будет каждую секунду выводить текущее время и делать запрос к приложению: Давайте удалять правила маршрутизации, а в отдельном терминале — мониторить приложение на время ответов и пропущенные запросы.

$ while sleep 1; do date +%X; curl -sS http://<your load balancer ip>/ | grep ^Hello; done

На выходе мы получим столбцы со временем и текстом ответа от пода:

10:14:41 Hello world! via k8s-hello-world-55f48f8c94-vrkr9
10:14:43 Hello world! via k8s-hello-world-55f48f8c94-tjg4n

Итак, давайте удалять правила маршрутизации с узла, но сначала разберёмся, как это делать.

Режимом по умолчанию со времён Kubernetes 1. kube-proxy может работать в трёх режимах: userspace, iptables и ipvs. (Прим. 2 является iptables. 8 и получил статус бета-версии в 1. перев.: Последний режим, ipvs, появился в релизе K8s 1. 9.)

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

Примечание: Учтите, что вызов iptables -F может оборвать SSH-подключение.

Если всё прошло по плану, вы увидите нечто подобное:

10:14:41 Hello world! via k8s-hello-world-55f48f8c94-xzvlc
10:14:43 Hello world! via k8s-hello-world-55f48f8c94-tjg4n
# в этот момент выполнена команда `iptables -F`
10:15:10 Hello world! via k8s-hello-world-55f48f8c94-vrkr9
10:15:11 Hello world! via k8s-hello-world-55f48f8c94-vrkr9

Как легко заметить, с момента сбрасывания правил iptables до следующего ответа потребовалось около 27 секунд (с 10:14:43 до 10:15:10).

Почему всё снова стало хорошо после 27 секунд? Что произошло за это время? Может быть, это просто совпадение?

Давайте сбросим правила ещё раз:

11:29:55 Hello world! via k8s-hello-world-55f48f8c94-xzvlc
11:29:56 Hello world! via k8s-hello-world-55f48f8c94-tjg4n
# в этот момент выполнена команда `iptables -F`
11:30:25 Hello world! via k8s-hello-world-55f48f8c94-npkn6
11:30:27 Hello world! via k8s-hello-world-55f48f8c94-vrkr9

Но кластер снова вернулся к работе. Теперь видна пауза в 29 секунд, с 11:29:56 до 11:30:25.

На узел приходят запросы даже без таблицы маршрутизации? Почему для ответа требуется 30 секунд?

В другом терминале запустите цикл, который делает запросы к приложению каждую секунду, но на сей раз — обращайтесь к узлу, а не балансировщику нагрузки. Можно посмотреть на то, что происходит на узле на протяжении этих 30 секунд.

$ while sleep 1; printf %"s\n" $(curl -sS http://<ip of the node>:30000); done

И снова сбросьте правила iptables. Получится такой лог:

Hello world! via k8s-hello-world-55f48f8c94-xzvlc
Hello world! via k8s-hello-world-55f48f8c94-tjg4n
# в этот момент выполнена команда `iptables -F`
curl: (28) Connection timed out after 10003 milliseconds
curl: (28) Connection timed out after 10004 milliseconds
Hello world! via k8s-hello-world-55f48f8c94-npkn6
Hello world! via k8s-hello-world-55f48f8c94-vrkr9

Неудивительно, что подключения к узлу заканчиваются таймаутом после сброса правил. Но интересно, что curl ждёт ответа по 10 секунд.

Это бы объяснило 30-секундную задержку, однако останется непонятным, почему узел готов принимать соединения после достаточно продолжительного ожидания. А что, если в предыдущем примере балансировщик нагрузки ждёт новых подключений?

Кто восстанавливает правила iptables? Так почему же трафик снова идёт через 30 секунд?

Перед тем, как сбрасывать правила iptables, можно их посмотреть:

$ iptables -L

Сбросьте правила и продолжайте выполнять эту команду — вы увидите, что правила восстанавливаются за несколько секунд.

Да! Это ты, kube-proxy? В официальной документации kube-proxy можно найти два интересных флага:

  • --iptables-sync-period — максимальный интервал, за который правила iptables будут обновлены (например: ‘5s’, ‘1m’, ‘2h22m’). Должен быть больше 0. По умолчанию — 30s;
  • --iptables-min-sync-period — минимальный интервал, за который правила iptables должны быть обновлены, когда происходят изменения в endpoints и services (например: ‘5s’, ‘1m’, ‘2h22m’). По умолчанию — 10s.

То есть: kube-proxy обновляет правила iptables каждые 10—30 секунд. Если мы сбросим правила iptables, для kube-proxy потребуется до 30 секунд, чтобы осознать это и восстановить их.

Это также объясняет, как таблицы маршрутизации попадают с главного (master) узла на рабочий (worker). Вот почему около 30 секунд ушло на то, чтобы узел снова заработал! Другими словами, каждый раз при добавлении или удалении пода главный узел переделывает список маршрутов, а kube-proxy регулярно синхронизирует правила с текущим узлом. Их регулярной синхронизацией занимается kube-proxy.

Итак, резюмируем, как Kubernetes и kube-proxy восстанавливаются, если кто-то испортил правила iptables на узле:

  1. Правила iptables были удалены с узла.
  2. Запрос направляется балансировщику нагрузки и маршрутизируется на узел.
  3. Узел не принимает входящие запросы, поэтому балансировщик ждёт.
  4. Через 30 секунд kube-proxy восстанавливает правила iptables.
  5. Узел снова может принимать трафик. Правила iptables перенаправляют запрос балансировщика на под.
  6. Под отвечает балансировщику нагрузки с итоговой задержкой в 30 секунд.

Ожидание 30 секунд может быть недопустимым для приложения. В таком случае стоит подумать об изменении стандартного интервала обновления в kube-proxy. Где эти настройки и как их изменить?

Документация по статичным подам предполагает, что kubelet проверяет содержимое определённого каталога и создаёт все ресурсы из него. На узле есть агент — kubelet, — и именно он отвечает за запуск kube-proxy как статичного пода на каждом узле.

Элементарный ls приоткрывает завесу тайны: Если взглянуть на процесс kubelet, работающий на узле, можно увидеть, что он запущен с флагом --pod-manifest-path=/etc/kubernetes/manifests.

$ ls -l /etc/kubernetes/manifests
total 4 -rw-r--r-- 1 root root 1398 Feb 24 08:08 kube-proxy.manifest

Что же содержится в этом kube-proxy.manifest?

apiVersion: v1
kind: Pod
metadata: name: kube-proxy
spec: hostNetwork: true containers: - name: kube-proxy image: gcr.io/google_containers/kube-proxy:v1.8.7-gke.1 command: - /bin/sh - -c -> echo -998 > /proc/$$$/oom_score_adj && exec kube-proxy --master=https://35.190.207.197 --kubeconfig=/var/lib/kube-proxy/kubeconfig --cluster-cidr=10.4.0.0/14 --resource-container="" --v=2 --feature-gates=ExperimentalCriticalPodAnnotation=true --iptables-sync-period=30s 1>>/var/log/kube-proxy.log 2>&1

Примечание: В целях упрощения здесь приведено неполное содержимое файла.

Как видно, для обновления правил iptables каждые 30 секунд используется опция --iptables-sync-period=30s. Тайна разгадана! Здесь же можно изменить минимальное и максимальное время обновления правил на конкретном узле.

Выводы

Сброс правил iptables равносилен тому, чтобы сделать узел недоступным. Трафик по-прежнему отправляется на узел, однако он не способен пробросить его дальше (т.е. на под). Kubernetes может восстанавливаться после такой проблемы с помощью отслеживания правил маршрутизации и их обновления при необходимости.

Большая благодарность Manabu Sakai за публикацию в блоге, которая во многом вдохновила на этот текст, а также Valentin Ouvrard за изучение вопроса пробрасывания правил iptables с мастера на другие узлы.

P.S. от переводчика

Читайте также в нашем блоге:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть