Bare-metal k8s: как сохранить стабильность Cilium L2 LoadBalancer при обслуживании узлов

Published: 2026-07-13

На bare-metal кластере Kubernetes главная задача не просто поднять сервис, а сохранить внешний IP доступным после перезагрузки, дренажа или перехода сервиса на другой узел. В этой статье разберём, как работает Cilium L2 LoadBalancer, какие sysctl важно задать на хостах и как сделать поведение внешнего IP предсказуемым.


Проблема

В облаке LoadBalancer сервис получает внешний адрес автоматически. В bare-metal такого механизма нет: Kubernetes может выставить EXTERNAL-IP, но этот IP не начинает отдаваться на физическом сегменте сам по себе.

В результате сервисы выглядят живыми внутри кластера, но снаружи не отвечают, потому что ни один узел не отвечает на ARP/NDP запросы за этот адрес.

Это особенно заметно при обслуживании узлов: после kubectl drain или reboot трафик может продолжать уходить на старый MAC, а новый узел ещё не успел объявить адрес.

bash$ kubectl get svc apisix -n ingress-apisix
NAME    TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)
apisix  LoadBalancer   10.96.45.23   172.16.57.193    80:32000/TCP

IP есть, но без сетевого владельца он остаётся недоступным.

Как это работает

Cilium L2 Announcement добавляет сервисному IP сетевой хозяин на уровне ARP/NDP.

  • CiliumLoadBalancerIPPool задаёт пул адресов, из которого Cilium выдаёт IP для LoadBalancer сервисов.
  • CiliumL2AnnouncementPolicy определяет, какие узлы и по каким интерфейсам будут объявлять эти адреса.
  • Когда сервис создаётся, Cilium резервирует IP и начинает отвечать на ARP/NDP на выбранном интерфейсе.
  • При переносе сервиса на другой узел старый анонс снимается, а новый узел берёт тот же IP на себя.

Ключевой момент: Kubernetes знает только IP сервиса. Доступность зависит от того, что Cilium управляет ARP/NDP на физическом интерфейсе.

Почему важны sysctl на хосте

На bare metal часто ломается возвратный путь из-за настроек ядра:

  • net.ipv4.ip_forward=1 нужен, чтобы узел мог перенаправлять пакеты между интерфейсами.
  • net.ipv4.conf.all.rp_filter=0 и net.ipv4.conf.default.rp_filter=0 нужны, если ответ на пакет уходит не по тому интерфейсу, по которому он пришёл.
  • net.ipv4.conf.<iface>.rp_filter=0 полезен, когда объявление IP происходит на одном интерфейсе, а основной маршрут — через другой.

Если rp_filter=1, ядро может отбросить валидный ответ, потому что путь обратно кажется «неправильным».

Когда фиксировать IP

В продакшне стоит назначать loadBalancerIP, чтобы DNS, firewall и NAT-правила работали с предсказуемым адресом.

yamlapiVersion: v1
kind: Service
metadata:
  name: apisix
  namespace: ingress-apisix
spec:
  type: LoadBalancer
  loadBalancerIP: 172.16.57.193
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: apisix

Cilium попытается выдать именно этот IP из пула или вернёт ошибку, если адрес уже занят.

Установка

Шаг 1: задаём пул IP

Выберите блок внутри вашего LAN, который не используется DHCP и не конфликтует с другими сервисами.

yamlapiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
  name: baremetal-lb-pool
spec:
  blocks:
    - cidr: 172.16.57.192/28

Примените через:

bashkubectl apply -f cilium-lb-pool.yaml

Шаг 2: ограничиваем анонсы нужными узлами и интерфейсом

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

yamlapiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata:
  name: baremetal-l2-policy
spec:
  serviceSelector:
    matchLabels:
      app.kubernetes.io/managed-by: Flux
  nodeSelector:
    matchLabels:
      kubernetes.io/os: linux
      node-role.kubernetes.io/worker: ""
  interfaces:
    - eth0
  externalIPs: true
  loadBalancerIPs: true

Поле interfaces — самый важный. Если Cilium анонсирует на неправильном NIC или на мосту без LAN, IP не станет доступным.

Шаг 3: настраиваем узлы

