Отказоустойчивый Kubernetes на BGP. Часть 6: адреса LoadBalancer из Calico по BGP

Кластер работает, API отказоустойчив. Теперь научим Calico выдавать адреса сервисам type: LoadBalancer и анонсировать их роутеру. Отдельный MetalLB не нужен: Calico умеет и то, и другое.

Серия «Отказоустойчивый Kubernetes на BGP»

  1. Схема и план
  2. Готовим узлы Ubuntu 24.04
  3. BGP на роутере (FRR)
  4. kube-vip в режиме BGP и первый мастер
  5. Calico с eBPF и рабочие узлы
  6. Адреса LoadBalancer из Calico по BGP (эта статья)
  7. Traefik за BGP и externalTrafficPolicy Local
  8. Проверка отказоустойчивости и разбор полётов

Что мы собираемся сделать

  1. Сказать Calico свою AS (65030) и выключить полносвязную BGP-сетку между узлами.
  2. Завести сессии BGP от воркеров к роутеру. Мастера в этом участия не принимают.
  3. Создать пул адресов, из которого можно выдавать адреса только сервисам.
  4. Включить BGP в Installation.
  5. Проверить на тестовом сервисе.

BGPConfiguration

apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 65030
  logSeverityScreen: Info
  nodeToNodeMeshEnabled: false
  serviceLoadBalancerAggregation: Disabled
  serviceLoadBalancerIPs:
    - cidr: 192.168.20.64/26

Разберём поля.

nodeToNodeMeshEnabled: false. По умолчанию Calico при включении BGP строит полную сетку: каждый узел пиринговался бы с каждым. Нам в этом нет смысла: трафик подов идёт через VXLAN, а BGP нужен только чтобы рассказать роутеру про адреса сервисов. Включать BGP без этого — значит получить сетку из восьми узлов. Применяем конфигурацию до включения BGP.

asNumber: 65030. Умолчание 64512, поэтому задаём явно: роутер — 65010, мастера (kube-vip) — 65020, воркеры (Calico) — 65030.

serviceLoadBalancerIPs. Какие адреса вообще разрешено анонсировать. Совпадает с пулом.

serviceLoadBalancerAggregation: Disabled. Самый важный параметр, остановимся на нём.

Зачем отключать агрегацию

Когда агрегация включена (а это умолчание), Calico анонсирует весь блок пула (192.168.20.64/26) со всех узлов сразу, не глядя на то, где живут эндпойнты сервиса. Роутер строит ECMP по всем воркерам. Для сервисов с externalTrafficPolicy: Cluster это допустимо (любой узел перебросит пакет дальше), а вот для Local катастрофа: пакет, пришедший на узел, где нет пода сервиса, просто теряется.

При Disabled Calico анонсирует отдельный /32 на каждый адрес, и для сервисов с Local — только с тех узлов, где есть готовый эндпойнт (Calico смотрит на готовые адреса в Endpoints). Это страхует от трёх вещей:

  1. Под упал или не проходит readiness. Сессия жива, но маршрут с узла снимается сам. При включённой агрегации роутер продолжал бы слать часть новых соединений в никуда.
  2. Новый рабочий узел. calico-node поднимает сессию раньше, чем под скачает образ и станет готов; при агрегации узел сразу попал бы в ECMP пустым.
  3. Сервис с одной-двумя репликами. При включённой агрегации он сломался бы сразу.

Цена — по /32 с узла, то есть ничего. Настройка глобальная.


BGPPeer: только воркеры

apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: router
spec:
  nodeSelector: "!has(node-role.kubernetes.io/control-plane)"
  peerIP: 192.168.10.1
  asNumber: 65010

nodeSelector исключает мастера намеренно. На мастерах сессию с роутером уже держит kube-vip, а два BGP-демона на одном узле не удержат сессию с одним и тем же соседом: сессия определяется парой адресов, и роутер примет только одну от 192.168.10.x. В итоге на мастерах BIRD (BGP-демон Calico) запущен, но пиров у него нет вообще, и он ничего не анонсирует. Правильный ответ calicoctl node status на мастере — No IPv4 peers found.

AS самого Calico задаётся глобально в BGPConfiguration, а здесь указана AS соседа.


Пул адресов для сервисов

apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: loadbalancer-pool
spec:
  cidr: 192.168.20.64/26
  allowedUses:
    - LoadBalancer
  disableBGPExport: false
  natOutgoing: false
  nodeSelector: "all()"
  • allowedUses: [LoadBalancer] — из этого пула нельзя выдать адрес поду, только сервису.
  • nodeSelector: all() — иначе Calico отвергнет пул с сообщением «IP Pool with AllowedUse LoadBalancer must have node selector set to all()». Для пула сервисов селектор смысла и не несёт: кто анонсирует адрес, решает расположение эндпойнтов, а не пул.
  • Пул начинается с .64 и не пересекается с VIP API (192.168.20.1).

