Перейти к содержимому
← Блог

У режима ipvs теперь есть дата удаления.

В Kubernetes 1.37 объявлено ровно одно устаревание, и это режим ipvs в kube-proxy. Выключен по умолчанию в 1.40, код удалён в 1.43. Здесь — переключение на nftables, требование к ядру, четыре изменения поведения и ловушка, которая достаёт даже кластеры без ipvs.

·24 мин чтения
  • Kubernetes
  • Сети
  • Linux
  • nftables

Kubernetes v1.37 вышел 26 августа 2026 года с 67 улучшениями, и ровно одно из них — объявление об устаревании: режим ipvs в kube-proxy. На фоне gang scheduling или сертификатов для подов это заголовок помельче — и эксплуатационная задача заметно покрупнее, потому что это единственный пункт релиза, который заканчивается удалением кода из плоскости данных, работающей в продакшене прямо сейчас. Расписание уже записано: feature gate в 1.37, выключение по умолчанию в 1.40, удаление pkg/proxy/ipvs в 1.43.

Обложка из трёх панелей. Левая панель под заголовком «mode: ipvs» показывает три слоя друг над другом: виртуальные серверы IPVS, фиктивный интерфейс kube-ipvs0, на котором висят адреса ClusterIP, и под ними более широкий слой с подписью iptables и ipset; поверх — красный штамп «код удалён в 1.43». Центральная колонка — таймлайн из пяти остановок: 1.35 предупреждение, 1.37 feature gate, 1.40 выключен по умолчанию, 1.43 код удалён, 1.46 gate удалён. Правая панель под заголовком «mode: nftables» показывает два блока с подписями table ip kube-proxy и table ip6 kube-proxy над строкой «требуется Linux 5.13», а под ними — зелёную полосу с надписью «следующий режим по умолчанию».
Что именно снимают с эксплуатации: три слоя состояния в ядре, из которых IPVS — только верхний. На их месте остаются две таблицы nftables и требование к версии ядра.

У этой задачи есть версия на одну строчку в ConfigMap и версия на неудачный вторник, и разница целиком в том, что вы проверите первым. Дальше — вся картина: пять этапов KEP и какие из них даты, а какие намерения; как выяснить, что кластер действительно исполняет (включая ответ, который хуже, чем ipvs); почему этот режим никогда не был тем побегом от iptables, каким его запомнили; почему планировщики IPVS не делают работу, ради которой их держат; требование к ядру и какие образы узлов ему соответствуют; четыре задокументированных изменения поведения; раскатка по одному узлу; состояние, которое приходится подчищать руками; скрипт проверки, возвращающий ненулевой код; и откат, который честно укладывается в две команды.

В 1.37 объявили одно устаревание, и это оно

Ни один из симптомов ниже не произносит слова устаревание, и именно поэтому его замечают поздно — обычно тот, кто обновляется до 1.40, через три релиза после первого предупреждения. Режим не деградирует. Он безупречно работает до того релиза, в котором его больше нет, а предупреждения всё это время уходят в журнал, который никто не грепает.[rel137]

Что вы видитеЧто это на самом деле значитГде разбирается
kube-proxy при каждом старте пишет The ipvs proxier is now deprecatedЭтап 1 KEP-5495, вышедший в Kubernetes 1.35. Ничего не сломано; часы пошли два релиза назад.Пять этапов
mode: в ConfigMap kube-proxy пустКластер не выбрал плоскость данных. Апстрим сменит рекомендуемое значение по умолчанию с iptables на nftables, и незафиксированный кластер поедет за ним.Какой режим
После обновления до 1.40 kube-proxy падает с ошибкой и перечислением допустимых режимовЭтап 3. Gate KubeProxyIPVS теперь по умолчанию false, и никто не вернул его обратно.Пять этапов
Сразу после переключения NodePort перестал отвечать на вторичном интерфейсеРежим nftables по умолчанию использует --nodeport-addresses primary. Это самая частая регрессия в этом переходе, с большим отрывом.Что меняется
Трафик к одному ClusterIP теряется, и только на одном узлеНа оставшемся kube-ipvs0 висят устаревшие адреса, а правил IPVS за ними нет. Узел по-прежнему отвечает на ARP за адрес, который больше не обслуживает.Остатки

Вторую строку этой таблицы стоит прочитать дважды, потому что она не имеет отношения к IPVS и достаёт кластеры, которые им никогда не пользовались. Режим по умолчанию в Kubernetes 1.37 — по-прежнему iptables, и документация прямо говорит, что в одном из будущих релизов это значение сменится на nftables. Если в конфигурации kube-proxy режим не назван явно, вы находитесь на iptables не по решению, а по совпадению — вы находитесь на том, что апстрим рекомендует сегодня, и замена плоскости данных у вас назначена на чужой календарь.[vips]

Собственная рекомендация апстрима и самое дешёвое действие во всей этой статье: чтобы бэкенд прокси в кластере не сменился неожиданно во время обновления, убедитесь, что во всех кластерах конфигурация kube-proxy явно указывает используемый режим. Это правка на пять минут, и её стоит сделать сегодня — независимо от того, на каком режиме вы в итоге окажетесь.

Пять этапов, и какие два из них — даты

KEP-5495 раскладывает это на пять этапов, и полезная дисциплина — отделить объявления от дат. Этапы 1 и 2 уже произошли и ничего не изменили в том, как работает кластер. Этап 3 меняет значение по умолчанию, то есть меняет то, что произойдёт с кластером, где никто ничего не делал. Этап 4 удаляет код, и он единственный необратимый.[kep5495]

ЭтапKubernetesЧто меняетсяЧто требуется от вас
11.35kube-proxy в режиме ipvs предупреждает при старте; документация помечена как устаревшая. Исправления nftables бэкпортированы в 1.33 и 1.34, чтобы старые кластеры тоже могли перейти.Заметить. И больше ничего.
21.37Добавлен feature gate KubeProxyIPVS: статус GA, по умолчанию true. Поведение не меняется.Спланировать. Это последний тихий релиз.
31.40Значение gate по умолчанию становится false. kube-proxy, запущенный в режиме ipvs без переопределения, падает с ошибкой и перечислением допустимых режимов.Закончить — или выставить gate и купить три минорных релиза.
41.43pkg/proxy/ipvs удаляется из дерева. Feature gate больше ничего не вернёт; остатки упоминаний уходят из документации.Ничего. Режима ipvs не существует.
51.46Удаляется сам feature gate.Важно только инструментам, читающим gates.

Две строки заслуживают примечания. Этап очистки в 1.46 существует в KEP и почти ни в одном пересказе этого устаревания, и важен он только тем, кто пишет инструменты, читающие feature gates: до этого момента gate — настоящий, перечисленный, GA-шный. И причину, которую назвала SIG Network, знать стоит, потому что дело не в производительности: в группе нет мейнтейнеров, знакомых с кодом бэкенда ipvs, и всем, кто приносит баги по ipvs, уже некоторое время предлагают переходить на nftables. Бэкенд, который никто не чинит, — это риск независимо от того, что когда-то показывали его бенчмарки.[depol][k8srel]

В каком режиме этот кластер работает на самом деле

Начните с того, что настроено на самом деле, а не с того, что написано в регламенте: на любом кластере старше пары лет это два разных документа. Ответов три, и интересен третий: ipvs, iptables или ничего.[kpcfg]

# The one-liner from the v1.37 release announcement. On a kubeadm-built cluster
# the whole of kube-proxy's configuration lives in one ConfigMap key.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep 'mode:'
# mode: ipvs

