Если вы поднимаете Kubernetes в компактной конфигурации на VDS, K3s — естественный выбор: минималистичный, экономный по памяти и времени. Но у него нет облачного провайдера, а значит, для Service типа LoadBalancer нужен внешний балансировщик. Самый логичный вариант — MetalLB. В этой статье разберем, как построить устойчивую схему с Service IP на VDS: режимы layer2 и BGP, healthchecks, BFD, sysctl и настройку kube-proxy.
Почему MetalLB в K3s на VDS — это не «поставил и забыл»
В K3s по умолчанию есть простой балансировщик (ServiceLB), но он не выдает «реальный» Service IP, доступный из внешней сети, — чаще это NodePort со снабженными iptables правилами. MetalLB же выделяет и анонсирует настоящие адреса из пула, обеспечивая привычный опыт «managed» LoadBalancer. На VDS это особенно востребовано: хочется иметь понятные публичные IP для Ingress-контроллера, API или TCP/UDP сервисов.
Подводные камни кроются в сетевых политиках провайдера. Для layer2 MetalLB отвечает на ARP (IPv4) или NDP (IPv6), «притягивая» Service IP к активному узлу. Многие провайдеры фильтруют ARP/NDP-спуфинг: это может сломать layer2. Тогда понадобится BGP: ваш кластер будет анонсировать префикс на граничный маршрутизатор, а тот — раздавать маршруты дальше. Не у всех провайдеров есть частный BGP-пиринг, поэтому иногда приходится ставить свой «пограничный» маршрутизатор (например, FRR) на отдельном VDS и строить eBGP от нод к нему.
Топология и выбор режима: layer2 или BGP
Коротко:
- Layer2: просто и быстро, один активный держатель IP. Требования — провайдер не блокирует ARP/NDP для «чужих» IP, у вас есть пул адресов в той же L2-сети, где узлы кластера.
- BGP: сложнее, но мощнее — ECMP, анонс с нескольких узлов, быстрая сходимость с BFD. Требования — пиринг к сетевому оборудованию (провайдера или вашему пограничному VDS с FRR/BIRD), выделенный префикс, фильтрация маршрутов, пароли BGP.
Правило большого пальца: если провайдер позволяет ARP-ответы для выделенного вам дополнительного префикса — начинайте с layer2. Если видите «немой» ARP и нулевой эффект от анонса IP — переезжайте на BGP (через eBGP к маршрутизатору или к своему FRR).