Включаем BGP

kubectl apply -f bgpconfiguration.yaml
kubectl apply -f bgppeer.yaml
kubectl apply -f loadbalancer-ippool.yaml
kubectl patch installation default --type=merge -p '{"spec":{"calicoNetwork":{"bgp":"Enabled"}}}'

Порядок существенен: сначала BGPConfiguration, потом включение. Инкапсуляция трафика подов не меняется — VXLAN CrossSubnet остаётся.

Проверяем с воркера:

kubectl -n calico-system exec ds/calico-node -c calico-node -- birdcl show protocols

В выводе должен появиться протокол Node_192_168_10_1 в состоянии Established (имя — Node_ плюс адрес соседа с подчёркиваниями). Либо, если на узле есть calicoctl: sudo calicoctl node status.

На роутере:

sudo vtysh -c 'show bgp summary'

Теперь должно быть восемь соседей: .17–.19 из AS 65020 и .20–.24 из AS 65030. У мастеров в PfxRcd стоит по 1 префиксу (VIP). У воркеров пока 0 — и это правильно: у нас ещё нет ни одного сервиса LoadBalancer, а сеть подов не экспортируется.


Как попросить адрес

Контроллер Calico может выдавать адреса двумя способами: всем сервисам подряд (AllServices) или только тем, которые попросили явно (RequestedServicesOnly). Я выбрал второй — адреса не должны раздаваться «просто так».

Режим задаётся в KubeControllersConfiguration:

kubectl patch kubecontrollersconfiguration default --type=merge \
  -p '{"spec":{"controllers":{"loadBalancer":{"assignIPs":"RequestedServicesOnly"}}}}'

Адрес просится аннотацией на сервисе:

metadata:
  annotations:
    projectcalico.org/loadBalancerIPs: '["192.168.20.100"]'

Аннотация — это JSON-массив, а не строка

Это самая частая причина недоумения. Правильно '["192.168.20.100"]' (с квадратными скобками и кавычками). От обычной строки 192.168.20.100 контроллер падает с failed to parse annotations, сервис молча остаётся в <pending>, и по самому сервису этого не видно — ошибка только в логах calico-kube-controllers:

kubectl -n calico-system logs -l k8s-app=calico-kube-controllers --tail=50 | grep -i loadbalancer

Тестовый сервис

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
spec:
  replicas: 2
  selector:
    matchLabels: {app: whoami}
  template:
    metadata:
      labels: {app: whoami}
    spec:
      containers:
        - name: whoami
          image: traefik/whoami:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: whoami
  annotations:
    projectcalico.org/loadBalancerIPs: '["192.168.20.100"]'
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: whoami
  ports:
    - port: 80
      targetPort: 80
kubectl apply -f whoami.yaml
kubectl get svc whoami          # EXTERNAL-IP 192.168.20.100
kubectl get pods -l app=whoami -o wide

На роутере:

sudo vtysh -c 'show bgp ipv4 unicast 192.168.20.100'
ip route show 192.168.20.100

Маршрут должен прийти ровно с тех узлов, где живут поды whoami, а не со всех пяти. С клиента из другой подсети (не из подсети узлов):

curl http://192.168.20.100/

whoami печатает имя пода, и по повторным запросам видно, что роутер разносит соединения по разным узлам.

Удаляем пробу:

kubectl delete -f whoami.yaml

Что ещё надо знать

С самого узла адрес сервиса доступен, но проверять его надо curl. BIRD ставит на каждом анонсирующем узле маршрут blackhole 192.168.20.100, и ip route get на этом адресе отвечает Invalid argument. При этом соединения самих процессов узла работают: eBPF переписывает connect() на под раньше маршрутизации. Проверять надо curl -k https://<адрес>, а не ip route get.

Риск для сервисов с Local. В Calico известна гонка (calico#11523): когда под появляется на узле, BGP может анонсировать /32 раньше, чем eBPF запрограммирует нужные карты. Окно короткое и бывает только при перезапуске или выкатке пода.

Предохранитель на роутере. Когда я включал BGP, значение disableBGPExport: true у пула подов сначала не держалось: оператор возвращал его за секунды, и Calico предлагал роутеру девять блоков 172.20.x.x/26. Все девять отбрасывал prefix-list SERVICES — в received-routes было 10 (9 filtered). Предохранитель, который мы поставили в части 3, оказался не формальностью. Причина была в том, что оператор сверяет пул со списком в Installation, а не с самим IPPool — потому в части 5 мы сразу задали флаг там.


В части 7 повесим на этот механизм реальный сервис — Traefik — и разберём, почему для него нужен externalTrafficPolicy: Local.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.