# Careful with the empty answer, because it is the most common one and it does
# NOT mean "iptables forever". An unset mode means "whatever kube-proxy decides
# is the recommended default", and upstream says in as many words that a future
# release will change that default from iptables to nftables. If this comes back
# blank, you have a data-plane change scheduled that nobody in your team chose.
#   mode: ""     <- pin it, whichever mode you intend to be on

# Managed clusters and non-kubeadm installers do not necessarily use that
# ConfigMap. Ask the process instead - this is true wherever the config came from.
kubectl -n kube-system get ds kube-proxy \
  -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n'

# And from the node, which is the only answer that cannot be out of date:
ps -o args= -C kube-proxy
ipvsadm -L -n --daemon 2>/dev/null; ipvsadm -L -n | head -20
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
# TCP  10.96.0.1:443 rr
#   -> 192.168.4.11:6443            Masq    1      3          0

# Not every cluster runs kube-proxy at all. If this returns nothing, your CNI
# is implementing Services itself and none of this deprecation reaches you.
kubectl -n kube-system get ds -l k8s-app=kube-proxy

Затем найдите предупреждение — оно лежит в ваших журналах с Kubernetes 1.35 и служит самым дешёвым доказательством того, что это вас касается. Feature gate, добавленный в 1.37, стоит отыскать тем же заходом: не потому, что его нужно выставлять — он включён по умолчанию и в этом релизе ничего не меняет, — а потому, что именно его вам предложат в 1.40, и лучше заранее знать, что он отодвигает срок на три минорных релиза, а не отменяет его.[gates]

# Stage 1 of the deprecation landed in Kubernetes 1.35: kube-proxy logs a
# warning on every start in ipvs mode. It has been in your logs for two releases.
kubectl -n kube-system logs ds/kube-proxy --tail=-1 --prefix \
  | grep -i -m5 'ipvs.*deprecat'
# The ipvs proxier is now deprecated and may be removed in a future release.
# Please use 'nftables' instead.

# Stage 2 is what 1.37 added: a feature gate. Today it is on by default, so
# nothing changes yet. In 1.40 the default flips and kube-proxy in ipvs mode
# exits with an error unless the gate is set back by hand; in 1.43 the gate
# cannot save you because the code is gone.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -A3 featureGates

# What the 1.40 override would look like. Write this down as the thing you are
# choosing NOT to depend on, rather than as a plan:
#
#   featureGates:
#     KubeProxyIPVS: true
#
# It buys three minor releases, or roughly nine months at the current cadence,
# and it stops working entirely in 1.43.

# Count how much of the fleet this is really about, because in most clusters
# the answer is "some node pools, historically".
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,'\
'KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage'

Что такое режим ipvs — в отличие от того, каким его помнят

Стоит быть точным в том, что именно снимают с эксплуатации, потому что народная версия этой истории неверна ровно так, чтобы поменять решение. Режим ipvs появился в Kubernetes 1.8, чтобы уйти от стоимости списка правил, растущего пропорционально числу Service. С этим он справился. Чего он никогда не делал — не заменял iptables: API IPVS в ядре сам по себе не выражает весь Service API Kubernetes — решения о маскараде, фильтрацию NodePort, ограничение источников для LoadBalancer, поведение reject для Service без эндпоинтов, — поэтому режим управляет iptables и ipset под таблицей IPVS. Анонс v1.37 говорит ровно это. Посчитайте правила на своём узле, если не хотите верить на слово.[kep3866]

# Three separate pieces of kernel state, which is the first surprise for anyone
# who believed ipvs mode meant "no iptables".

# (a) The IPVS virtual servers. This part is what people think of as ipvs mode.
ipvsadm -L -n | wc -l

# (b) A dummy interface holding every ClusterIP - and every LoadBalancer IP -
#     as a /32 on the node. This is why the node answers ARP for addresses it
#     does not own, and therefore why MetalLB in layer-2 mode needs strictARP.
ip -brief addr show kube-ipvs0 | head
# kube-ipvs0  DOWN  10.96.0.1/32 10.96.0.10/32 10.107.44.9/32 ...

# (c) The iptables rules and ipsets that ipvs mode drives underneath, because
#     the kernel IPVS API on its own cannot express masquerade decisions,
#     LoadBalancer source ranges, NodePort filtering or the reject rules for a
#     Service with no endpoints. Count them before you claim to be iptables-free.
ipset list -name | grep -c '^KUBE-'
iptables-save -t nat | grep -c '^-A KUBE-'

# The scheduler in use, which is the setting this whole mode is usually kept for:
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -A4 '^ipvs:'
# ipvs:
#   scheduler: "lc"
#   strictARP: true

# One field, for the whole cluster. There is no per-Service scheduler in
# Kubernetes: it is not in the Service API, and kube-proxy does not read one.
# Whatever is on that line is what every Service on every node gets.

Вторая половина народной версии — планировщики, и именно из-за них большинство кластеров, до сих пор сидящих на ipvs, до сих пор на нём сидят. Кто-то много лет назад выставил scheduler: "lc", и с тех пор считается, что кластер балансирует по наименьшему числу соединений. Он этого не делает, и причина архитектурная, а не в баге. kube-proxy работает на каждом узле, и у каждого экземпляра своя таблица IPVS, которая считает только те соединения, которые открыл этот узел. «Наименьшее число соединений» поэтому вычисляется в пределах узла-клиента: на двадцати нагруженных узлах вы получаете двадцать независимых локальных решений, а не одно общее, и трафик, приходящий снаружи кластера, не попадает ни в один из этих счётчиков. SIG Network прямо указала на это заблуждение, когда оформляла устаревание.[vips]

# The claim to test: "we run lc so connections go to the least loaded pod".
#
# Every node runs its own kube-proxy with its own IPVS table, and that table
# only counts connections this node opened. Ask two nodes about the same
# Service and the connection counts will not agree - because they are answers
# to different questions.

SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
for node in $(kubectl get nodes -o name | head -3); do
  echo "== ${node#node/}"
  kubectl debug "$node" -q -it --image=busybox --profile=general -- \
    chroot /host ipvsadm -L -n -t "$SVC_IP:8080" 2>/dev/null | tail -n +4
done
# == node-01
#   -> 10.244.1.7:8080   Masq  1  118  4
#   -> 10.244.2.4:8080   Masq  1    0  0      <- zero, from THIS node
# == node-02
#   -> 10.244.1.7:8080   Masq  1    2  1
#   -> 10.244.2.4:8080   Masq  1   96  3      <- the other pod, same Service

# Neither node is wrong. "Least connections" is being computed per client node,
# so with N busy client nodes you get N independent local decisions, not one
# global one. Add a client outside the cluster, or a client behind a
# LoadBalancer that lands on a different node, and IPVS never sees it at all.
#
# Clean up the debug pods. `kubectl debug node/...` names them
# node-debugger-<node>-<suffix> and sets no label, so match on the name.
kubectl get pods -o name | grep '^pod/node-debugger-' | xargs -r kubectl delete

# The two things people actually want from a scheduler have supported answers
# that survive this migration, and neither of them is an IPVS scheduler:
#   sticky clients   -> Service .spec.sessionAffinity: ClientIP
#   keep it local    -> Service .spec.internalTrafficPolicy: Local
kubectl get svc -A -o json | jq -r '
  .items[] | select(.spec.sessionAffinity == "ClientIP")
  | "\(.metadata.namespace)/\(.metadata.name) sessionAffinity=ClientIP"'
