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-запись.