Подготовка K3s
Предположим, у нас несколько узлов (1 control-plane + 2 workers) на VDS. K3s по умолчанию использует flannel, чего достаточно. Нам важно отключить встроенный ServiceLB и, если хотим, включить kube-proxy в режиме IPVS со strict ARP. Сконфигурируем K3s через файл:
# /etc/rancher/k3s/config.yaml
write-kubeconfig-mode: "0644"
disable:
- servicelb
kube-proxy-arg:
- proxy-mode=ipvs
- ipvs-strict-arp=true
Перезапустите K3s на всех узлах после изменения конфигурации. IPVS не обязателен, но в больших кластерах он предсказуемее под нагрузкой. Опция ipvs-strict-arp=true предотвращает ARP-флюкс и важна для корректной работы с MetalLB.
Полезные sysctl для сети
Дополнительно имеет смысл включить маршрутинг и ужесточить ARP-поведение:
# /etc/sysctl.d/99-k3s-metallb.conf
net.ipv4.ip_forward=1
net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
Примените: sysctl --system.
Если вы планируете Ingress, посмотрите наш обзор контроллеров и режимов публикации: Ingress в K3s: Traefik, NGINX, HAProxy.
Установка MetalLB
Создадим namespace и установим MetalLB (через Helm или манифест релиза MetalLB):
kubectl create namespace metallb-system
kubectl apply -n metallb-system -f metallb-native.yaml
Далее мы опишем CRD для пулов адресов и объявлений. Разделим сценарии на layer2 и BGP.
Layer2: минимальная живая схема
Требования: дополнительная подсеть/пул адресов в вашей L2-сети у провайдера. Часто это /29 или /28, маршрутизированная на один из ваших серверов. Уточните у провайдера, разрешены ли ARP-ответы с любых узлов (иначе layer2 может не сработать).
Пул адресов и объявление
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public-pool
namespace: metallb-system
spec:
addresses:
- 203.0.113.64-203.0.113.79
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: public-l2
namespace: metallb-system
spec:
ipAddressPools:
- public-pool
В этом режиме MetalLB выберет один узел-«владельца» IP для каждого Service. При падении узла IP быстро переедет на другой.
Service типа LoadBalancer
apiVersion: v1
kind: Service
metadata:
name: echo-lb
namespace: default
annotations:
metallb.universe.tf/address-pool: public-pool
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: echo
ports:
- name: http
port: 80
targetPort: 8080
externalTrafficPolicy: Local важен для корректности исходных IP клиента и для «умного» анонса: MetalLB может ограничивать анонс узлами, на которых есть готовые Endpoints (см. раздел про healthchecks ниже). Если не используете Local, трафик может прилетать на любой узел и проксироваться дальше kube-proxy.
Ограничения layer2
- Один активный узел на IP — не будет ECMP с внешней стороны.
- Некоторые провайдеры фильтруют ARP — проверяйте заранее.
- Failover зависит от таймингов ARP/NDP и обнаружения отказа узла.
BGP: гибкость, ECMP и быстрая сходимость
Если у вас есть пиринг к провайдерскому маршрутизатору — прекрасно. Если нет, поднимите «пограничный» VDS с FRR (или BIRD) в той же сети и сделайте eBGP от нод MetalLB к нему. Этот пограничный узел будет анонсировать ваш префикс дальше (статикой провайдера или, если договоритесь, через пиринг).
Пул адресов и объявления для BGP
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public-pool
namespace: metallb-system
spec:
addresses:
- 198.51.100.128/27
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: edge-router
namespace: metallb-system
spec:
myASN: 65010
peerASN: 65000
peerAddress: 192.0.2.1
holdTime: 180s
keepAliveTime: 60s
ebgpMultiHop: true
password: "supersecret-md5"
bfdProfile: fast-bfd
---
apiVersion: metallb.io/v1beta2
kind: BFDProfile
metadata:
name: fast-bfd
namespace: metallb-system
spec:
receiveInterval: 50ms
transmitInterval: 50ms
echoInterval: 50ms
passiveMode: false
minimumTtl: 254
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: public-adv
namespace: metallb-system
spec:
ipAddressPools:
- public-pool
aggregationLength: 32
Пояснения:
myASN/peerASN— приватные AS для eBGP. В реальном пиринге используйте согласованные номера.ebgpMultiHop: true— удобно, если peer не в одном L2-сегменте или вы строите пиринг через внешний хост.password— BGP MD5 для базовой защиты сессии.BFDProfile— быстрые детекторы отказов; проверьте поддержку на стороне соседа.
FRR на пограничном VDS (пример)
Если вы поднимаете собственный маршрутизатор, настройте eBGP с каждым рабочим узлом кластера и пропишите анонс префикса наружу. В FRR это проверяется командами:
vtysh -c "show ip bgp summary"
vtysh -c "show ip route 198.51.100.128/27"
Не забудьте firewall: TCP/179 для BGP, UDP/3784 для BFD (single-hop) или 4784 (multihop).