Планировщик IPVSЧто он делает на одном узлеЧто это значит для кластера
rr (по умолчанию)Круговой перебор по списку эндпоинтов этого узла.Функционально то же, что уже делают режимы iptables и nftables, выбирая бэкенд случайно. Здесь ничего не теряется.
lc, wlc, sed, nqНаименьшее число активных соединений — в счёте этого узла.Это не общекластерный least-connections. N нагруженных узлов-клиентов принимают N независимых локальных решений, а внешнего трафика нет ни в одном из счётчиков.
sh, dhХеш по адресу источника или назначения.Часто путают с липкостью сессии. Поддерживаемый эквивалент — .spec.sessionAffinity: ClientIP, он задаётся на Service и переживает переход.
lblc, lblcrLeast connection с учётом локальности.Локальность в Kubernetes — это .spec.internalTrafficPolicy: Local, о котором API действительно знает и которого планировщик не видит.
mh (Maglev)Консистентное хеширование.kube-proxy всегда выставляет mh-port и никогда не включает mh-fallback, так что на практике это хеш по источнику с портами.

Планировщика на уровне Service нет и никогда не было. ipvs.scheduler — одно поле в конфигурации kube-proxy, и оно действует на каждый Service на каждом узле; оно не входит в Service API, и никакая аннотация его не меняет. На этом стоит задержаться, потому что отсюда следует главное: у двух вещей, ради которых на самом деле тянутся к планировщикам, — липкость клиента и локальность узла — есть поддерживаемые ответы в самом API, и переход их не трогает: sessionAffinity: ClientIP и internalTrafficPolicy: Local.[svc][stp]

РежимСтатус в 1.37ЯдроИспользует iptables под собойЧем заканчивается
iptablesЗначение по умолчанию. Не устарел.любоеДа, по определениюВ какой-то момент перестанет быть значением по умолчанию; удаление не объявлено.
ipvsУстарел с 1.35; feature gate добавлен в 1.37любоеДа — маскарад, фильтрация NodePort, ограничение источников LoadBalancer, правила rejectВыключен по умолчанию в 1.40, код удалён в 1.43.
nftablesСтабилен с 1.33; рекомендованная замена ipvs5.13 или новееНетСтанет значением по умолчанию в одном из будущих релизов.
kernelspaceТолько узлы WindowsНичего из перечисленного его не касается.

Потянут ли эти узлы режим nftables

Теперь единственное жёсткое ограничение. Режим nftables требует Linux 5.13 или новее, частичной поддержки нет и запасного пути нет — ниже этой версии kube-proxy в таком режиме просто не запустится. Именно эта проверка решает, будет ли переход правкой ConfigMap или проектом по пересборке образов узлов, так что делайте её до того, как писать любой план.[vips]

#!/usr/bin/env bash
# Precheck. nftables mode needs Linux 5.13 or newer; there is no partial
# support and no fallback - kube-proxy will not start in nftables mode below it.
set -euo pipefail

need_major=5 need_minor=13
fail=0

while read -r node kernel os; do
  ver=${kernel%%-*}                       # 6.8.0-51-generic -> 6.8.0
  IFS=. read -r maj min _ <<<"$ver"
  if (( maj > need_major )) || { (( maj == need_major )) && (( min >= need_minor )); }; then
    printf '  ok    %-22s %s\n' "$node" "$kernel"
  else
    printf '  TOO OLD %-20s %-24s %s\n' "$node" "$kernel" "$os"
    fail=1
  fi
done < <(kubectl get nodes -o custom-columns=\
'NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage' \
  --no-headers)

(( fail == 0 )) && echo "all nodes can run nftables mode" \
                || echo "some nodes cannot: rebuild the image or stay on iptables mode"

# On a node that reports too old, the answer is almost never "patch the kernel".
# It is "this node image is end of life". Check what the distro offers before
# planning anything: an Ubuntu 20.04 node on the HWE kernel is fine, the same
# release on the GA kernel is not.
uname -r
nft --version
Образ узлаЯдро по умолчаниюРежим nftables
RHEL 9, Rocky Linux 9, AlmaLinux 95.14Да
RHEL 106.12Да
RHEL 8, CentOS 74.18 и старшеНет — это пересборка образа узла
Ubuntu 24.04 LTS6.8Да
Ubuntu 22.04 LTS5.15Да
Ubuntu 20.04 LTS5.4 GA, 5.15 с HWEТолько на ядре HWE
Debian 126.1Да
Debian 115.10Нет
Amazon Linux 20236.1Да

Если часть узлов не проходит порог, честный вывод обычно в том, что образ узла достиг конца жизни, а не в том, что ядру нужен патч, и правильное лечение — пересборка. Если пересобрать до 1.40 не получится, запасной вариант — режим iptables, а не feature gate: бэкенд iptables не объявлен устаревшим, его производительность существенно выросла уже после появления режима ipvs, и апстрим рекомендует именно его для этого случая. Тем, кто взвешивает сроки, стоит добавить ещё один довод: в устаревании отмечено, что все ядра, слишком старые для режима nftables, покидают долгосрочную поддержку к концу 2026 года, — то есть у возражения про ядро есть собственный срок годности.[kernrel][kep5495]

Четыре вещи после переключения ведут себя иначе

Меняется поведение в четырёх местах, и три из них задокументированы под заголовком — переход с режима iptables на nftables, — который пользователям ipvs незачем было читать. Здесь они применимы один в один, потому что это свойства точки назначения, а не точки отправления.[vips]

ПоведениеРежим iptables / ipvsРежим nftablesЧто с этим делать
Адреса, на которых слушают NodePortВсе локальные адреса, если не суженоПо умолчанию --nodeport-addresses primaryПроверьте, какие адреса реально используются, и задайте параметр явно, если основного мало.
NodePort на 127.0.0.1В режиме iptables работает по умолчаниюНедоступно; возвращено в 1.37 за альфа-гейтомПрочитайте kubeproxy_iptables_localhost_nodeports_accepted_packets_total прежде, чем решить, что вам это не нужно.
Локальный файрволkube-proxy добавляет разрешающие правила на каждый NodePortНе делает ничегоРазрешите диапазон NodePort в собственном файрволе хоста.
Баг сброса conntrack до 6.1Обход установленПо умолчанию не установленПроверьте kubeproxy_iptables_ct_state_invalid_dropped_packets_total; если не ноль, несите с собой --conntrack-tcp-be-liberal.
Ваши собственные правила на цепочках kube-proxyKUBE-SERVICES, KUBE-SEP-* существуют в iptablesСостояние живёт в table ip kube-proxy / table ip6 kube-proxyЭто никогда не было API. Подцепляйте свою таблицу на том же приоритете netfilter.
  • NodePort перестают слушать на всех локальных адресах. В режимах iptables и ipvs Service типа NodePort доступен на всех локальных IP, если вы это не сузили. Режим nftables по умолчанию использует --nodeport-addresses primary, то есть основной адрес узла IPv4 и/или IPv6 из объекта Node — и больше ничего. Всё, что ходит на NodePort через вторую сетевую карту, служебный адрес или плавающий VIP, перестаёт работать. Если старый охват нужен, задайте параметр явно — 0.0.0.0/0 его возвращает, — но сначала проведите аудит, потому что новое значение по умолчанию апстрим считает тем, чего люди на самом деле хотели.
  • NodePort на localhost — отдельный случай, и в 1.37 он изменился. Подключение к 127.0.0.1:<nodePort> работало в режиме iptables и в режиме nftables не работало вовсе. Начиная с Kubernetes 1.37 оно возможно — за альфа-гейтом KubeProxyNFTablesLocalhostNodePorts и при nodePortAddresses, равном primary,localhost. Прежде чем решать, нужно ли вам это, посмотрите счётчик: kube-proxy всё это время считал пакеты, принятые на NodePort через loopback.
  • kube-proxy перестаёт открывать за вас файрвол. Режим iptables добавляет разрешающие правила для каждого NodePort — из соображения, что слишком строгий локальный файрвол иначе их заблокирует. Против файрвола на основе nftables такой подход не работает в принципе, поэтому режим nftables здесь не делает ничего. Если на хосте есть файрвол, разрешать диапазон NodePort теперь должен он сам, — что, пожалуй, и было правильным устройством с самого начала, но это всё равно изменение.
  • Обходной путь для conntrack по умолчанию не ставится. В ядрах до 6.1 есть баг, из-за которого длинные TCP-соединения к адресам Service могут обрываться. Режим iptables ставит обход; позже выяснилось, что у самого обхода есть свои проблемы, поэтому режим nftables его не ставит. Нужен ли он вам — вопрос измеримый, а не вкусовой: счётчик есть в метриках. Если нужен, поддерживаемый способ вернуть поведение — --conntrack-tcp-be-liberal.
  • Всё ваше, что цеплялось за цепочки kube-proxy, сломано. Если скрипт файрвола, хук CNI или правило мониторинга обращались к KUBE-SERVICES или KUBE-SEP-* по имени, этих цепочек больше нет: состояние kube-proxy теперь живёт в table ip kube-proxy и table ip6 kube-proxy. Это никогда не было поддерживаемым — у апстрима есть отдельный пост ровно с таким заголовком, — но грепнуть по этому стоит до раскатки, а не после, потому что ломается оно молча.
