Отказоустойчивый Kubernetes на BGP. Часть 6: адреса LoadBalancer из Calico по BGP
Кластер работает, API отказоустойчив. Теперь научим Calico выдавать адреса сервисам type: LoadBalancer и анонсировать их роутеру. Отдельный MetalLB не нужен: Calico умеет и то, и другое.
Серия «Отказоустойчивый Kubernetes на BGP»
- Схема и план
- Готовим узлы Ubuntu 24.04
- BGP на роутере (FRR)
- kube-vip в режиме BGP и первый мастер
- Calico с eBPF и рабочие узлы
- Адреса LoadBalancer из Calico по BGP (эта статья)
- Traefik за BGP и externalTrafficPolicy Local
- Проверка отказоустойчивости и разбор полётов
Что мы собираемся сделать
- Сказать Calico свою AS (65030) и выключить полносвязную BGP-сетку между узлами.
- Завести сессии BGP от воркеров к роутеру. Мастера в этом участия не принимают.
- Создать пул адресов, из которого можно выдавать адреса только сервисам.
- Включить BGP в
Installation. - Проверить на тестовом сервисе.
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). Это страхует от трёх вещей:
- Под упал или не проходит readiness. Сессия жива, но маршрут с узла снимается сам. При включённой агрегации роутер продолжал бы слать часть новых соединений в никуда.
- Новый рабочий узел.
calico-nodeподнимает сессию раньше, чем под скачает образ и станет готов; при агрегации узел сразу попал бы в ECMP пустым. - Сервис с одной-двумя репликами. При включённой агрегации он сломался бы сразу.
Цена — по /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.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.