Healthchecks: как MetalLB решает, где анонсировать IP
Есть два уровня «здоровья»:
- Здоровье узла и процесса speaker: если speaker на ноде не жив, анонс оттуда снимется.
- Наличие готовых Endpoints для конкретного Service.
Ключевая практика — использовать externalTrafficPolicy: Local для тех сервисов, где важен исходный клиентский IP и/или вы хотите, чтобы IP анонсировался только с узлов, где реально есть готовые Pod'ы для этого Service. Так MetalLB не будет привлекать «пустые» ноды для входящего трафика.
В layer2 это означает: активным держателем IP становится узел, на котором есть готовые Endpoints. При их исчезновении (например, Deployment масштабирован в 0 или все Pods неготовы) IP переедет на другой узел с рабочими Endpoints. В BGP это означает: только такие узлы будут анонсировать маршрут, и внешние роутеры перестанут слать трафик на ноды без готовых Endpoints (ECMP динамически уменьшится).
Для быстрого обнаружения отказов в BGP используйте BFD. Для layer2 обратите внимание на тайминги ARP/NDP и настройку kube-proxy/IPVS (strict ARP).
Readiness и PodDisruptionBudget
Чтобы healthchecks имели смысл, убедитесь, что ваши приложения правильно выставляют readinessProbe, а также пользовались PodDisruptionBudget, чтобы во время обновлений у вас не исчезали все Endpoints сразу (что вызовет миграции VIP или спад анонсов).
Проверка и диагностика
Проверяем CRD и состояние компонентов
kubectl -n metallb-system get pods,svc
kubectl -n metallb-system get ipaddresspools,l2advertisements,bgppeers,bgpadvertisements
kubectl -n metallb-system logs deploy/controller
kubectl -n metallb-system logs ds/speaker -c speaker
Layer2: ARP/NDP и соседние таблицы
ip a
ip neigh
arping -I eth0 203.0.113.70 -c 3
tcpdump -i eth0 arp or icmp6 and ip6[40] == 135
Если не видите ARP-ответа на Service IP, хотя спикер активен, вероятно, провайдер режет ARP. Проверьте логи MetalLB, строчки вида «layer2: announce»/«withdraw».
BGP: сессии и маршруты
# На ноде k3s с MetalLB (FRR-интеграция или нативный стек MetalLB)
ss -ntp | grep :179
# На пограничном FRR
vtysh -c "show ip bgp summary"
vtysh -c "show bgp ipv4 unicast 198.51.100.128/27"
# Трафик BFD
tcpdump -ni eth0 udp port 3784 or udp port 4784
Обратите внимание на ECMP: внешние маршрутизаторы могут балансировать трафик на несколько нод. Убедитесь, что MTU и обратные пути корректны.
Частые проблемы и их решение
- В layer2 не пингуется Service IP: провайдер блокирует ARP/NDP. Решение — переход на BGP или запрос разрешений/вторичного маршрутизируемого префикса.
- BGP up, но трафик не идет: на внешнем роутере нет статик/redistribute для вашего префикса, либо фильтрация по спискам соседей/префиксов. Проверьте политики импорта/экспорта.
- Петли исходящих соединений от Pod'ов: проверьте policy routing,
externalTrafficPolicy: Local, SNAT на egress, MTU внутри overlay (flannel) и на хостах. - Дребезг маршрутов при рестарте Pod'ов: настройте
readinessProbe,maxUnavailableдля Deployments, используйте PDB. - IPVS/ARP-флюкс: проверьте
ipvs-strict-arp=trueи sysctlarp_ignore/arp_announce.
IPv6: NDP и двойной стек
MetalLB поддерживает IPv6 и NDP. В layer2 это ответ на NDP-соседские запросы, в BGP — анонс IPv6-префиксов. На практике в VDS важно проверить RA/ND-фильтры у провайдера. Для dual-stack Service укажите порты и убедитесь, что у вас есть независимые пулы IPv4/IPv6.
Наблюдаемость: метрики и алерты
MetalLB controller и speaker экспонируют основные метрики: состояние пиров, количество объявленных префиксов, время сходимости. Подключите их к вашей системе мониторинга и заводите алерты на:
- Падение BGP-пиров.
- Исчезновение объявлений для критичных Service.
- Аномальный рост переанонсов (flap) и перезапусков speaker.
Практические рекомендации по дизайну
- Разделяйте пулы по классам сервисов: публичные HTTP(S), гейм-сервисы/UDP, внутренние TCP. Для TCP/UDP см. также наш материал по stream-балансировке NGINX: балансировка TCP/UDP в NGINX.
- В BGP используйте разные пулы/префиксы для изоляции и простоты фильтрации.
- Для production обязательно MD5 на BGP и фильтр источников по IP, плюс ограничение TCP/179 на firewall.
- В layer2 держите небольшой запас IP, чтобы миграции и масштабирование не упирались в потолок пула.
- Обновите DNS: укажите A/AAAA на стабильные LoadBalancer IP. При необходимости используйте нашу регистрацию доменов и оформляйте SSL-сертификаты для HTTPS.
Чек-лист перед запуском
- K3s установлен, ServiceLB отключен, kube-proxy настроен (IPVS и strict ARP по желанию).
- MetalLB развернут, CRD применены: IPAddressPool, L2/BGP Advertisement, BGPPeer/BFDProfile при необходимости.
- Firewall: открыт TCP/179 для BGP, UDP/3784 или 4784 для BFD; разрешены ARP/NDP при layer2.
- Сервисы с
type: LoadBalancerиexternalTrafficPolicy: Localдля корректного исходного IP и анонсов по Endpoints. - ReadinessProbe и PDB на приложениях, чтобы не провоцировать flap.
- Наблюдаемость: метрики MetalLB, алерты на пиры и объявления.
Итоги
MetalLB делает K3s на VDS полноценной платформой с «настоящими» LoadBalancer Service IP: в layer2 режиме — просто и быстро, в BGP — мощно и масштабируемо. Заранее проверьте сетевые ограничения провайдера, определитесь с пулом адресов и подготовьте здравые healthchecks (readiness, PDB, externalTrafficPolicy: Local). С этими принципами ваш кластер выдержит отказы узлов и обновления, а входящий трафик всегда будет приходить по предсказуемым IP.