# The behaviour difference most likely to page you. In ipvs and iptables mode,
# NodePort Services are reachable on every local address unless you said
# otherwise. nftables mode defaults to --nodeport-addresses primary: the node's
# primary IPv4 and/or IPv6 address from the Node object, and nothing else.
#
# So the question to answer before the switch is: does anything reach a NodePort
# on a secondary address, a VIP, a management NIC, or on loopback?

# What is currently configured, if anything:
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -i nodePortAddresses
# (empty means "all local addresses")

# Which NodePorts exist at all, and who might be pointed at them:
kubectl get svc -A -o json | jq -r '
  .items[] | select(.spec.type == "NodePort" or .spec.type == "LoadBalancer")
  | .spec.ports[]? | select(.nodePort)
  | "\(.nodePort)\t\(.protocol)"' | sort -u

# Localhost NodePorts are the sharp edge: health checks, sidecars and a
# surprising number of monitoring agents connect to 127.0.0.1:<nodePort>.
# kube-proxy counts them for you, and a non-zero value means something out
# there depends on it.
kubectl -n kube-system exec ds/kube-proxy -- \
  wget -qO- http://127.0.0.1:10249/metrics \
  | grep kubeproxy_iptables_localhost_nodeports_accepted_packets_total
# kubeproxy_iptables_localhost_nodeports_accepted_packets_total 41822

# Kubernetes 1.37 gives that case a way out, as an alpha feature gate. If you
# need it, you need it on 1.37+ and you need both halves:
#   featureGates:
#     KubeProxyNFTablesLocalhostNodePorts: true
#   nodePortAddresses: ["primary", "localhost"]

# And the conntrack workaround: iptables mode installs one for a pre-6.1 kernel
# bug that resets long-lived TCP connections. nftables mode does not, by default.
# Non-zero here means you are relying on it - carry --conntrack-tcp-be-liberal.
kubectl -n kube-system exec ds/kube-proxy -- \
  wget -qO- http://127.0.0.1:10249/metrics \
  | grep kubeproxy_iptables_ct_state_invalid_dropped_packets_total

Общее у всех пяти одно: ни один не является отказом при старте. kube-proxy поднимается, рапортует, что здоров, обслуживает почти весь трафик правильно — и один конкретный путь перестаёт работать. Поэтому предварительная проверка ниже стоит дороже самой раскатки, и поэтому первый узел стоит в одиночестве целый рабочий день.[reset][ctsysctl]

Переключение, по одному узлу за раз

Сама правка — три строки. Дисциплина в том, чтобы удалить блок ipvs, а не оставить его рядом с новым режимом, потому что мёртвая конфигурация — это то, из-за чего следующий человек решит, что кластер всё ещё на ipvs. Одна вещь, которую надо проверить на кластерах, собранных kubeadm: этот ConfigMap заново генерируется при kubeadm upgrade, поэтому правка, сделанная только через kubectl, может молча откатиться на следующем обновлении. Внесите её и в конфигурацию кластера.[kubeadm]

# The change is three lines, and the discipline is in what you delete.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' > kube-proxy.conf.bak
cp kube-proxy.conf.bak kube-proxy.conf

# --- before ----------------------------------------------------------------
#   mode: ipvs
#   ipvs:
#     scheduler: "lc"
#     strictARP: true
#     syncPeriod: 30s
#
# --- after -----------------------------------------------------------------
#   mode: nftables
#   nftables:
#     minSyncPeriod: 1s
#     syncPeriod: 30s
#
# Delete the whole ipvs block rather than leaving it. In nftables mode it is
# dead configuration: scheduler has no equivalent and no effect, and strictARP
# was only ever there to stop the node answering ARP for the ClusterIPs that
# ipvs mode bound onto kube-ipvs0 - an interface nftables mode never creates.
# Leaving it behind is how the next person concludes the cluster is still ipvs.

# Apply. Note that on a kubeadm cluster this ConfigMap is regenerated by
# `kubeadm upgrade`, so make the same change in the cluster configuration or
# the next upgrade will quietly put ipvs back.
kubectl -n kube-system create configmap kube-proxy \
  --from-file=config.conf=kube-proxy.conf \
  --dry-run=client -o yaml | kubectl apply -f -

Затем раскатывайте так, как раскатывают всё, что владеет плоскостью данных на каждом узле: один узел, потом пул, потом парк. Этот случай необычно дёшев для канареечной проверки, потому что состояние целиком принадлежит узлу и заново строится из API-сервера при каждом старте: портить нечего, ничего общего нет, а узел, который не может работать в новом режиме, падает на старте, и версия ядра будет в тексте ошибки.[drain]

# Do not restart the DaemonSet across the fleet. kube-proxy owns the data plane
# on every node it runs on; a bad rollout is a cluster-wide outage, and this one
# is cheap to canary because the state is per node.

# Pause the DaemonSet so the ConfigMap change does not roll on its own.
kubectl -n kube-system patch ds kube-proxy \
  -p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":1}}}}'

# One node. Cordon it, move the workloads off, restart only that pod.
NODE=node-07
kubectl cordon "$NODE"
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data --timeout=5m
kubectl -n kube-system delete pod \
  --field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy

# Watch it come up in the new mode. A node that cannot run nftables mode fails
# here, loudly, with the kernel version in the message - which is the correct
# place to find that out.
kubectl -n kube-system logs -f --tail=40 \
  "$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
     --field-selector "spec.nodeName=$NODE" -o name)"

kubectl uncordon "$NODE"

# Give it real traffic and a working day before the second node. The failures
# this migration produces are not startup failures; they are "one client on a
# secondary interface stopped reaching a NodePort", and that takes a shift to
# surface. Then a pool, then the fleet.

Что ipvs оставляет после себя

