Минуту работало, потом таймауты: MetalLB L2 с kube-proxy ipvs без strictARP
Перевод хостов на новый LoadBalancer IP работал недолго, потом внешний балансировщик увидел таймауты на L4. На ARP за один и тот же IP отвечали все ноды.
Часть вывода ingress-nginx из эксплуатации на bare-metal кластере в Optimacros — переключение хостов по одному со старого ingress на LoadBalancer IP Envoy Gateway. Перед кластером стоит внешний HAProxy в другой подсети, но в том же L2-сегменте. Я направил первый SNI-хост на новый IP, и он ответил как надо.
Примерно через минуту все хосты, направленные туда, ушли в таймауты. HAProxy писал Layer4 timeout и помечал бэкенд упавшим. С моего ноутбука тот же IP работал. MetalLB показывал, что IP анонсируется ровно с нужной ноды — той, где под Envoy. Я откатил изменение в HAProxy, сервис восстановился — около минуты влияния на четыре хоста.
Не тот MAC-адрес
Первый полезный взгляд — с самой VM HAProxy: ip neigh show <lb-ip>. Закэширован был MAC воркера, на котором нет пода Envoy. MetalLB анонсировал с одной ноды, а HAProxy ходил на другую.
У Service стоял externalTrafficPolicy: Local — правильно для сохранения клиентских IP, и это значит, что нода без локального эндпоинта трафик отбрасывает. SYN’ы уходили не на ту ноду и исчезали.
Почему на ARP отвечали все
kube-proxy в режиме ipvs вешает каждый LoadBalancer IP на dummy-интерфейс kube-ipvs0 на каждой ноде. В этом кластере — 306 адресов на ноду. С strictARP: false — дефолт kubespray и kubeadm, стоявший с установки, — ядро держит arp_ignore=0, и все ноды охотно отвечают на ARP за эти IP, а не только speaker MetalLB, владеющий анонсом.
Какой MAC закэширует клиент — гонка. Отсюда и «работало, потом перестало»: первый ответ случайно пришёл с нужной ноды, следующий — нет. Клиентам за роутером везло с его кэшем, поэтому проблема так долго была невидимой. Внешний HAProxy раньше ходил только на NodePort’ы мастеров; миграция стала первым случаем, когда кто-то в этом сегменте сам резолвил LB IP.
MetalLB пишет об этом прямо: L2-режим с kube-proxy в ipvs требует strictARP: true.
Починка и как я её проверил
- Бэкап ConfigMap
kube-proxy. - Поставить
strictARP: trueи перезапустить DaemonSet kube-proxy. kube-proxy выставитarp_ignore=1иarp_announce=2, и на ARP будет отвечать только анонсирующий speaker. - Сбросить протухшую neighbour-запись на VM HAProxy.
- Пробовать целую минуту, а не верить одному запросу:
for i in $(seq 1 60); do timeout 2 bash -c "</dev/tcp/<lb-ip>/443" && echo ok || echo FAIL; sleep 1; done | sort | uniq -c
Потом я заново переключил хосты по одному, с периодом выдержки после каждого. kubeadm при апгрейдах читает KubeProxyConfiguration из того же ConfigMap, так что настройка пережила хопы Kubernetes, которые шли в тот же день.
Выводы
- С MetalLB L2 и kube-proxy ipvs первым делом проверь
strictARP. Если тамfalse, каждая нода — кандидат на ответ за каждый LoadBalancer IP. externalTrafficPolicy: Localпревращает неправильный ARP-ответ в тихий дроп.- В ARP-гонке один успешный запрос ничего не доказывает. Пробуй во времени и выдерживай каждое изменение.
- Латентный сетевой баг часто ждёт первого нового клиента в сегменте. Миграции — как раз момент, когда он появляется.