На каждом bare-metal узле зафиксируйте эти sysctl:

bashcat <<'EOF' | sudo tee /etc/sysctl.d/99-cilium-l2.conf
net.ipv4.ip_forward=1
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.eth0.rp_filter=0
EOF
sudo sysctl --system

Если у вас несколько физических интерфейсов или VLAN, повторите net.ipv4.conf.<iface>.rp_filter=0 для нужного NIC.

Шаг 4: проверяем ресурсы Cilium

bashkubectl get ciliumloadbalancerippool -A
kubectl get ciliuml2announcementpolicy -A
kubectl describe ciliuml2announcementpolicy baremetal-l2-policy

Ищите пул в Active состоянии и политику, привязанную к рабочим узлам.

Шаг 5: создаём LoadBalancer сервис

Примените манифест сервиса и проверьте внешний IP:

bashkubectl apply -f apisix-service.yaml
kubectl get svc apisix -n ingress-apisix -o wide

Затем проверьте доступность извне:

bashcurl -I http://172.16.57.193

Если нет ответа, проверьте ARP на клиенте или маршрутизаторе:

baship neigh show 172.16.57.193

Ожидаемый результат:

text172.16.57.193 dev eth0 lladdr 52:54:00:12:34:56 REACHABLE

Шаг 6: обслуживаем узлы без сюрпризов

Для обслуживания используйте обычный kubectl drain и reboot, но помните, что на время перезагрузки сервис может терять IP.

bashkubectl drain node-01 --ignore-daemonsets --delete-emptydir-data
sudo reboot

При перезагрузке Cilium перестаёт анонсировать IP. Сервис будет недоступен до тех пор, пока другой узел не возьмёт этот адрес на себя.

Чтобы поведение было предсказуемым:

  • используйте один пул для всего кластера;
  • фиксируйте loadBalancerIP для продакшн-сервисов;
  • давайте анонсы только рабочим узлам;
  • если можете, уменьшите TTL ARP на upstream-оборудовании.

Когда узел снова появится, Cilium должен вернуть анонс автоматически.

Что может пойти не так

IP остаётся pending. Обычно это означает, что сервис не попадает под политику или свободных адресов в пуле нет.

bashkubectl get svc apisix -n ingress-apisix -o jsonpath='{.status.loadBalancer.ingress}'
  • Проверьте, что в CiliumLoadBalancerIPPool есть свободные блоки.
  • Проверьте селекторы CiliumL2AnnouncementPolicy и поле interfaces.
  • Убедитесь, что селектор сервиса соответствует политике.

Клиенты видят неправильный MAC. Это происходит, если один и тот же IP заявляют два узла.

  • ip neigh show <ip> на клиенте.
  • sudo tcpdump -nei eth0 arp or icmp на узлах.
  • Убедитесь, что политики не пересекаются и пул уникален.

После reboot сервис недоступен. Кластер мог восстановиться, но upstream хранит старую ARP-запись.

  • Очистите ARP на маршрутизаторе или клиенте.
  • Уменьшите ARP cache timeout на маршрутизаторе или Linux-клиентах.
  • Проверьте Cilium: kubectl get pods -n kube-system -l k8s-app=cilium и логи.

Анонс прошёл на неправильный интерфейс. Это самая частая ошибка на bare metal.

bashkubectl describe ciliuml2announcementpolicy baremetal-l2-policy
  • Убедитесь, что interfaces: указывает на LAN-интерфейс.
  • Проверьте, не переименовался ли интерфейс из-за firmware или cloud-init.
  • Убедитесь, что ip route и ip addr показывают выбранный NIC как тот, который выходит в сеть.

Итого

  • Bare-metal LoadBalancer требует сетевого хозяина IP; Cilium L2 берёт это на себя через ARP/NDP.
  • Определите пул, ограничьте анонсы правильным LAN-интерфейсом и зафиксируйте IP для продакшн-сервисов.
  • Настройте ip_forward и rp_filter на хостах — иначе ответы могут отбрасываться.
  • Во время дренажа и reboot внешний IP может исчезать до тех пор, пока другой узел не возьмёт его на себя.
  • Основные ошибки: неверный селектор политики, неправильный интерфейс, пересекающийся пул или устаревшая ARP-запись.