Чистка — то место, где этот переход отличается от всех прежних смен режима kube-proxy, и отличается в лучшую сторону. Раньше kube-proxy пытался прибираться за другими режимами и перестал — эту логику убрал KEP-2448, — потому что бэкенды iptables, ipvs и userspace писали в частично общие цепочки, и чистка правил одного режима удаляла правила работающего. Режим nftables не делит ничего: всё его состояние — в двух собственных таблицах. Поэтому переключение в эту сторону спроектировано так, чтобы удалять старые правила при старте, и поэтому ручная чистка безопасна, когда она всё же нужна.[kep2448][ipvsadm]

# kube-proxy in nftables mode is designed to remove the iptables and ipvs rules
# it finds on startup - the modes do not share state, which is exactly why this
# direction is safe when switching between the iptables-family modes was not.
# Verify rather than assume, on the first node, before the second one.

ipvsadm -L -n | tail -n +4 | wc -l         # want 0
ip link show kube-ipvs0 2>/dev/null        # want "does not exist"
ipset list -name | grep -c '^KUBE-'        # want 0
iptables-save -t nat | grep -c '^-A KUBE-' # want 0 (or only your own rules)

# If something survived - an older kube-proxy, a node that was rebooted mid-way,
# a third party that wrote into those chains - clear it explicitly. Every one of
# these is safe once kube-proxy is confirmed running in nftables mode on the node.
ipvsadm --clear
ip link delete kube-ipvs0                  # recreated only by ipvs mode
for s in $(ipset list -name | grep '^KUBE-'); do ipset destroy "$s"; done

# The stale kube-ipvs0 addresses are the ones that actually hurt. Left in place
# with no IPVS rules behind them, the node still claims those ClusterIPs and
# still answers ARP for them, and traffic that lands there is dropped rather
# than redirected. That is a black hole that looks like an application problem.

# The strictARP sysctls are set at runtime and do not revert on their own. They
# are harmless, but if you want the node back to stock:
sysctl -w net.ipv4.conf.all.arp_ignore=0
sysctl -w net.ipv4.conf.all.arp_announce=0

# There is also a supported flush. It is documented as cleaning up iptables and
# ipvs rules, so it is the right tool for this direction and the wrong one for
# the other. Run it with kube-proxy stopped on that node.
kube-proxy --cleanup

Один остаток заслуживает больше внимания, чем прочие. kube-ipvs0 — фиктивный интерфейс, на который режим ipvs вешает каждый ClusterIP и каждый адрес LoadBalancer как /32 на узле, и именно поэтому MetalLB в режиме L2 требует strictARP для кластеров на ipvs. Режим nftables такого интерфейса не создаёт, так что требование перестаёт применяться, а настройка превращается в мёртвую конфигурацию. Но если интерфейс переживёт переключение вместе с адресами и без правил IPVS за ними, узел продолжит заявлять эти ClusterIP и отвечать на ARP по ним, роняя трафик. Это чёрная дыра на одном узле, выглядящая как плавающий отказ приложения, — и это лучший из существующих доводов проверять чистку, а не предполагать её.[metallb][chains]

# Everything kube-proxy now owns lives in two tables of its own, one per IP
# family, which is the property that makes the whole thing inspectable.
nft list tables
# table ip kube-proxy
# table ip6 kube-proxy

# The Service map, which is the equivalent of what `ipvsadm -L -n` used to show.
# Note that it is a map lookup rather than a rule chain: this is the performance
# argument for the new backend, and it is visible in the output.
nft list table ip kube-proxy | head -40

# One Service end to end:
SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
nft list table ip kube-proxy | grep -A3 "$SVC_IP"

# Live rule changes, which is the closest thing to watching kube-proxy think:
nft monitor rules

# What kube-proxy does NOT own any more - and must not, if the cleanup worked:
nft list ruleset | grep -c 'KUBE-SVC\|KUBE-SEP'   # want 0
ipvsadm -L -n | tail -n +4 | wc -l               # want 0

# A standing caution that predates all of this: kube-proxy's chains and tables
# are not API. If something of yours matched on KUBE-SERVICES by name, it is
# broken now, and it was unsupported before. Hook your own table into the same
# netfilter priorities instead of writing into kube-proxy's.

Проверять, а не надеяться

У проверки в этом переходе особая форма, продиктованная характером отказов: почти всё, что идёт не так, оставляет kube-proxy запущенным и здоровым. Поэтому проверка «под в состоянии Running» не доказывает ровно ничего. Скрипт ниже проверяет режим, который kube-proxy действительно выбрал, а не тот, что записан в ConfigMap; что состояние ipvs не пережило переключение; что фиктивного интерфейса нет; что синхронизация правил проходит успешно, — а затем делает то, чего не сделает никакой осмотр: открывает настоящее соединение к настоящему Service из пода на этом узле.[dbgsvc]

#!/usr/bin/env bash
# Run on a migrated node, from a machine with kubectl and cluster access.
# Exits non-zero on anything that would be silently wrong. The failures this
# migration produces do not stop kube-proxy, so "the pod is Running" proves
# nothing at all.
set -uo pipefail
NODE=${1:?usage: verify.sh <node>}
NS=${NS:-default}
rc=0
say() { printf '%-46s %s\n' "$1" "$2"; }
chk() { if [[ $2 == "$3" ]]; then say "$1" "ok"; else say "$1" "FAIL ($2 != $3)"; rc=1; fi; }

POD=$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
      --field-selector "spec.nodeName=$NODE" -o jsonpath='{.items[0].metadata.name}')

# 1. the mode kube-proxy actually chose, not the one in the ConfigMap
mode=$(kubectl -n kube-system logs "$POD" | grep -om1 'Using .* Proxier' | awk '{print $2}')
chk "proxy mode" "$mode" "nftables"

# 2. no ipvs state left on the node
left=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
       chroot /host sh -c 'ipvsadm -L -n 2>/dev/null | tail -n +4 | wc -l' 2>/dev/null | tr -d ' ')
chk "ipvs virtual servers remaining" "${left:-0}" "0"

# 3. the dummy interface is gone, so no stale ClusterIP black holes
iface=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
        chroot /host sh -c 'ip link show kube-ipvs0 >/dev/null 2>&1 && echo present || echo absent' \
        2>/dev/null | tr -d ' ')
chk "kube-ipvs0" "${iface:-absent}" "absent"

