Замена MetalLB на Cilium LB IPAM без единого потерянного запроса
В кластере был ровно один LoadBalancer IP, и от него зависели все маршруты. Как я передал его от MetalLB к Cilium без единой ошибки — дав обоим контроллерам поработать одновременно.
После перевода домашней лаборатории на Cilium с заменой kube-proxy MetalLB остался последним компонентом платформы вне GitOps. Он был старый (релиз 2023 года) и всё ещё обслуживал устаревший API AddressPool, который светился в apiserver_requested_deprecated_apis. LB IPAM в Cilium уже был включён, а объявлять LoadBalancer IP по L2 Cilium умеет сам, так что второй контроллер был чистым дублированием.
Весь риск сводился к одной цифре. В кластере был ровно один LoadBalancer Service — у Gateway, — и все upstream’ы reverse-proxy смотрели на его адрес. Хуже того, адрес никогда не был закреплён: MetalLB выдал его динамически из пула, а нынешнее значение было историческим совпадением. Наивная миграция «успешно» выдала бы первый свободный адрес нового пула и сломала бы все маршруты разом.
Ловушка, которую пришлось убрать первой
Прежде чем что-то трогать, я проверил, какая машина отвечает на ARP по этому адресу. Ответили все семь.
kube-proxy удалили за несколько недель до этого, но kubeadm upgrade node тихо возвращал аддон kube-proxy на каждом хопе апгрейда. Его IPVS-интерфейс kube-ipvs0 пережил две полные перезагрузки кластера и всё ещё держал LoadBalancer IP и около 44 протухших ClusterIP — при нуле правил IPVS. С дефолтным arp_ignore=0 ядро отвечает на ARP за любой локальный адрес на любом интерфейсе, поэтому IP заявляли все ноды.
Под MetalLB с этим можно было жить: при externalTrafficPolicy: Cluster и обработке сервисов в eBPF любая нода способна обслужить адрес. Но это уничтожало единственный дешёвый способ проверить L2-передачу — посмотреть, чей MAC владеет IP. Выборы лидера стали бы декорацией. Поэтому первым шагом я убрал с kube-ipvs0 только этот один /32 на каждой ноде: сначала канарейка на ноде-не-владельце, текущий владелец — последним.
Контринтуитивный порядок
- Убрать протухший адрес, чтобы владение по ARP стало однозначным и наблюдаемым.
- Включить
l2announcementsв Helm values Cilium через GitOps и перезапустить агент и оператор: чарт не перезапускает поды, если поменялся только ConfigMap. - Применить пул IP и L2-политику Cilium, пока MetalLB ещё работает.
- Только потом масштабировать MetalLB в ноль, проверить failover и удалить его.
Именно третий шаг — причина, по которой простоя не было. Два ARP-ответчика звучат как split brain, но при externalTrafficPolicy: Cluster неважно, кто победит: любая нода обслужит трафик в eBPF. А поскольку новый пул состоял из одного /32, оба IPAM могли вычислить только один и тот же адрес — статус Service ни разу не дёрнулся. Cilium взял lease, и ARP переехал на его ноду, пока MetalLB ещё был жив.
Адрес я закрепил пулом из одного адреса, а не аннотацией. Service принадлежит Gateway, и его контроллер вычищает аннотации. Пул /32 делает правильный результат единственным арифметически возможным — это надёжнее, чем доверять подсказке.
Измерено, а не предположено
| Шаг | Результат |
|---|---|
| Пул и политика применены при живом MetalLB | ARP переехал к владельцу lease, 0 ошибок |
| MetalLB масштабирован в ноль | 10 проверок подряд, адрес не изменился |
| Владелец lease убит | адрес переехал примерно за 2,3 с, 0 ошибок из 40 запросов |
| MetalLB удалён полностью | ingress отвечал 200 после каждого удаления |
Порядок удаления тоже важен: validating webhook — первым, потому что webhook без живого бэкенда блокирует удаление собственных custom resources.
Испуг, который оказался плохим тестом
С ноды curl на LoadBalancer IP возвращал 000, а ARP-таблица оставалась пустой. Это читалось как «ноды не достают собственный LoadBalancer IP» и едва не съело окно миграции. Это не было ни сбоем, ни старой поломкой. Код выхода был 35 — ошибка TLS, то есть TCP-соединение установилось. Cilium в eBPF перенаправляет адрес на локальный Envoy ноды, так что пакет вообще не выходит в сеть и ARP не нужен. TLS падал из-за passthrough-listener’а, которому не с чем сопоставить SNI при обращении к голому IP. С правильным заголовком Host или --resolve ответ был 200.
Что взять с собой
- Перед L2-передачей найдите всех ARP-ответчиков на адрес. Забытые dummy-интерфейсы делают понятие «владелец» бессмысленным.
- Два контроллера, объявляющие один адрес, безопасны, когда его может обслужить любая нода. Используйте это для перекрытия вместо резкого переключения.
- Критичный адрес закрепляйте пулом, который не может выдать ничего другого.
%{http_code}сворачивает любой сбой транспорта в000. Всегда смотрите код выхода.- Быстрый failover — это цифра мягкого сценария. При жёсткой потере ноды ждать придётся весь срок lease.