El modo ipvs ya tiene fecha de borrado.
Kubernetes 1.37 trae una sola deprecación y es el modo ipvs de kube-proxy. Apagado por defecto en 1.40, código eliminado en 1.43. Aquí está el cambio a nftables, el mínimo de kernel, los cuatro comportamientos que cambian y la trampa que alcanza también a los clústeres que nunca usaron ipvs.
- Kubernetes
- Redes
- Linux
- nftables
Kubernetes v1.37 se publicó el 26 de agosto de 2026 con 67 mejoras, y exactamente una de ellas es una deprecación: el modo ipvs de kube-proxy. Es un titular más pequeño que el gang scheduling o los certificados de Pod, y un asunto operativo bastante más grande, porque es lo único de la versión que termina con código borrado de un plano de datos que ahora mismo está en producción. El calendario ya está escrito: una feature gate en 1.37, apagada por defecto en 1.40 y pkg/proxy/ipvs eliminado en 1.43.

Hay una versión de esto que es editar una línea de un ConfigMap y otra que es un martes malo, y la diferencia está entera en qué compruebas primero. Lo que viene es el asunto completo: las cinco etapas del KEP y cuáles de ellas son fechas en vez de intenciones, cómo averiguar qué está ejecutando tu clúster de verdad (incluida la respuesta que es peor que ipvs), por qué ese modo nunca fue la huida de iptables que la gente recuerda, por qué los schedulers de IPVS no están haciendo el trabajo por el que se conservan, el mínimo de kernel y qué imágenes de nodo lo cumplen, los cuatro cambios de comportamiento documentados, un despliegue nodo a nodo, el estado que hay que limpiar a mano, un script de verificación que devuelve un código distinto de cero y una vuelta atrás que de verdad son dos comandos.
La 1.37 trae una sola deprecación, y es esta
Ninguno de los síntomas de abajo dice deprecación, y por eso esto suele detectarse tarde, normalmente por quien está haciendo la actualización a 1.40, tres versiones después de que apareciera el primer aviso. El modo no se degrada. Sigue funcionando perfectamente hasta la versión en la que ya no existe, y los avisos intermedios van a parar a un log que nadie filtra.[rel137]
| Lo que ves | Lo que significa en realidad | Dónde se trata |
|---|---|---|
kube-proxy escribe The ipvs proxier is now deprecated en cada arranque | Etapa 1 de KEP-5495, publicada en Kubernetes 1.35. No hay nada roto; el reloj arrancó hace dos versiones. | Las cinco etapas |
mode: está vacío en el ConfigMap de kube-proxy | El clúster no ha elegido plano de datos. Upstream va a cambiar el valor recomendado de iptables a nftables, y un clúster sin fijar lo sigue. | Qué modo |
| Tras actualizar a 1.40, kube-proxy termina con un error que lista los modos válidos | Etapa 3. La gate KubeProxyIPVS pasa a valer false por defecto y nadie la volvió a poner. | Las cinco etapas |
| Un NodePort dejó de responder en una interfaz secundaria justo después del cambio | El modo nftables usa por defecto --nodeport-addresses primary. Es la regresión más frecuente de esta migración, con diferencia. | Qué cambia |
| El tráfico hacia una ClusterIP se descarta, en un solo nodo | Direcciones caducas sobre un kube-ipvs0 sobrante y sin reglas de IPVS detrás. El nodo sigue respondiendo ARP por una dirección que ya no sirve. | Residuos |
La segunda fila de esa tabla merece leerse dos veces, porque no tiene nada que ver con IPVS y alcanza a clústeres que nunca lo usaron. El modo por defecto en Kubernetes 1.37 sigue siendo iptables, y la documentación dice sin rodeos que una versión futura cambiará ese valor por defecto a nftables. Si la configuración de tu kube-proxy no nombra un modo, no estás en iptables por decisión propia: estás en lo que upstream recomiende en cada momento, y tienes una sustitución del plano de datos agendada en el calendario de otra gente.[vips]
La recomendación del propio proyecto, y lo más barato de este artículo: para evitar que el backend del proxy de un clúster cambie de forma inesperada durante una actualización, deberías asegurarte de que todos los clústeres tienen una configuración de kube-proxy que indica explícitamente qué modo usar. Es un cambio de cinco minutos y merece la pena hacerlo hoy, sea cual sea el modo en el que acabes.
Las cinco etapas, y cuáles dos son fechas
KEP-5495 lo desglosa en cinco etapas, y la disciplina útil consiste en separar las que son anuncios de las que son fechas. Las etapas 1 y 2 ya han ocurrido y no han cambiado nada de cómo funciona un clúster. La etapa 3 cambia un valor por defecto, lo que significa que cambia lo que le pasa a un clúster en el que nadie hizo nada. La etapa 4 elimina el código, y es la única irreversible.[kep5495]
| Etapa | Kubernetes | Qué cambia | Qué te pide |
|---|---|---|---|
| 1 | 1.35 | kube-proxy avisa al arrancar en modo ipvs; la documentación queda marcada como obsoleta. Se retroportan correcciones de nftables a 1.33 y 1.34 para que los clústeres antiguos puedan migrar igualmente. | Enterarte. Nada más. |
| 2 | 1.37 | Se añade la feature gate KubeProxyIPVS: GA, por defecto true. El comportamiento no cambia. | Planificar. Es la última versión tranquila. |
| 3 | 1.40 | La gate pasa a valer false por defecto. Un kube-proxy arrancado en modo ipvs sin sobrescribirla termina con un error que lista los modos válidos. | Haber terminado, o poner la gate y comprar tres versiones menores. |
| 4 | 1.43 | pkg/proxy/ipvs desaparece del árbol. La feature gate ya no puede devolverlo; las menciones que quedaban salen de la documentación. | Nada. No existe el modo ipvs. |
| 5 | 1.46 | Se elimina la propia feature gate. | Solo relevante para herramientas que inspeccionan gates. |
Dos de esas filas merecen una nota. La etapa de limpieza de 1.46 existe en el KEP y en prácticamente ninguna cobertura de esta deprecación, lo cual solo importa si escribes herramientas que inspeccionan feature gates: hasta entonces la gate es real, está listada y es GA. Y la razón que dio SIG Network para hacer todo esto conviene conocerla, porque no es el rendimiento: el grupo no tiene mantenedores familiarizados con el código del backend de ipvs, y lleva tiempo diciéndole a quien reporta bugs de ipvs que se pase a nftables. Un backend que nadie arregla es un pasivo, digan lo que digan sus benchmarks de hace ocho años.[depol][k8srel]
En qué modo está este clúster de verdad
Empieza por lo que está configurado de verdad, no por lo que dice el runbook, porque en cualquier clúster de más de dos años esos son dos documentos distintos. Hay tres respuestas posibles y la tercera es la interesante: ipvs, iptables o nada en absoluto.[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-proxyDespués busca el aviso, que lleva en tus logs desde Kubernetes 1.35 y es la prueba más barata de que esto te aplica. La feature gate añadida en 1.37 conviene localizarla en la misma pasada, no porque haga falta activarla —viene encendida y no cambia nada en esta versión— sino porque es la palanca que te van a ofrecer en 1.40, y es mejor saber de antemano que solo alarga el plazo tres versiones menores en lugar de eliminarlo.[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'Qué es el modo ipvs, frente a cómo se le recuerda
Conviene ser preciso sobre qué se está retirando, porque la versión popular de la historia es falsa de una forma que cambia la decisión. El modo ipvs se añadió en Kubernetes 1.8 para escapar del coste de una lista de reglas proporcional al número de Services. Eso lo consiguió. Lo que nunca hizo fue sustituir a iptables: la API de IPVS del kernel por sí sola no puede expresar toda la API de Service de Kubernetes —las decisiones de masquerade, el filtrado de NodePort, los rangos de origen de LoadBalancer, el comportamiento de rechazo de un Service sin endpoints— así que el modo conduce iptables e ipset por debajo de la tabla de IPVS. El anuncio de la v1.37 dice exactamente esto. Cuenta las reglas en tu propio nodo si prefieres no fiarte de nadie.[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.La segunda mitad de la versión popular son los schedulers, y esa es la razón por la que la mayoría de los clústeres que siguen en ipvs siguen ahí. Alguien puso scheduler: "lc" hace años, y desde entonces se entiende que el clúster hace balanceo por menor número de conexiones. No lo hace, y el motivo es arquitectónico, no un bug. kube-proxy corre en cada nodo y cada instancia mantiene su propia tabla de IPVS, que solo cuenta las conexiones que abrió ese nodo. Es decir, «menos conexiones» se calcula por nodo cliente: con veinte nodos ocupados tienes veinte decisiones locales independientes, no una global, y el tráfico que llega de fuera del clúster no está en ninguna de esas cuentas. SIG Network señaló este malentendido de forma explícita al escribir la deprecación.[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"'| Scheduler de IPVS | Qué hace en un nodo | Qué significa eso para el clúster |
|---|---|---|
rr (el valor por defecto) | Round robin sobre la lista de endpoints de este nodo. | Funcionalmente lo que ya hacen los modos iptables y nftables al elegir un backend al azar. Aquí no se pierde nada. |
lc, wlc, sed, nq | Menos conexiones activas, según las cuenta este nodo. | No es «menos conexiones» a nivel de clúster. N nodos cliente ocupados toman N decisiones locales independientes, y el tráfico de fuera del clúster no está en ninguna. |
sh, dh | Hash sobre la dirección de origen o de destino. | Se confunde a menudo con afinidad de sesión. El equivalente soportado es .spec.sessionAffinity: ClientIP, que es por Service y sobrevive a la migración. |
lblc, lblcr | Menos conexiones con criterio de localidad. | La localidad en Kubernetes es .spec.internalTrafficPolicy: Local, que la API sí conoce y el scheduler no puede ver. |
mh (Maglev) | Hashing consistente. | kube-proxy siempre pone mh-port y nunca activa mh-fallback, así que en la práctica se comporta como hash de origen con puertos. |
Tampoco hay un scheduler por Service, ni lo hubo nunca. ipvs.scheduler es un único campo de la configuración de kube-proxy y aplica a todos los Services de todos los nodos: no forma parte de la API de Service y ninguna anotación lo cambia. Merece la pena detenerse un momento en eso, porque implica que las dos cosas que la gente busca de verdad en los schedulers, clientes pegajosos y localidad de nodo, tienen respuestas soportadas en la API que sobreviven a esta migración sin tocarse: sessionAffinity: ClientIP e internalTrafficPolicy: Local.[svc][stp]
| Modo | Estado en 1.37 | Kernel | Usa iptables por debajo | Dónde termina |
|---|---|---|---|---|
iptables | El valor por defecto. No está obsoleto. | cualquiera | Sí, por definición | Deja de ser el valor por defecto en algún momento; no hay eliminación anunciada. |
ipvs | Obsoleto desde 1.35; feature gate añadida en 1.37 | cualquiera | Sí: masquerade, filtrado de NodePort, rangos de origen de LoadBalancer, reglas de rechazo | Apagado por defecto en 1.40, código eliminado en 1.43. |
nftables | Estable desde 1.33; el sustituto recomendado para ipvs | 5.13 o posterior | No | Pasa a ser el valor por defecto en una versión futura. |
kernelspace | Solo nodos Windows | — | — | No le afecta nada de esto. |
¿Pueden estos nodos correr el modo nftables?
Ahora la única restricción dura. El modo nftables requiere Linux 5.13 o posterior, y no hay soporte parcial ni repliegue: por debajo de esa versión, kube-proxy sencillamente no arranca en ese modo. Esta es la comprobación que decide si esta migración es editar un ConfigMap o un proyecto de imágenes de nodo, así que hazla antes de escribir ningún plan.[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| Imagen de nodo | Kernel que trae por defecto | Modo nftables |
|---|---|---|
| RHEL 9, Rocky Linux 9, AlmaLinux 9 | 5.14 | Sí |
| RHEL 10 | 6.12 | Sí |
| RHEL 8, CentOS 7 | 4.18 y anteriores | No: toca reconstruir la imagen de nodo |
| Ubuntu 24.04 LTS | 6.8 | Sí |
| Ubuntu 22.04 LTS | 5.15 | Sí |
| Ubuntu 20.04 LTS | 5.4 GA, 5.15 con HWE | Solo con el kernel HWE |
| Debian 12 | 6.1 | Sí |
| Debian 11 | 5.10 | No |
| Amazon Linux 2023 | 6.1 | Sí |
Si algunos nodos no llegan al mínimo, la lectura honesta suele ser que la imagen de nodo está en fin de vida y no que al kernel le falte un parche, y el arreglo correcto es reconstruirla. Si esa reconstrucción no puede pasar antes de 1.40, el repliegue es el modo iptables y no la feature gate: el backend de iptables no está obsoleto, su rendimiento mejoró sustancialmente después de que se introdujera el modo ipvs, y upstream lo recomienda por encima de ipvs justo para este caso. Un dato para quien esté sopesando los plazos: la deprecación señala que todos los kernels demasiado antiguos para el modo nftables salen del soporte a largo plazo antes de que termine 2026, que es el argumento de que la objeción del kernel tiene su propia fecha de caducidad.[kernrel][kep5495]
Cuatro cosas se comportan distinto después
Cambian cuatro comportamientos, y tres de ellos están documentados bajo un encabezado —migrar del modo iptables a nftables— que los usuarios de ipvs no tenían ninguna razón para haber leído. Aquí aplican igual, porque son propiedades del destino y no del origen.[vips]
| Comportamiento | Modo iptables / ipvs | Modo nftables | Qué hacer al respecto |
|---|---|---|---|
| Direcciones en las que escucha un NodePort | Todas las direcciones locales, salvo restricción explícita | --nodeport-addresses primary por defecto | Audita qué direcciones se usan de verdad y fija la opción explícitamente si necesitas algo más que la primaria. |
NodePort en 127.0.0.1 | Funciona en modo iptables por defecto | No disponible; recuperado en 1.37 detrás de una gate alfa | Lee kubeproxy_iptables_localhost_nodeports_accepted_packets_total antes de decidir que no lo necesitas. |
| Cortafuegos local | kube-proxy añade reglas de aceptación por NodePort | No hace nada | Permite el rango de NodePort en tu propio cortafuegos de host. |
| Bug de reset de conntrack anterior al 6.1 | Apaño instalado | No se instala por defecto | Revisa kubeproxy_iptables_ct_state_invalid_dropped_packets_total; si no es cero, arrastra --conntrack-tcp-be-liberal. |
| Reglas tuyas sobre las cadenas de kube-proxy | KUBE-SERVICES y KUBE-SEP-* existen en iptables | El estado vive en table ip kube-proxy / table ip6 kube-proxy | Nunca fue API. Engancha tu propia tabla en la misma prioridad de netfilter en vez de escribir en la suya. |
- Los NodePort dejan de escuchar en todas las direcciones locales. En los modos iptables e ipvs, un Service de
type: NodePortes alcanzable en todas las IP locales salvo que lo hayas restringido. El modo nftables usa por defecto--nodeport-addresses primary, que significa la dirección IPv4 y/o IPv6 primaria del nodo según el objeto Node, y nada más. Cualquier cosa que llegue a un NodePort por una NIC secundaria, una dirección de gestión o una VIP flotante deja de funcionar. Fija la opción explícitamente si necesitas el alcance antiguo —0.0.0.0/0lo restaura—, pero audita primero, porque el valor por defecto es el que upstream cree que la gente quería en realidad. - Los NodePort en localhost son un caso aparte, y la 1.37 lo cambió. Conectarse a
127.0.0.1:<nodePort>funcionaba en modo iptables y no funcionaba en absoluto en modo nftables. Desde Kubernetes 1.37 puede funcionar, detrás de la gate alfaKubeProxyNFTablesLocalhostNodePortsy connodePortAddressespuesto aprimary,localhost. Antes de decidir si te importa, lee el contador: kube-proxy lleva todo este tiempo contando los paquetes aceptados en NodePort por loopback. - kube-proxy deja de abrirte el cortafuegos. El modo iptables añade reglas de aceptación para cada NodePort, sobre la teoría de que un cortafuegos local demasiado agresivo los bloquearía. Ese enfoque no puede funcionar contra un cortafuegos basado en nftables, así que el modo nftables no hace absolutamente nada aquí. Si tienes cortafuegos de host, ahora le toca a él permitir el rango de NodePort, que probablemente es como debió ser siempre y sigue siendo un cambio.
- El apaño de conntrack no viene instalado por defecto. Los kernels anteriores al 6.1 tienen un bug que puede resetear conexiones TCP de larga duración hacia IP de Service. El modo iptables instala un apaño; más tarde se vio que ese apaño causaba problemas propios, así que el modo nftables lo deja fuera. Si lo necesitas o no es medible en vez de opinable —el contador está en las métricas—, y si lo necesitas,
--conntrack-tcp-be-liberales la forma soportada de recuperar el comportamiento. - Cualquier cosa tuya que enganchara con las cadenas de kube-proxy está rota. Si un script de cortafuegos, un hook de CNI o una regla de monitorización se refería a
KUBE-SERVICESo aKUBE-SEP-*por nombre, esas cadenas ya no están: el estado de kube-proxy vive ahora entable ip kube-proxyytable ip6 kube-proxy. Esto nunca estuvo soportado —upstream tiene un artículo permanente titulado exactamente eso— pero conviene buscarlo antes del despliegue y no después, porque falla en silencio.
# 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_totalEl patrón en los cinco es el mismo: ninguno es un fallo de arranque. kube-proxy levanta, se reporta sano, sirve correctamente casi todo el tráfico, y una ruta concreta deja de funcionar. Por eso la comprobación previa de abajo vale más que el despliegue en sí, y por eso el primer nodo se queda solo durante una jornada laboral entera.[reset][ctsysctl]
El cambio, nodo a nodo
La edición son tres líneas. La disciplina está en borrar el bloque ipvs en lugar de dejarlo al lado del modo nuevo, porque la configuración muerta es justo la forma en que la siguiente persona concluye que el clúster sigue en ipvs. Una cosa que comprobar en clústeres construidos con kubeadm: este ConfigMap lo regenera kubeadm upgrade, así que un cambio hecho solo con kubectl puede revertirse en silencio en la siguiente actualización. Hazlo también en la configuración del clúster.[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 -Después despliégalo como desplegarías cualquier cosa que sea dueña del plano de datos en todos los nodos, es decir: un nodo, luego un pool, luego la flota. Este caso es inusualmente barato de probar en canario porque el estado es enteramente por nodo y se reconstruye desde el API server en cada arranque: no hay nada compartido que corromper, y un nodo que no puede correr el modo nuevo falla al arrancar con la versión del kernel en el mensaje.[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.Lo que ipvs deja detrás
La limpieza es donde esta migración se diferencia de cualquier cambio de modo anterior de kube-proxy, y para bien. kube-proxy solía intentar recoger lo que dejaban los otros modos y dejó de hacerlo —KEP-2448 eliminó esa lógica— porque los backends de iptables, ipvs y userspace escribían en algunas de las mismas cadenas, de modo que limpiar las reglas de un modo borraba también las del modo en marcha. El modo nftables no comparte nada: todo su estado está en sus dos tablas propias. Por eso el cambio en esta dirección está diseñado para eliminar las reglas antiguas al arrancar, y por eso limpiar a mano es seguro cuando hace falta.[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 --cleanupUn residuo merece más atención que los demás. kube-ipvs0 es una interfaz dummy que el modo ipvs usa para enlazar cada ClusterIP —y cada IP de LoadBalancer— como una /32 en el nodo, que es toda la razón por la que MetalLB en modo capa 2 documenta strictARP como requisito para clústeres con ipvs. El modo nftables no crea ninguna interfaz así, de modo que ese requisito deja de aplicar y el ajuste se convierte en configuración muerta. Pero si la interfaz sobrevive al cambio con las direcciones todavía puestas y sin reglas de IPVS detrás, el nodo sigue reclamando esas ClusterIP y sigue respondiendo ARP por ellas mientras descarta el tráfico. Eso es un agujero negro en un solo nodo que se presenta como un fallo intermitente de la aplicación, y es la mejor razón que hay para verificar la limpieza en vez de darla por hecha.[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.Verificar, en lugar de confiar
La verificación tiene una forma específica en esta migración, dictada por los modos de fallo: casi todo lo que sale mal deja a kube-proxy en marcha y sano. Así que comprobar que el pod está en Running no demuestra absolutamente nada. El script de abajo comprueba el modo que kube-proxy eligió de verdad y no el que pone el ConfigMap, que no sobrevivió ningún estado de ipvs, que la interfaz dummy ya no está, que la sincronización de reglas funciona, y después hace lo único que ninguna inspección puede hacer, que es abrir una conexión real a un Service real desde un pod de ese nodo.[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 $rcDespués, tres contadores merecen una alerta durante quince días. Fíjate en lo que falta de la lista: la referencia de métricas de Kubernetes tiene contadores específicos de iptables y contadores específicos de nftables, y absolutamente nada para ipvs. La monitorización que tengas hoy del modo ipvs te la construiste tú a base de ipvsadm, que es una ilustración pequeña y concreta del argumento de mantenimiento que hay detrás de toda la deprecación.[metrics]
| Métrica | Qué te dice | Merece alerta |
|---|---|---|
kubeproxy_sync_proxy_rules_nftables_sync_failures_total | kube-proxy no puede escribir el conjunto de reglas, así que el plano de datos se está separando del API server. | Cualquier incremento, de forma permanente. |
kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total | No consigue borrar reglas que considera caducas: normalmente restos del modo antiguo, o algo más escribiendo en las mismas tablas. | Cualquier incremento durante los quince días posteriores al cambio. |
kubeproxy_sync_proxy_rules_last_timestamp_seconds | Cuándo coincidieron por última vez las reglas del nodo con el API server. Esta es la que caza el fallo silencioso. | Más de cinco minutos en el pasado. |
kubeproxy_iptables_localhost_nodeports_accepted_packets_total | Algo se está conectando a un NodePort por loopback. | Distinto de cero antes del cambio, como bloqueante. |
kubeproxy_sync_proxy_rules_duration_seconds | Cuánto tarda una sincronización completa. El argumento de rendimiento de la migración, si tienes suficientes Services para que lo haya. | Toma una línea base antes y después; sin alerta. |
# 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.Volver atrás, y qué te compra eso
Volver atrás aquí sale genuinamente barato, cosa que no es cierta de las otras migraciones que aterrizan este año en estos mismos nodos. No hay datos que convertir, ni formato en disco que degradar, ni estado que sobreviva a un reinicio: el cambio entero es una clave de ConfigMap más estado de kernel por nodo que kube-proxy reconstruye desde el API server cada vez que arranca. Vuelve a poner la clave antigua, borra el pod de ese nodo, y el nodo está donde estaba en menos de un minuto.[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.Dos matices, y los dos son de alcance y no de mecanismo. kube-proxy en modo ipvs está diseñado para borrar las reglas de nftables que se encuentra al arrancar, pero la vuelta atrás importa precisamente cuando el backend nuevo se está portando mal, así que conviene conocer la versión manual: cada regla de la que kube-proxy es dueño está dentro de sus propias tablas, y dos nft delete table se lo llevan todo. Y ten claro qué te compra la vuelta atrás. Termina bien una ventana de mantenimiento que ha ido mal. No mueve la 1.40, y no mueve la 1.43.[skew]
La otra respuesta: no ejecutar kube-proxy
Hay una segunda respuesta a esta deprecación que merece nombrarse, porque para algunos clústeres es la mejor: dejar de ejecutar kube-proxy. Cilium y Calico implementan los Services de Kubernetes directamente en eBPF, con una búsqueda en tabla hash en lugar de una lista de reglas, y los dos documentan cómo funcionar con kube-proxy eliminado en lugar de en paralelo. Si ya estás usando uno de ellos, la deprecación de ipvs puede ser la invitación a hacer un cambio que ibas a hacer de todas formas.[cilium]
Es un cambio mucho más grande que el que describe este artículo, y hay que pesarlo como tal. Sustituir kube-proxy mete la implementación de Service dentro del CNI, lo que acopla tu plano de datos a la cadencia de versiones de ese proyecto, a sus requisitos de kernel —que son más altos que 5.13, no más bajos— y a sus herramientas de depuración, y no es algo que se haga dentro de una ventana de deprecación y con prisa. La secuencia honesta para la mayoría de los clústeres es: pasa a nftables ahora porque es editar un ConfigMap, y evalúa la cuestión de eBPF por sus propios méritos y en su propio calendario.[calico][netpol]
El orden en el que hacer esto
Comprimida, la decisión es mucho más pequeña que el artículo. Si tus kernels son 5.13 o posteriores, esto es un cambio de configuración con un despliegue cuidadoso. Si no lo son, es un proyecto de imágenes de nodo, y la respuesta provisional es el modo iptables y no la feature gate. Y si la configuración de tu kube-proxy no nombra ningún modo, eso es lo que hay que arreglar esta semana al margen de todo lo demás.[kep5495]
| Dónde estás | Qué hacer |
|---|---|
| Modo ipvs, todos los nodos en 5.13 o posterior | Pásate a nftables. Es todo este artículo, y es editar un ConfigMap más un despliegue cuidadoso. |
| Modo ipvs, algunos nodos por debajo de 5.13 | Reconstruye esas imágenes de nodo. Si eso no puede pasar antes de 1.40, muévelos al modo iptables en lugar de poner la feature gate: iptables no está obsoleto y upstream lo recomienda por encima de ipvs para kernels antiguos. |
| Modo iptables, sin plan | Fija mode: iptables explícitamente hoy para que una actualización no pueda cambiarlo, y pásate a nftables cuando tu mínimo de kernel lo permita. |
| Ningún modo fijado | Fíjalo esta semana, el que quieras. Este es el punto con una fecha límite que no eligió nadie de tu organización. |
| El CNI ya sustituye a kube-proxy | Nada que hacer. Confirma que kube-proxy de verdad no está corriendo en lugar de suponerlo, porque existen sustituciones parciales. |
- Fija el modo, sea el que sea. Si
mode:está vacío en el ConfigMap de kube-proxy, ponlo explícitamente hoy. Es el único punto de la lista que aplica a clústeres sin nada de ipvs, y el único con una fecha límite que no elegiste tú. - Comprueba los kernels antes de escribir un plan. Un comando sobre toda la flota decide si esto es una tarde o un trimestre. Los nodos por debajo de 5.13 necesitan una imagen nueva, no un parche.
- Mide los dos contadores que deciden los filos. Los NodePort en localhost y los descartes por estado inválido de conntrack se responden desde las propias métricas de kube-proxy, antes de tocar nada. Audita en la misma pasada en qué direcciones se está escuchando cada NodePort.
- Un nodo, una jornada laboral entera, luego un pool. Las regresiones de aquí no son fallos de arranque; son un único cliente en una interfaz secundaria, y eso tarda un turno en aflorar. Verifica a mano la limpieza de
kube-ipvs0y de los ipsets en ese primer nodo. - Borra el bloque ipvs y el ajuste strictARP. Ninguno de los dos hace nada en modo nftables, y dejarlos es la forma en que la siguiente persona concluye que la migración nunca ocurrió. Después haz el mismo cambio allí donde se genere el ConfigMap, o el siguiente
kubeadm upgradelo deshará.
Este es uno de varios cambios que aterrizan en los mismos nodos el mismo año, y se leen mejor juntos: el paso de Ingress NGINX a la Gateway API, que es la otra mitad del trabajo de red y comparte la misma disciplina de despliegue; migrar containerd 1.7 a 2.x, el runtime de debajo con su propio calendario de retirada; y la migración de cgroup v1 a cgroup v2, que es el cambio a nivel de nodo que Kubernetes ya ha hecho obligatorio. Si estás sopesando cuánto de todo esto necesitas de verdad, cuándo no usar Kubernetes es la otra cara del argumento.
Preguntas frecuentes
¿Se elimina el modo ipvs de kube-proxy en Kubernetes 1.37?
No. Kubernetes 1.37 añade la feature gate KubeProxyIPVS, que vale true por defecto, así que el modo ipvs se comporta exactamente igual que antes. Las fechas que importan son 1.40, cuando la gate pasa a false por defecto y kube-proxy en modo ipvs termina con un error salvo que la sobrescribas, y 1.43, cuando se borra pkg/proxy/ipvs y la gate ya no puede ayudarte. La propia gate se elimina en 1.46.
¿Cómo sé qué modo de proxy está usando mi clúster?
En un clúster construido con kubeadm, kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:'. Si eso vuelve vacío, el clúster no ha elegido modo y está corriendo lo que kube-proxy recomiende en cada momento, que es iptables en 1.37 y pasará a ser nftables en una versión futura. En un clúster que no use ese ConfigMap, lee los argumentos del DaemonSet, o ejecuta ipvsadm -L -n en un nodo.
¿Tengo que migrar a nftables o puedo quedarme en modo iptables?
El modo iptables es un destino legítimo. No está obsoleto, no tiene mínimo de kernel y su rendimiento mejoró sustancialmente en los años posteriores a la introducción del modo ipvs; upstream lo recomienda explícitamente por encima de ipvs para sistemas demasiado antiguos para el modo nftables. nftables es el objetivo mejor allí donde el kernel lo permita, y es hacia donde va el valor por defecto, pero «de ipvs a iptables» es una respuesta real para imágenes de nodo antiguas y no una excusa.
¿Qué versión de kernel necesita el modo nftables de kube-proxy?
Linux 5.13 o posterior, y solo en nodos Linux. No hay soporte parcial: kube-proxy no arranca en modo nftables por debajo de esa versión. En la práctica eso descarta RHEL 8, CentOS 7, Debian 11 y Ubuntu 20.04 con el kernel GA, y deja dentro RHEL 9 y 10, Debian 12, Ubuntu 22.04 y 24.04 y Amazon Linux 2023. Compruébalo con kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion en lugar de por nombre de distribución.
¿Voy a perder el scheduler de IPVS que tenía configurado?
Vas a perder el ajuste, y casi con total seguridad no el comportamiento que creías que te daba. ipvs.scheduler es un único campo para todo el clúster, no por Service, y la tabla de IPVS de cada nodo solo cuenta las conexiones que abrió ese nodo, así que lc es «menos conexiones» por nodo cliente y no a lo largo del clúster. Si lo que querías era clientes pegajosos, eso es .spec.sessionAffinity: ClientIP en el Service. Si era localidad de nodo, eso es .spec.internalTrafficPolicy: Local. La migración no afecta a ninguno de los dos.
¿Sigo necesitando strictARP después de pasar al modo nftables?
No. strictARP existe porque el modo ipvs enlaza cada ClusterIP y cada IP de LoadBalancer sobre la interfaz dummy kube-ipvs0, lo que hace que el nodo responda ARP por direcciones que MetalLB está intentando controlar. El modo nftables nunca crea esa interfaz, así que el problema de fondo desaparece y el ajuste queda como configuración muerta: bórralo junto con el resto del bloque ipvs. Eso sí, verifica que kube-ipvs0 de verdad no está en cada nodo migrado, porque una interfaz sobrante con direcciones caducas se traga el tráfico.
¿Es seguro cambiar de modo en un clúster en marcha?
Cambiar entre un modo de la familia iptables y nftables está diseñado para serlo: el modo nftables guarda todo su estado en sus dos tablas propias y borra al arrancar las reglas de iptables e ipvs que encuentra, y lo mismo vale al volver atrás. Eso es específicamente distinto de cambiar entre iptables e ipvs, que compartían cadenas y por lo que se eliminó la limpieza automática de kube-proxy en KEP-2448. Aun así es un cambio del plano de datos en cada nodo que toca, así que haz cordon, drain, reinicia un solo pod de kube-proxy, verifica y deja ese nodo en paz una jornada laboral.
¿Qué es lo que más se rompe en esta migración?
La alcanzabilidad de los NodePort. El modo nftables usa por defecto --nodeport-addresses primary, así que un NodePort al que se llegaba por una NIC secundaria, una dirección de gestión, una VIP flotante o por 127.0.0.1 deja de responder, mientras kube-proxy sigue sano y todo lo demás sigue funcionando. Los dos casos son medibles de antemano desde las propias métricas de kube-proxy y desde el ajuste actual de nodePortAddresses.
¿No sería mejor sustituir kube-proxy por Cilium o Calico en eBPF?
Es una opción real y para algunos clústeres la mejor, pero es un cambio mucho mayor que un cambio de modo: mete la implementación de Service dentro del CNI y acopla tu plano de datos a las versiones, los requisitos de kernel y las herramientas de depuración de ese proyecto. Decidirlo bajo la presión de una deprecación es hacerlo al revés. Pasar a nftables es editar un ConfigMap y quita la fecha límite de encima; evalúa lo de eBPF después, en su propio calendario.
¿Cómo vuelvo atrás si el modo nftables se porta mal?
Restaura la clave anterior del ConfigMap y borra el pod de kube-proxy de ese nodo; el modo antiguo reconstruye su estado desde el API server al arrancar, así que el nodo vuelve en menos de un minuto. Si la limpieza propia de kube-proxy no se ejecuta —que es justo el caso en el que la vuelta atrás importa— todas sus reglas están dentro de sus propias tablas, así que nft delete table ip kube-proxy y nft delete table ip6 kube-proxy se lo llevan todo. La vuelta atrás termina bien una ventana de mantenimiento que ha ido mal; no mueve la 1.40 ni la 1.43.
Una nota a nivel de versión, porque el balance de la 1.37 no es el que cuenta casi todo el mundo: el cambio que de verdad puede dejar pods en ContainerCreating es SELinuxMount al llegar a GA, mientras que el fallo de cgroup v1 aterrizó en la 1.35, la restricción de los pods estáticos en la 1.34 y el precipicio de containerd sigue por delante, en la 1.38. qué se rompe de verdad al actualizar a Kubernetes 1.37 separa las tres columnas y da la auditoría que hay que hacer antes de actualizar, no después.
Fuentes
Solo fuentes primarias, revisadas el 27 de agosto de 2026. Donde la documentación de Kubernetes y un artículo secundario no coincidan en un número de versión de esta deprecación, hay que hacer caso a la documentación y al KEP: el escalonado ya se ha revisado una vez.
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Kubernetes v1.37 sneak peek: the pre-announcement of the same deprecation, published four weeks before the release
- 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
- Kubernetes - EndpointSlices: the objects kube-proxy actually watches, and the reason rule-sync cost scales with endpoint churn rather than with Service count alone
- 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
- Kubernetes - Cluster networking: where kube-proxy sits relative to the CNI plugin, which decides whether any of this applies to your cluster at all
- 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
- Kubernetes - Safely drain a node: the cordon, drain and uncordon sequence this migration slots into, one node at a time
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- nft(8) manual page: list, delete, monitor and the ruleset commands used in the verification section
- The netfilter project's nftables page: the upstream statement of what nftables replaces and why development moved there
- 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
- 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
- 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
- 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
- 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
- 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
¿Te ha resultado útil?