k3s с Cilium в режиме замены kube-proxy и L2 LoadBalancer
Published: 2026-02-06
Запустить k3s без Flannel и без kube-proxy звучит радикально, но Cilium заменяет и то, и другое eBPF-программами, которые превосходят iptables-аналоги по производительности. В связке с L2-анонсами Cilium вы получаете полный сетевой стек без MetalLB и без внешнего балансировщика нагрузки.
Этот пост — полная настройка: флаги k3s, Helm values для Cilium, конфигурация LBIPPool, политики L2-анонсов, инструменты отладки и частые ловушки.
Зачем заменять kube-proxy
kube-proxy записывает правила iptables для каждого Service и Endpoint в кластере. При росте числа сервисов (100+, 1000+ эндпоинтов) цепочки iptables становятся огромными, и каждый пакет проходит через все. eBPF-замена kube-proxy в Cilium делает поиск за O(1) через хеш-таблицы.
Что мы видим на практике:
- Маршрутизация сервисов не деградирует с ростом кластера
- Меньше нагрузки на таблицу
conntrack - Меньше обрывов соединений при всплесках трафика
- Таймауты
conntrackне влияют на eBPF-отслеживаемые соединения так же, как на обычные
Разница в производительности становится заметной выше ~200 сервисов. Ниже этой отметки оба подхода работают нормально. Главная причина переходить раньше — не делать миграцию позже, когда кластер большой и критичный.
Необходимые флаги k3s
bashk3s server \
--flannel-backend=none \
--disable-kube-proxy \
--disable servicelb \
--disable traefik
--flannel-backend=none говорит k3s не устанавливать CNI — эту роль берёт Cilium.
--disable-kube-proxy отключает запуск процесса kube-proxy.
--disable servicelb убирает встроенный контроллер LoadBalancer (Klipper) из k3s.
--disable traefik убирает стандартный ingress; мы используем APISIX через Helm.
Порядок операций важен: k3s запускается, но узел остаётся NotReady, пока не установлен Cilium. Это ожидаемо — не ждите Ready перед установкой Cilium.
Helm values для Cilium в k3s
yamlkubeProxyReplacement: true
k8sServiceHost: 192.168.1.10 # IP control-plane, не 127.0.0.1
k8sServicePort: 6443
ipam:
mode: kubernetes
operator:
replicas: 1
socketLB:
enabled: true
hostNamespaceOnly: true
nodePort:
enabled: true
hostPort:
enabled: true
# L2-анонсы (замена MetalLB)
l2announcements:
enabled: true
# Наблюдаемость через Hubble
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
k8sServiceHost должен указывать на реальный IP узла, а не 127.0.0.1. Если Cilium запустится без kube-proxy и попытается достучаться до API-сервера через localhost — получится петля. Используйте ansible_default_ipv4.address из Ansible inventory.
socketLB.hostNamespaceOnly: true ограничивает балансировку на уровне сокетов namespace хоста. Это важно на узлах, где запущены другие сервисы (например, агенты мониторинга), трафик которых не должен перехватываться socket LB Cilium.
Values для кластера из нескольких узлов
yamloperator:
replicas: 2 # HA для оператора Cilium
tunnel: disabled # Нативная маршрутизация вместо VXLAN для лучшей производительности
autoDirectNodeRoutes: true # Узлы маршрутизируют напрямую друг к другу
# IPAM per-node
ipam:
mode: kubernetes
# Управление полосой пропускания (опционально, требует ядро 5.1+)
bandwidthManager:
enabled: true
Нативная маршрутизация требует, чтобы узлы могли достигать друг друга на уровне pod CIDR — обычно это выполняется на on-prem flat-сетях и у облачных провайдеров с VPC-маршрутизацией.
LBIPPool: выделение внешних IP
L2-анонсы Cilium требуют пула IP для выделения. Создайте CiliumLoadBalancerIPPool:
yamlapiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: default-pool
spec:
blocks:
- cidr: "192.168.1.100/28" # 16 адресов для LoadBalancer Services
CIDR должен находиться в той же подсети, что и интерфейсы узла, — чтобы ARP мог разрешить адрес. Если поставить другую подсеть, L2-анонсы не сработают — ARP-ответы не примет сетевой коммутатор.
Можно использовать несколько пулов с разными CIDR:
yamlspec:
blocks:
- cidr: "192.168.1.100/28" # 14 адресов: .101 - .114
- cidr: "192.168.1.200/30" # 2 адреса: .201, .202 (для критических сервисов)
Чтобы закрепить конкретный IP за Service:
yamlmetadata:
annotations:
"lbipam.cilium.io/ips": "192.168.1.101"
L2AnnouncementPolicy: на каких интерфейсах анонсировать
yamlapiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: default-policy
spec:
interfaces:
- ^eth[0-9]+
externalIPs: true
loadBalancerIPs: true
Regex в interfaces означает, что политика применяется ко всем eth0, eth1 и т.д. Без этой политики анонсов не будет и внешние IP останутся недоступными.
Политика действует на весь кластер — все узлы участвуют в ARP для анонсируемых IP. Cilium выбирает узел-лидер для каждого IP для ответа на ARP-запросы. Если этот узел падает, другой перехватывает роль за несколько секунд (контролируется leaseDuration в политике).
Для более точного контроля — ограничьте политику конкретными лейблами узлов:
yamlspec:
nodeSelector:
matchLabels:
role: edge-node
interfaces:
- ^eth0
loadBalancerIPs: true
Это ограничивает L2-анонсы узлами с лейблом role: edge-node — полезно, если только некоторые узлы подключены к внешней сети.
Проверка работоспособности
После деплоя Service с type: LoadBalancer:
bash# Проверить, что IP выдан
kubectl get svc my-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# Убедиться, что Cilium анонсирует его
cilium l2announce list
# Проверить, какой узел является текущим лидером для IP
cilium l2announce get 192.168.1.100
# Проверить доступность IP с другой машины в LAN
ping 192.168.1.100
curl http://192.168.1.100/healthz
Если IP выдан, но недоступен:
bash# Проверить логи агента Cilium на ошибки L2-анонсов
kubectl -n kube-system logs -l app.kubernetes.io/name=cilium | grep -i "l2announce\|arp"
# Проверить, применена ли политика
kubectl get ciliuml2announcementpolicies
# Запустить ARP со шлюза/коммутатора
arping -I eth0 192.168.1.100
Частая проблема: rp_filter
Если внешний трафик достигает IP балансировщика, но ответные пакеты теряются — проверьте reverse path filtering:
bashsysctl net.ipv4.conf.eth0.rp_filter
Должно быть 0 или 2 (loose). В строгом режиме (1) ядро отбрасывает ответные пакеты, потому что они уходят не через тот интерфейс, через который пришли. Устанавливайте через sysctl в Ansible playbook, чтобы значение сохранялось после перезагрузки:
yaml- { key: net.ipv4.conf.all.rp_filter, value: "0" }
- { key: net.ipv4.conf.default.rp_filter, value: "0" }
Отладка с Cilium CLI
CLI cilium (устанавливается на каждый узел или запускается через kubectl exec) — основной инструмент отладки:
bash# Статус и здоровье Cilium
cilium status --verbose
# Мониторинг трафика в реальном времени (как tcpdump для Cilium)
cilium monitor --type drop # только отброшенные пакеты
cilium monitor --type l7 # L7 HTTP-потоки
# Проверить eBPF-таблицу для Service
cilium service list
cilium service get <id>
# Тест связности эндпоинтов
cilium connectivity test
# Проверить загруженные BPF-программы
bpftool prog list | grep cilium
Hubble — инструмент наблюдаемости более высокого уровня:
bash# Просмотр потоков в реальном времени
hubble observe --follow
# Фильтр по namespace
hubble observe --namespace default --follow
# Только отброшенные потоки
hubble observe --verdict DROPPED
# HTTP-потоки для конкретного пода
hubble observe --pod frontend --protocol HTTP
kube-proxy против Cilium eBPF
| kube-proxy | Cilium eBPF | |
|---|---|---|
| Маршрутизация сервисов | Цепочки DNAT в iptables | eBPF sockmap / hash |
| Масштабируемость | O(n) правил | O(1) поиск |
| LoadBalancer IP | Нужен MetalLB/cloud | L2-анонсы Cilium |
| Наблюдаемость | Нет | Потоки Hubble |
| NodePort | iptables | eBPF NodePort |
| Отслеживание соединений | conntrack ядра | eBPF CT (обходит conntrack) |
| Сетевые политики | iptables | eBPF (более выразительные) |
Операционный компромисс: когда что-то идёт не так, вы отлаживаете через CLI cilium вместо iptables -L — что значительно приятнее. У eBPF-стека меньше движущихся частей, и инструментарий лучше.
Главная причина не переходить: если команда хорошо знает отладку iptables и держит для неё готовый инструментарий, освоение eBPF-отладки потребует усилий. Закладывайте на это время.