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-отладки потребует усилий. Закладывайте на это время.