# 4. sync is succeeding, which is the counter that replaces "is it up"
m=$(kubectl -n kube-system exec "$POD" -- wget -qO- http://127.0.0.1:10249/metrics)
fails=$(awk '/^kubeproxy_sync_proxy_rules_nftables_sync_failures_total/ {s+=$2} END{print s+0}' <<<"$m")
chk "nftables sync failures" "$fails" "0"
awk '/^kubeproxy_sync_proxy_rules_last_timestamp_seconds/ {print "  last successful sync:", $2}' <<<"$m"

# 5. and the part no inspection can establish: a real connection to a real
#    Service, from a pod on this node, plus a NodePort from off-box.
kubectl -n "$NS" run "nftcheck-$$" --rm -i --restart=Never \
  --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" \
  --image=curlimages/curl -- \
  curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 \
  http://kubernetes.default.svc.cluster.local:443 >/dev/null 2>&1 \
  && say "in-cluster Service connect" "ok" \
  || { say "in-cluster Service connect" "FAIL"; rc=1; }

exit $rc

После этого три счётчика заслуживают алерта на пару недель. Обратите внимание, чего в списке нет: в справочнике метрик Kubernetes есть счётчики для iptables, есть счётчики для nftables и нет ровно ничего для ipvs. Какой бы мониторинг режима ipvs у вас сегодня ни был, вы собрали его сами вокруг ipvsadm, — а это маленькая и очень конкретная иллюстрация того аргумента о сопровождении, который стоит за всем этим устареванием.[metrics]

МетрикаО чём говоритСтоит ли алерта
kubeproxy_sync_proxy_rules_nftables_sync_failures_totalkube-proxy не может записать набор правил, то есть плоскость данных расходится с API-сервером.Любой рост, постоянно.
kubeproxy_sync_proxy_rules_nftables_cleanup_failures_totalОн не может удалить правила, которые считает устаревшими, — обычно остатки прежнего режима или кто-то ещё пишет в те же таблицы.Любой рост в течение двух недель после переключения.
kubeproxy_sync_proxy_rules_last_timestamp_secondsКогда правила узла в последний раз совпадали с API-сервером. Именно эта метрика ловит молчаливый отказ.Больше пяти минут назад.
kubeproxy_iptables_localhost_nodeports_accepted_packets_totalКто-то ходит на NodePort через loopback.Ненулевое значение до переключения — это блокер.
kubeproxy_sync_proxy_rules_duration_secondsСколько занимает полная синхронизация. Аргумент о производительности — если Service у вас достаточно для такого аргумента.Снять базовый уровень до и после; алерт не нужен.
# kube-proxy serves these on 10249 on every node. Three of them are worth an
# alert for the fortnight after the migration; the rest are for the postmortem.

# Sync is failing on this node - the data plane is drifting from the API server:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_sync_failures_total[5m])) > 0

# Cleanup is failing - usually leftovers from the previous mode, or something
# else writing into the same tables:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total[5m])) > 0

# Rules are stale. This is the one that catches the silent failure, because
# kube-proxy stays Running while it happens:
time() - max by (node) (kubeproxy_sync_proxy_rules_last_timestamp_seconds) > 300

# And the pre-migration baseline worth keeping: sync duration before and after.
# In clusters with a few thousand Services the improvement is the point; in a
# cluster with forty Services there is nothing to see and that is fine too.
histogram_quantile(0.99, sum by (le) (rate(kubeproxy_sync_proxy_rules_duration_seconds_bucket[5m])))

# Note what is not in this list. The Kubernetes metrics reference has
# iptables-specific counters and nftables-specific counters and no ipvs-specific
# counter at all. Whatever observability you have for ipvs mode today, you built
# it yourself out of ipvsadm - which is its own argument about maintenance.

Откат — и что он вам покупает

Откат здесь честно дёшев, чего не скажешь про остальные миграции, приходящие на эти же узлы в этом же году. Данные конвертировать не надо, форматов на диске, которые пришлось бы понижать, нет, и состояния, переживающего перезапуск, тоже: всё изменение — это один ключ в ConfigMap плюс состояние в ядре узла, которое kube-proxy заново строит из API-сервера при каждом старте. Вернули старый ключ, удалили под на этом узле — и меньше чем через минуту узел там же, где был.[kep3866]

# Rollback here is genuinely cheap, which is not true of most migrations on
# these nodes. There is no data to convert and no format to downgrade: the whole
# of the change is one ConfigMap key and per-node kernel state that is rebuilt
# from the API server on every start.

kubectl -n kube-system create configmap kube-proxy \
  --from-file=config.conf=kube-proxy.conf.bak \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl -n kube-system delete pod \
  --field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy

# kube-proxy in ipvs mode removes the nftables rules it finds on startup. If it
# does not - and rollback matters most exactly when the new backend is
# misbehaving - the manual version is two commands, because every rule
# kube-proxy owns is inside its own tables:
nft delete table ip kube-proxy
nft delete table ip6 kube-proxy

# Then confirm the old data plane is actually back, rather than assuming:
ipvsadm -L -n | tail -n +4 | wc -l     # want non-zero again
ip -brief addr show kube-ipvs0         # the dummy interface returns

# Be honest about what the rollback bought. ipvs mode is off by default in
# Kubernetes 1.40 and the code is deleted in 1.43. A rollback ends a bad
# maintenance window; it does not move the date.

Две оговорки, и обе про область действия, а не про механику. kube-proxy в режиме ipvs спроектирован так, чтобы удалять найденные при старте правила nftables, но откат важен ровно тогда, когда новый бэкенд ведёт себя плохо, — так что ручную версию стоит знать: каждое правило, которым владеет kube-proxy, лежит в его собственных таблицах, и две команды nft delete table убирают всё. И ясно понимайте, что откат вам покупает. Он закрывает неудачное окно обслуживания. Он не двигает 1.40 и не двигает 1.43.[skew]

Другой ответ: не запускать kube-proxy вовсе

У этого устаревания есть второй ответ, и назвать его стоит, потому что для части кластеров он лучше: перестать запускать kube-proxy. Cilium и Calico реализуют Kubernetes Services напрямую в eBPF — поиск по хеш-таблице вместо списка правил, — и оба документируют работу с удалённым kube-proxy, а не рядом с ним. Если один из них у вас уже стоит, устаревание ipvs может оказаться поводом сделать то, что вы и так собирались сделать.[cilium]

Это гораздо более крупное изменение, чем то, о котором эта статья, и взвешивать его надо соответственно. Замена kube-proxy переносит реализацию Service внутрь CNI, а значит, привязывает вашу плоскость данных к релизному циклу того проекта, к его требованиям по ядру — которые выше 5.13, а не ниже, — и к его инструментам отладки; такое не делают внутри окна устаревания под давлением сроков. Честная последовательность для большинства кластеров такая: переходите на nftables сейчас, потому что это правка ConfigMap, а вопрос про eBPF оценивайте по существу и по собственному графику.[calico][netpol]

В каком порядке всё это делать

В сжатом виде решение сильно меньше статьи. Если ядра 5.13 или новее — это изменение конфигурации с аккуратной раскаткой. Если нет — это проект по пересборке образов узлов, а промежуточный ответ — режим iptables, а не feature gate. А если в конфигурации kube-proxy режим не назван вовсе, именно это стоит починить на этой неделе, независимо от всего остального здесь.[kep5495]

Где вы находитесьЧто делать
Режим ipvs, все узлы на 5.13 или новееПереходите на nftables. Это и есть вся статья: правка ConfigMap плюс аккуратная раскатка.
Режим ipvs, часть узлов ниже 5.13Пересоберите образы этих узлов. Если до 1.40 не успеваете, переведите их в режим iptables, а не выставляйте feature gate: iptables не устарел, и апстрим рекомендует именно его вместо ipvs для старых ядер.
Режим iptables, планов нетЗафиксируйте mode: iptables явно сегодня, чтобы обновление не смогло его сменить, а на nftables переходите, когда позволит порог по ядру.
Режим не задан вовсеЗафиксируйте на этой неделе, какой захотите. Это пункт со сроком, который в вашей организации не выбирал никто.
CNI уже заменяет kube-proxyДелать нечего. Убедитесь, что kube-proxy действительно не запущен, а не предполагайте: частичные замены встречаются.
  1. Зафиксируйте режим, какой бы он ни был. Если mode: в ConfigMap kube-proxy пуст, задайте его явно сегодня. Это единственный пункт списка, который относится и к кластерам, где ipvs нет нигде, и единственный, у которого срок выбрали не вы.
  2. Проверьте ядра до того, как писать план. Одна команда по парку решает, задача это на вечер или на квартал. Узлам ниже 5.13 нужен новый образ, а не патч.
  3. Снимите два счётчика, которые решают судьбу острых углов. И NodePort на localhost, и отбрасывания по невалидному состоянию conntrack отвечаются собственными метриками kube-proxy до того, как вы что-либо меняете. Тем же заходом проведите аудит адресов, на которых слушают NodePort.
  4. Один узел, полный рабочий день, потом пул. Регрессии здесь не про отказ при старте: это один клиент на вторичном интерфейсе, и чтобы это всплыло, нужна смена. Чистку kube-ipvs0 и ipset на первом узле проверьте руками.
  5. Удалите блок ipvs и настройку strictARP. В режиме nftables они не делают ничего, а оставить их — верный способ добиться, чтобы следующий человек решил, что перехода не было. Затем внесите то же изменение туда, откуда генерируется ConfigMap, иначе следующий kubeadm upgrade его отменит.

Это одно из нескольких изменений, приходящих на одни и те же узлы в один и тот же год, и вместе они читаются лучше: переход с Ingress NGINX на Gateway API — вторая половина сетевой работы с той же дисциплиной раскатки; переход с containerd 1.7 на 2.x — среда выполнения под ними, снимаемая с эксплуатации по собственному расписанию; и переход с cgroup v1 на cgroup v2 — изменение на уровне узла, которое Kubernetes уже сделал обязательным. Если вы взвешиваете, сколько из этого вам вообще нужно, когда не стоит использовать Kubernetes — другая сторона того же аргумента.

Частые вопросы

Удалён ли режим ipvs в kube-proxy в Kubernetes 1.37?

Нет. Kubernetes 1.37 добавляет feature gate KubeProxyIPVS со значением по умолчанию true, так что режим ipvs ведёт себя ровно как раньше. Важные даты — это 1.40, когда gate по умолчанию становится false и kube-proxy в режиме ipvs падает с ошибкой, если не переопределить его вручную, и 1.43, когда pkg/proxy/ipvs удаляют и gate уже ничем не помогает. Сам gate убирают в 1.46.

Как узнать, какой режим прокси использует мой кластер?

На кластере, собранном kubeadm: kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:'. Если ответ пуст, кластер режим не выбирал и работает на том, что kube-proxy рекомендует сегодня, — это iptables в 1.37, и в одном из будущих релизов станет nftables. На кластере, который этот ConfigMap не использует, читайте аргументы DaemonSet или запустите ipvsadm -L -n на узле. Ещё один надёжный ответ — журнал kube-proxy: строка вида Using ipvs Proxier показывает режим, который процесс действительно выбрал.

Обязательно ли переходить на nftables или можно на iptables?

Режим iptables — законная точка назначения. Он не устарел, у него нет порога по ядру, а его производительность существенно выросла за годы после появления режима ipvs — апстрим прямо рекомендует его вместо ipvs для систем, слишком старых для режима nftables. Там, где ядро позволяет, nftables — цель лучше, и именно туда движется значение по умолчанию, но «с ipvs на iptables» — это настоящий ответ для старых образов узлов, а не отговорка.

Какая версия ядра нужна режиму nftables в kube-proxy?

Linux 5.13 или новее, и только на узлах Linux. Частичной поддержки нет: ниже этой версии kube-proxy в режиме nftables не запустится. На практике это отсекает RHEL 8, CentOS 7, Debian 11 и Ubuntu 20.04 на GA-ядре и проходит на RHEL 9 и 10, Debian 12, Ubuntu 22.04 и 24.04, а также Amazon Linux 2023. Проверяйте командой kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion, а не по названию дистрибутива.

Потеряю ли я настроенный планировщик IPVS?

Настройку — потеряете, а поведение, которое вы ей приписывали, почти наверняка нет, потому что его и не было. ipvs.scheduler — одно поле на весь кластер, а не на Service, и таблица IPVS каждого узла считает только те соединения, которые открыл этот узел, — то есть lc означает least-connections в пределах узла-клиента, а не по кластеру. Если вам нужна была липкость клиента, это .spec.sessionAffinity: ClientIP на Service. Если локальность — это .spec.internalTrafficPolicy: Local. Переход не трогает ни то, ни другое.

Нужен ли strictARP после перехода на режим nftables?

Нет. strictARP существует потому, что режим ipvs вешает каждый ClusterIP и каждый адрес LoadBalancer на фиктивный интерфейс kube-ipvs0, из-за чего узел отвечает на ARP по адресам, которыми пытается управлять MetalLB. Режим nftables такого интерфейса не создаёт вовсе, так что исходная проблема исчезает, а настройка становится мёртвой конфигурацией — удалите её вместе с остальным блоком ipvs. При этом убедитесь, что kube-ipvs0 действительно исчез на каждом переведённом узле: оставшийся интерфейс со старыми адресами будет проглатывать трафик.

Безопасно ли менять режим на работающем кластере?

Переключение между режимом семейства iptables и nftables так и спроектировано: режим nftables держит всё своё состояние в двух собственных таблицах и удаляет найденные при старте правила iptables и ipvs, а при откате всё работает в обратную сторону. Это принципиально отличается от переключения между iptables и ipvs, которые делили цепочки, — из-за чего автоматическую чистку в kube-proxy и убрали в KEP-2448. Но это всё равно изменение плоскости данных на каждом узле, которого оно касается, так что: cordon, drain, перезапуск одного пода kube-proxy, проверка — и оставьте этот узел в покое на рабочий день.

Что ломается в этом переходе чаще всего?

Доступность NodePort. Режим nftables по умолчанию использует --nodeport-addresses primary, поэтому NodePort, до которого ходили через вторую сетевую карту, служебный адрес, плавающий VIP или 127.0.0.1, перестаёт отвечать — при том что kube-proxy остаётся здоровым, и всё остальное продолжает работать. Оба случая измеримы заранее: по собственным метрикам kube-proxy и по текущему значению nodePortAddresses.

Может, вместо этого заменить kube-proxy на Cilium или Calico eBPF?

Это реальный вариант, и для части кластеров лучший, но это гораздо более крупное изменение, чем смена режима: оно переносит реализацию Service в CNI и привязывает вашу плоскость данных к релизам, требованиям по ядру и инструментам отладки того проекта. Принимать такое решение под давлением устаревания — значит делать всё наоборот. Переход на nftables — правка ConfigMap, которая снимает срок; вопрос про eBPF оценивайте после и по собственному графику.

Как откатиться, если режим nftables ведёт себя плохо?

Верните прежний ключ ConfigMap и удалите под kube-proxy на этом узле; старый режим заново построит своё состояние из API-сервера при старте, так что узел вернётся меньше чем за минуту. Если собственная чистка kube-proxy не отработает — а это ровно тот случай, когда откат и нужен, — то каждое правило, которым он владеет, лежит в его собственных таблицах, так что nft delete table ip kube-proxy и nft delete table ip6 kube-proxy убирают всё. Откат закрывает неудачное окно обслуживания; он не двигает ни 1.40, ни 1.43.

Замечание на уровне релиза, потому что баланс 1.37 не такой, каким его описывают: пады действительно могут застрять в ContainerCreating из-за перехода SELinuxMount в GA, тогда как отказ на cgroup v1 появился в 1.35, ограничение статических подов - в 1.34, а обрыв с containerd ещё впереди, в 1.38. что действительно ломается при обновлении до Kubernetes 1.37 разделяет эти три колонки и даёт аудит, который нужно провести до обновления, а не после.

Источники

Только первичные источники, проверены 27 августа 2026 года. Там, где документация Kubernetes и вторичный пересказ расходятся в номере версии по этому устареванию, верить надо документации и KEP: этапы уже один раз пересматривали.

  1. Kubernetes - Virtual IPs and Service Proxies: the reference page for every kube-proxy mode. It carries the deprecation notice for ipvs mode with the 1.40 and 1.43 dates, the kernel 5.13 requirement for nftables mode, the full list of IPVS schedulers and the ipvs.scheduler field they are set through, the four documented behaviour differences when migrating to nftables, and the sentence that matters most to clusters that are not on ipvs at all: the default mode is iptables in 1.37 and a future release will change it to nftables
  2. Kubernetes v1.37: Garhwal - the release announcement of 26 August 2026. One of the 67 enhancements is a deprecation and it is this one. The deprecation section states the timeline, gives the jsonpath one-liner for finding out which mode a cluster is running, and says outright that ipvs mode continues to use iptables underneath because the kernel IPVS API alone cannot implement Kubernetes Services
  3. KEP-5495: Deprecate ipvs mode in kube-proxy - the enhancement proposal itself, and the only document that carries all five stages. Stage 2 in 1.37 adds the KubeProxyIPVS feature gate; stage 3 in 1.40 flips it off by default; stage 4 in 1.43 removes pkg/proxy/ipvs; the cleanup stage in 1.46 removes the gate. It also records why: SIG Network has no maintainers familiar with the ipvs backend, and the kernels too old for nftables mode will be out of LTS by the end of 2026
  4. KEP-3866: Add an nftables-based kube-proxy backend - the design document for the mode you are migrating to, including the section titled "The ipvs mode of kube-proxy will not save us", the reasoning behind switching modes being safe in this direction when it was not between the iptables-family modes, and the two nft commands that remove every rule kube-proxy owns
  5. KEP-265: IPVS load balancing mode in Kubernetes - the original 2017 proposal, worth reading now mainly to see which of its promises were kept and which were quietly not
  6. KEP-2448: Remove kube-proxy automatic clean-up logic - why kube-proxy stopped trying to tidy up after the other modes, and therefore why the cleanup step in this migration is something you do rather than something that happens
  7. kube-proxy command line reference: --proxy-mode, --cleanup, --nodeport-addresses, --conntrack-tcp-be-liberal, --ipvs-scheduler, --ipvs-strict-arp and the rest. Note that --cleanup is documented as cleaning up iptables and ipvs rules, which is the direction that matters here
  8. KubeProxyConfiguration API reference: the schema of the config.conf that lives in the kube-proxy ConfigMap, including the mode field, the ipvs and nftables sections, and which options are read in which mode
  9. Kubernetes feature gates: where KubeProxyIPVS and KubeProxyNFTablesLocalhostNodePorts are listed with their stage and default, and the reference for how a gate is passed to a component that is not the API server
  10. Kubernetes metrics reference: the exact names of the kube-proxy counters used in this article, including kubeproxy_sync_proxy_rules_nftables_sync_failures_total, kubeproxy_iptables_ct_state_invalid_dropped_packets_total and kubeproxy_iptables_localhost_nodeports_accepted_packets_total. Also, by omission, the fact that there is no ipvs-specific counter anywhere in the list
  11. Kubernetes deprecation policy: what a deprecation of a component flag or behaviour actually commits the project to, which is the frame for reading the KEP-5495 stages as dates rather than intentions
  12. Kubernetes releases: the supported branches and their end-of-life dates, which is how you turn "1.40" and "1.43" into calendar quarters for your own cluster
  13. Kubernetes version skew policy: how far kube-proxy is allowed to lag the API server and the kubelet, which bounds how long a partially migrated fleet can stay partially migrated
  14. Kubernetes v1.35: Timbernetes - the release where stage 1 of this deprecation landed and kube-proxy started logging a warning on startup in ipvs mode. If nobody in your organisation noticed, that is the point
  15. Kubernetes v1.36: Haru - the release in between, useful for placing the deprecation on the same timeline as the other node-level changes of 2026
  16. Kubernetes v1.37 sneak peek: the pre-announcement of the same deprecation, published four weeks before the release
  17. Kubernetes - Service: the API that all of this implements, and the reference for sessionAffinity, which is the feature people mistakenly believe the IPVS sh scheduler is providing
  18. Kubernetes - EndpointSlices: the objects kube-proxy actually watches, and the reason rule-sync cost scales with endpoint churn rather than with Service count alone
  19. Kubernetes - Service internal traffic policy: the supported way to keep traffic on the local node, which is the thing IPVS locality-based schedulers are sometimes reached for instead
  20. Kubernetes - Cluster networking: where kube-proxy sits relative to the CNI plugin, which decides whether any of this applies to your cluster at all
  21. Kubernetes - Debug Services: the official checklist for a Service that does not answer, and the first thing to run when a node comes back on a new proxy mode
  22. Kubernetes - Safely drain a node: the cordon, drain and uncordon sequence this migration slots into, one node at a time
  23. Kubernetes - Upgrading kubeadm clusters: for kubeadm-built clusters, the place the kube-proxy DaemonSet and its ConfigMap come from, and the reason a config change can be reverted by the next upgrade if it is not also made in the cluster configuration
  24. Kubernetes blog - Kubernetes's iptables chains are not API: the standing warning that anything of yours which hooks into kube-proxy's own chains is unsupported. It is the single best predictor of what will break when the chains are replaced by nftables tables
  25. Kubernetes blog - IPVS-based in-cluster load balancing deep dive: the 2018 introduction to the mode being retired, including the kube-ipvs0 dummy interface and the ipset usage that this article tells you to go and clean up
  26. Kubernetes blog - kube-proxy subtleties, debugging an intermittent connection reset: the original write-up of the conntrack invalid-state problem whose workaround nftables mode does not install by default
  27. kubernetes/kubernetes, pkg/proxy/ipvs: the directory KEP-5495 stage 4 deletes. Worth a look if you want to see for yourself how much iptables the ipvs mode is driving
  28. kubernetes/kubernetes, pkg/proxy/nftables: the implementation you are moving to, and the authority on which table and chain names to expect on a migrated node
  29. nftables wiki: the syntax reference for reading what kube-proxy now writes, in particular sets, maps and verdict maps, which are the features the iptables API cannot express and the reason the new backend is faster
  30. nft(8) manual page: list, delete, monitor and the ruleset commands used in the verification section
  31. The netfilter project's nftables page: the upstream statement of what nftables replaces and why development moved there
  32. Linux kernel documentation - nf_conntrack sysctls: nf_conntrack_tcp_be_liberal, which is what --conntrack-tcp-be-liberal sets, and the surrounding timeouts kube-proxy also manages
  33. kernel.org - active kernel releases: the longterm branches and their projected end-of-life dates, which is how to check the KEP's claim that every kernel too old for nftables mode leaves LTS by the end of 2026
  34. ipvsadm(8): the tool for reading and clearing the IPVS table that kube-proxy leaves behind, including -L -n for inspection and -C for the flush used in the cleanup step
  35. MetalLB installation: the source of the strict ARP requirement for kube-proxy in ipvs mode. It is an ipvs-only requirement because it works around an ipvs-only behaviour, which is why it stops applying after this migration
  36. Cilium - Kubernetes without kube-proxy: the other answer to this deprecation, which is to stop running kube-proxy at all and let an eBPF data plane implement Services
  37. Calico - enabling the eBPF data plane: the same answer from the other major CNI, including the requirement to disable kube-proxy rather than run both

Было полезно?