← Все статьи

Минуту работало, потом таймауты: 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.

Починка и как я её проверил

  1. Бэкап ConfigMap kube-proxy.
  2. Поставить strictARP: true и перезапустить DaemonSet kube-proxy. kube-proxy выставит arp_ignore=1 и arp_announce=2, и на ARP будет отвечать только анонсирующий speaker.
  3. Сбросить протухшую neighbour-запись на VM HAProxy.
  4. Пробовать целую минуту, а не верить одному запросу:
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-гонке один успешный запрос ничего не доказывает. Пробуй во времени и выдерживай каждое изменение.
  • Латентный сетевой баг часто ждёт первого нового клиента в сегменте. Миграции — как раз момент, когда он появляется.