Aller au contenu
kube-proxy et ses alternatives

kube-proxy et ses alternatives

300 Concevoir ⏱ 1 h 20 kubernetesiptablesnftablesciliumebpf

À la fin, vous saurez

  • Lire les chaînes KUBE-SERVICES, KUBE-SVC et KUBE-SEP et expliquer la répartition par probabilités
  • Comparer les modes iptables, nftables et ipvs, et situer leur statut dans Kubernetes 1.36
  • Créer un cluster kind sans kube-proxy et le remplacer par Cilium
  • Diagnostiquer une table conntrack saturée et les entrées UDP périmées
  • Régler sessionAffinity, trafficDistribution et internalTrafficPolicy selon le besoin
  • Afficher et interpréter les règles de Service dans un nœud

Prérequis

Testé avec cilium 1.20.2 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

Un Service est une adresse qui n'appartient à personne. Pour qu'elle serve à quelque chose, chaque nœud doit transformer les paquets qui la visent en paquets vers un pod réel, et cela doit rester vrai pendant que les pods apparaissent, disparaissent, deviennent prêts ou non. Cette tâche est celle de kube-proxy (glossaire), un agent présent sur chaque nœud. Il n'est sur le chemin d'aucun paquet : il programme le noyau pour que celui-ci fasse le travail.

Cette conception a un défaut qu'on découvre à l'échelle. Les règles de traduction sont réécrites à chaque modification d'un Service ou d'un EndpointSlice, et sur un cluster qui compte des milliers de Services et des dizaines de milliers de pods, la réécriture elle-même, et la consultation des règles pour chaque paquet, deviennent coûteuses. C'est ce qui a donné naissance aux modes alternatifs, et aux plugins qui remplacent kube-proxy.

Il y a aussi un intérêt pratique immédiat : la plupart des pannes de Service se diagnostiquent dans les règles du nœud. Savoir les lire transforme « le Service ne répond pas » en « la chaîne du Service n'a aucun point d'accès » ou « la règle existe mais conntrack a gardé une vieille entrée ». Cette leçon ouvre les règles, compare les modes, remplace kube-proxy sur kind, et traite les pièges de conntrack.

Les concepts

Ce que kube-proxy surveille

kube-proxy observe l'API de Kubernetes : les Services et les EndpointSlices (la liste des pods prêts pour chaque Service, voir la leçon Services et DNS), plus les nœuds, pour certains cas. À chaque changement, il met à jour les règles du noyau du nœud. Il en résulte que chaque nœud a sa propre copie des règles, mises à jour à son rythme : après la disparition d'un pod, il existe une courte fenêtre où des nœuds différents n'ont pas la même vue. C'est cette asynchronie qu'on compense par l'arrêt gracieux des pods.

Le mode se choisit par l'option mode de sa configuration (KubeProxyConfiguration). Sous Linux, trois modes : iptables, nftables, ipvs. Dans la documentation de Kubernetes, iptables est le mode par défaut, et la page recommande de fixer le mode explicitement pour ne pas changer de mode sans le savoir quand le défaut évoluera.

Le mode iptables

iptables est l'interface historique de Netfilter, le cadre de filtrage du noyau Linux. Les règles sont rangées en tables (filter, nat...) et en chaînes ; chaque chaîne est une liste de règles qu'un paquet parcourt dans l'ordre jusqu'à ce qu'une règle décide de son sort. kube-proxy crée dans la table nat une hiérarchie de chaînes dont la documentation décrit les rôles :

  • KUBE-SERVICES : le point d'entrée. Elle est appelée depuis PREROUTING et OUTPUT. Elle contient une règle par Service (adresse virtuelle, protocole, port) qui saute vers la chaîne du Service ;
  • KUBE-SVC-<hash> : une chaîne par Service (et par port). Elle répartit entre les points d'accès ;
  • KUBE-SEP-<hash> (Service EndPoint) : une chaîne par point d'accès. Elle contient la règle de DNAT vers l'adresse et le port du pod ;
  • KUBE-MARK-MASQ et KUBE-POSTROUTING : marquent les paquets qui doivent être masqués, puis appliquent la traduction de source en sortie (pour le trafic externe en Cluster et pour le retour en épingle à cheveux, leçon 3) ;
  • KUBE-NODEPORTS et KUBE-EXTERNAL-SERVICES : les entrées pour les ports de nœud et les adresses de répartiteur.

Le <hash> est une empreinte du nom du Service ou du point d'accès, de longueur fixe : les noms des chaînes sont donc illisibles, mais un commentaire sur chaque règle (module comment) indique le Service (signalements/signalements:http). On s'y repère par le commentaire, pas par le nom de chaîne.

La répartition par probabilités. iptables ne sait pas « choisir au hasard parmi N chaînes ». kube-proxy le simule avec le module statistic : une règle par point d'accès, dans l'ordre, chacune avec une probabilité d'être prise, calculée pour que le résultat soit uniforme. Pour trois points d'accès :

RègleProbabilité écriteCe qui se passe si elle ne prend pas
11/3passe à la règle 2
21/2passe à la règle 3
3(aucune, toujours prise)

La probabilité cumulée est bien un tiers pour chacun : 1/3 pour le premier ; 2/3 × 1/2 = 1/3 pour le deuxième ; 2/3 × 1/2 × 1 = 1/3 pour le troisième. Avec N points d'accès, la i-ème règle porte la probabilité 1/(N-i+1). La documentation donne le même exemple. Dans la sortie d'iptables-save, une règle de ce type ressemble à -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-... : la probabilité s'écrit avec une précision limitée.

Le chemin d'un premier paquet est donc : KUBE-SERVICES (une règle qui reconnaît l'adresse et le port) → KUBE-SVC-xxx (tirage au sort) → KUBE-SEP-yyy (DNAT vers le pod). Ensuite, conntrack s'occupe du reste de la connexion.

Le coût. Dans KUBE-SERVICES, les règles sont parcourues en séquence : pour un paquet qui vise le dernier Service de la liste, le noyau compare l'adresse à toutes les règles précédentes. Le temps de traitement croît donc avec le nombre de Services, et la mise à jour des règles (kube-proxy réécrit une grande partie de la table à chaque synchronisation) devient lente à grande échelle. Les paramètres iptables.syncPeriod (30 s par défaut) et iptables.minSyncPeriod (0 s par défaut, valeur à augmenter sur un cluster très dynamique) bornent la fréquence des réécritures.

Le mode nftables

nftables est le successeur d'iptables dans le noyau Linux, avec un langage de règles unifié, et des structures de données (ensembles, tables de correspondance ou maps) qui permettent de retrouver une règle par une recherche indexée plutôt que par un parcours séquentiel. Le mode nftables de kube-proxy est stable depuis Kubernetes 1.33, et la documentation le présente comme le futur mode par défaut. Il exige un noyau Linux 5.13 ou plus récent.

Il dépend de ces tables de correspondance : une recherche par adresse de Service renvoie directement la chaîne à exécuter, dans un temps qui ne dépend pas (ou presque) du nombre de Services. Les mises à jour sont incrémentales : on ajoute ou retire les éléments modifiés, au lieu de réécrire la table. Les règles vivent dans une table dédiée de la famille ip (et ip6) nommée kube-proxy, séparée des règles du reste du système. Avantage supplémentaire : kube-proxy n'interfère plus avec les règles de pare-feu que vous ou le système y ajoutez.

Deux différences de comportement, relevées par la documentation, à connaître avant de basculer :

  • Les NodePorts ne sont par défaut pas joignables par localhost en nftables (alors qu'ils l'étaient en iptables, au prix du réglage route_localnet) ;
  • kube-proxy ne positionne plus le réglage noyau net.ipv4.conf.all.route_localnet.

Si une application sur le nœud utilise 127.0.0.1:<NodePort>, elle cessera de fonctionner. Il faut tester le changement de mode sur un cluster d'essai.

Le mode ipvs

IPVS (IP Virtual Server) est un répartiteur de charge de couche 4 intégré au noyau, avec de vraies tables de hachage et plusieurs algorithmes de répartition (tourniquet, moins de connexions...). kube-proxy en mode ipvs crée un serveur virtuel par Service, et s'appuie encore un peu sur iptables pour le masquerade. Il offrait de meilleures performances que iptables sur de très gros clusters et plus de choix d'algorithmes.

Il est déprécié. La documentation de Kubernetes annonce, comme la leçon sur les Services : dépréciation depuis la 1.35, désactivation par défaut à partir de la 1.40, retrait prévu en 1.43. Pour de nouveaux clusters, il ne faut pas le choisir : le mode nftables apporte le même gain d'échelle avec un noyau récent, et le remplacement complet (ci-dessous) offre l'autre option.

Remplacer kube-proxy

Plutôt qu'un kube-proxy plus rapide, certains plugins réseau prennent en charge les Services eux-mêmes, et kube-proxy n'est alors pas déployé. C'est le cas de Cilium avec kubeProxyReplacement. Dans ce mode, l'agent Cilium implémente le ClusterIP, le NodePort, le LoadBalancer, les externalIPs et le HostPort en eBPF, avec des tables de hachage en mémoire du noyau : pas de règle iptables par Service, pas de réécriture de table, une recherche de complexité constante. La traduction du trafic interne se fait, on l'a vu, au connect() de la socket (leçon 3).

La documentation de Cilium indique pour l'installer :

$ helm install cilium cilium/cilium --version 1.20.2 \
    --namespace kube-system \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost=${API_SERVER_IP} \
    --set k8sServicePort=${API_SERVER_PORT}

k8sServiceHost et k8sServicePort sont l'adresse réelle du serveur d'API : sans kube-proxy, l'adresse virtuelle du Service kubernetes ne fonctionne pas encore quand l'agent démarre, car c'est lui-même qui doit la traduire. Il doit donc joindre le serveur d'API par son adresse directe. La documentation précise aussi que cette fonction dépend du socket-LB et d'un noyau suffisamment récent.

Les avantages : latence et consommation processeur plus faibles à grande échelle, préservation possible de l'adresse source (mode DSR), un Maglev pour une répartition cohérente. Les inconvénients : on dépend d'un plugin unique pour une fonction critique, le diagnostic passe par des outils différents (cilium-dbg service list, pas iptables-save), et il faut un noyau récent. Les offres managées qui choisissent Cilium (dont, par défaut, Scaleway Kapsule) en font souvent usage ; vérifiez sur votre cluster, et ne supposez pas que kube-proxy y existe.

conntrack et ses pièges

Dans les modes iptables, nftables et ipvs, la décision de traduction est prise au premier paquet et mémorisée dans la table de suivi de connexion du noyau, conntrack. Chaque entrée occupe de la mémoire noyau et a un délai d'expiration. Trois pièges.

La table pleine. La table a une taille maximale (nf_conntrack_max). Quand elle est pleine, le noyau jette les paquets des nouvelles connexions : le journal du noyau (dmesg) affiche nf_conntrack: table full, dropping packet. Les connexions déjà établies continuent ; les nouvelles échouent par intermittence, ce qui est un symptôme frustrant. kube-proxy règle la taille d'après sa configuration : conntrack.maxPerCore (32 768 entrées par cœur par défaut) et conntrack.min (131 072 entrées minimum), le maximum étant le plus grand des deux produits. Les délais par défaut : 24 h pour une connexion TCP établie (tcpEstablishedTimeout), 10 s en CLOSE_WAIT, 300 s pour l'UDP (udpTimeout). Un nœud qui traite beaucoup de connexions courtes (un DNS très sollicité, des appels sortants) remplit sa table plus vite que la durée de vie des entrées ne la vide.

Les entrées UDP périmées. Un client UDP envoie des paquets vers un Service ; conntrack retient la traduction vers le pod A. Le pod A disparaît : les nouveaux paquets du même quintuplet (même adresse et port source) trouvent l'entrée et continuent d'aller vers le pod A, qui n'existe plus : le trafic est perdu sans erreur, jusqu'à l'expiration de l'entrée. Pour ce cas, kube-proxy purge les entrées de conntrack des points d'accès UDP supprimés ; le piège réapparaît quand cette purge est en retard ou que le plugin ne la fait pas. C'est une cause connue de coupures DNS après le renouvellement de pods CoreDNS.

La condition de course sur le DNAT en UDP. Deux paquets UDP émis presque simultanément par le même socket (typiquement les requêtes DNS A et AAAA) peuvent créer deux entrées de conntrack concurrentes pour le même quintuplet avant que la première soit inscrite : l'une des insertions échoue, et le paquet est abandonné (compteur insert_failed de conntrack -S). Le client attend alors l'expiration d'un délai (5 secondes par défaut dans le résolveur). C'est le fameux « DNS à 5 secondes » traité à la leçon 5.

Affinité de session et préférence de proximité

Trois champs du Service modifient le choix du point d'accès, quel que soit le mode.

  • sessionAffinity: ClientIP : les connexions d'une même adresse source vont au même pod, pendant sessionAffinityConfig.clientIP.timeoutSeconds (10 800 s par défaut, trois heures). Elle est implémentée par kube-proxy avec un ensemble d'adresses récentes (module recent en iptables). Derrière un répartiteur ou en externalTrafficPolicy: Cluster, beaucoup de clients partagent une adresse source : l'affinité concentre alors la charge sur un pod.
  • trafficDistribution : une préférence (PreferSameZone, PreferSameNode) pour les points d'accès proches du client ; le champ est stable (depuis la 1.33, et PreferSameNode depuis la 1.35). Si aucun point d'accès proche n'est disponible, le trafic va ailleurs : c'est une préférence, pas une garantie.
  • internalTrafficPolicy: Local : le trafic interne n'est dirigé que vers les pods du même nœud que le client, sinon il est abandonné. C'est une contrainte, pas une préférence : pour un DaemonSet de collecte de journaux, c'est ce qu'on veut ; pour un Service ordinaire, c'est dangereux.

En pratique

Sur le cluster kind formation. Les commandes sont décrites avec ce que vous devez observer ; rien n'est reproduit comme capture.

Quel mode, quelle configuration ?

Dans un cluster kind classique (kubeadm), kube-proxy est un DaemonSet configuré par une ConfigMap :

$ kubectl -n kube-system get daemonset kube-proxy
$ kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}'

La première commande confirme que kube-proxy tourne ; la seconde affiche le KubeProxyConfiguration, dont le champ mode (vide ou iptables : le défaut), les paramètres conntrack et iptables. Sur le nœud, kube-proxy expose aussi son mode par un point d'accès local :

$ docker exec formation-worker curl -s localhost:10249/proxyMode

La réponse est le nom du mode actif (iptables, nftables...). Sur un cluster sans kube-proxy (Cilium en remplacement), le DaemonSet n'existe pas, et c'est un signe.

Lire les règles iptables

$ docker exec formation-worker iptables-save -t nat | grep signalements

iptables-save -t nat écrit toute la table nat en texte ; grep signalements filtre sur le commentaire de chaque règle. Pour un Service à trois points d'accès, vous voyez :

  • une règle dans KUBE-SERVICES qui contient l'adresse virtuelle du Service, le port, et un saut vers KUBE-SVC-... ;
  • dans KUBE-SVC-..., trois règles avec le module statistic : la première avec une probabilité d'environ un tiers, la deuxième d'environ un demi, la troisième sans probabilité ;
  • dans chacune des trois chaînes KUBE-SEP-..., une règle DNAT vers adresse-du-pod:8000 (et une règle de marquage si le pod est joint depuis lui-même).

Pour suivre un seul Service, vous pouvez partir de son adresse :

$ kubectl -n signalements get service signalements -o jsonpath='{.spec.clusterIP}'
$ docker exec formation-worker iptables -t nat -L KUBE-SERVICES -n | grep <clusterIP>

-L liste, -n évite les résolutions DNS. Les options à connaître : iptables -t nat -S <chaîne> affiche les règles d'une chaîne au format source, iptables -t nat -L <chaîne> -n -v ajoute les compteurs de paquets et d'octets, très utiles : un compteur à zéro sur la règle d'un Service pendant que vous l'appelez signifie que le paquet n'arrive pas jusque-là.

Lire les règles nftables

Si le mode est nftables, les règles sont dans la table kube-proxy :

$ docker exec formation-worker nft list table ip kube-proxy
$ docker exec formation-worker nft list ruleset | grep -c signalements

La première commande affiche les chaînes et les tables de correspondance : vous y voyez une table de correspondance des adresses de Service vers les chaînes de service, et pour chaque Service, une chaîne qui choisit un point d'accès (par un nombre aléatoire, dans un ensemble de numéros mis en correspondance avec des chaînes endpoint-...). La structure diffère nettement d'iptables : la recherche se fait par une table indexée, pas par un parcours. nft --version sur le nœud donne la version de l'outil.

Passer kube-proxy en nftables sur kind

La configuration de kind permet de choisir le mode à la création (kubeProxyMode) :

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  kubeProxyMode: "nftables"
nodes:
  - role: control-plane
  - role: worker
  - role: worker

Le noyau du poste doit gérer nftables (5.13 ou plus récent). Après recréation, curl localhost:10249/proxyMode dans un nœud répond nftables, et iptables-save -t nat ne contient plus de chaînes KUBE-SVC (les règles sont dans la table kube-proxy de nftables).

Un cluster sans kube-proxy, avec Cilium

Pour tester le remplacement, on retire kube-proxy et le plugin par défaut :

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true
  kubeProxyMode: "none"
nodes:
  - role: control-plane
  - role: worker
  - role: worker

kubeProxyMode: "none" demande à kind de ne pas installer kube-proxy. Après création, installez Cilium avec le remplacement, en donnant l'adresse directe du serveur d'API, qui est ici le conteneur du plan de contrôle sur le port 6443 :

$ helm install cilium cilium/cilium --version 1.20.2 \
    --namespace kube-system \
    --set image.pullPolicy=IfNotPresent \
    --set ipam.mode=kubernetes \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost=formation-control-plane \
    --set k8sServicePort=6443
$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep KubeProxyReplacement

La dernière commande est celle de la documentation : elle affiche l'état du remplacement (la version verbeuse, cilium-dbg status --verbose, détaille les fonctions actives : ClusterIP, NodePort, LoadBalancer...). Après le déploiement de Signalements, kubectl -n kube-system exec ds/cilium -- cilium-dbg service list liste les Services et leurs points d'accès tels que l'agent les connaît ; c'est l'équivalent de la lecture des chaînes iptables. Dans ce cluster, iptables-save -t nat | grep KUBE-SVC ne trouve rien.

Observer conntrack

$ docker exec formation-worker sh -c 'cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max'
$ docker exec formation-worker conntrack -S

La première commande affiche le nombre d'entrées actuelles puis le maximum ; le rapport indique la marge. La seconde donne des compteurs par processeur, dont insert_failed (conflits à l'insertion, la condition de course) et drop. Des valeurs qui augmentent signalent un problème ; une valeur stable à zéro est saine. Surveillez ces mesures plutôt que de les lire une fois.

Sous le capot

La synchronisation. kube-proxy ne réagit pas à chaque événement en réécrivant les règles : il regroupe les changements et déclenche une synchronisation au plus toutes les minSyncPeriod, et au moins toutes les syncPeriod. En iptables, il génère un fichier de règles complet pour les chaînes qu'il gère et le charge d'un coup avec iptables-restore --noflush : la mise à jour est atomique pour le noyau, mais son coût croît avec la taille de la table. En nftables, il envoie des transactions qui ne portent que les différences, ce qui est la source du gain d'échelle.

Les points d'accès terminating. Une EndpointSlice marque un pod en cours d'arrêt ready: false mais serving: true. kube-proxy retire alors le pod de la rotation normale. Avec externalTrafficPolicy: Local, quand il ne reste aucun pod prêt sur le nœud mais qu'un pod en cours d'arrêt répond encore, kube-proxy peut l'utiliser en dernier recours : les connexions en cours ne sont pas coupées brutalement pendant un déploiement progressif. C'est la raison pour laquelle les trois conditions existent.

La limite des probabilités. La répartition aléatoire donne une distribution uniforme en moyenne, pas un équilibrage de charge : elle ne tient compte ni du nombre de connexions en cours, ni de la charge d'un pod. Pour de longues connexions (gRPC, WebSocket), une répartition uniforme des connexions n'est pas une répartition uniforme de la charge. Il faut alors un équilibrage au niveau applicatif, ou un maillage de services (leçon 9).

Pièges courants

Changer de mode en production sans test. Le passage d'iptables à nftables change le comportement des NodePorts sur localhost ; un composant qui s'y appuyait casse. Testez sur un cluster d'essai, et fixez le mode explicitement dans la configuration.

Chercher les règles au mauvais endroit. Sur un cluster où Cilium remplace kube-proxy, iptables-save ne montre aucune chaîne de Service : on croit à une panne alors que les règles sont dans des tables eBPF. À l'inverse, en mode nftables, iptables-save ne montre pas les règles de kube-proxy (elles sont dans nftables). Vérifiez d'abord le mode actif.

nf_conntrack: table full, dropping packet. Des connexions échouent par intermittence sur un nœud chargé. Diagnostic : le rapport nf_conntrack_count sur nf_conntrack_max. Remèdes : relever la taille (conntrack.maxPerCore), réduire les délais d'expiration, réutiliser les connexions côté applications, répartir la charge DNS (NodeLocal DNSCache, leçon 5).

Un client UDP qui continue d'écrire vers un pod mort. Une entrée conntrack périmée. Vérifiez : conntrack -L -p udp pour la destination ; en cas d'urgence, conntrack -D -p udp --orig-dst <adresse-du-service> supprime les entrées concernées. Une cause durable doit être cherchée du côté de la purge de kube-proxy.

Une affinité qui concentre la charge. sessionAffinity: ClientIP derrière un répartiteur ou un SNAT : un seul pod reçoit tout. Symptôme : un pod à 100 % de processeur, les autres au repos. L'affinité a un sens pour une application à état en mémoire, pas pour Signalements.

internalTrafficPolicy: Local sur un Service ordinaire. Un pod appelant sur un nœud sans pod du Service voit ses connexions abandonnées. N'utilisez cette politique que si un pod local existe partout (DaemonSet).

Sécurité

  • kube-proxy est un composant privilégié (il modifie le pare-feu du nœud) : il tourne avec des droits élevés (NET_ADMIN) et un compte de service qui lit tous les Services et EndpointSlices du cluster. Gardez-le à jour, ne donnez pas son compte de service à d'autres usages.
  • Ses règles ne sont pas un pare-feu. Elles traduisent, elles ne filtrent pas les pods entre eux : tout pod joint tout Service. Les NetworkPolicies, appliquées par le plugin, filtrent (leçon 8).
  • Les externalIPs : un Service peut déclarer des adresses externes que kube-proxy se met à traduire, et quiconque peut créer un Service peut ainsi capter le trafic destiné à une adresse arbitraire, y compris celle d'un autre service. La fonction est dépréciée depuis Kubernetes 1.36 (voir la leçon Services et DNS) ; limitez-la par une politique d'admission, et préférez un répartiteur ou une passerelle.
  • localhostNodePorts : en iptables, rendre les NodePorts joignables via 127.0.0.1 demande route_localnet, un réglage qui peut exposer des services qui n'écoutaient que sur le localhost du nœud à des voisins du réseau. En nftables, c'est désactivé par défaut, et c'est plus sûr : n'activez pas localhostNodePorts sans nécessité.
  • Le remplacement par Cilium concentre la fonction : la compromission de l'agent donne la maîtrise de la traduction des Services. Mêmes mesures que pour un CNI : versions à jour, RBAC strict sur ses objets.

En production

  • Fixer le mode dans la configuration (mode: nftables ou iptables), jamais « par défaut », pour qu'une mise à jour de Kubernetes ne le change pas à votre insu. Pour un nouveau cluster auto-géré avec un noyau récent, nftables est le bon point de départ ; sur une offre managée, le choix revient au fournisseur.
  • Dimensionner conntrack d'après le profil du nœud, et le superviser (nf_conntrack_count sur nf_conntrack_max, compteurs insert_failed et drop), avec une alerte à 70 ou 80 % de remplissage. Une saturation est une panne partielle, difficile à reconnaître sans cette mesure.
  • Régler les périodes de synchronisation sur les très gros clusters : augmenter minSyncPeriod (par exemple de 1 s) réduit la charge de réécriture au prix d'un délai de propagation plus long. La documentation le recommande pour les clusters de plusieurs milliers de Services.
  • Évaluer le remplacement quand les Services se comptent en milliers, que la latence ou le processeur des nœuds compte, ou que vous voulez de l'observabilité par flux. Pour un cluster de quelques dizaines de Services, kube-proxy suffit largement : ne changez pas pour changer.
  • Préférer trafficDistribution: PreferSameZone pour les appels internes très fréquents dans un cluster multi-zones, pour la latence et les frais de trafic entre zones. Garder le comportement par défaut pour les Services à faible volume.

Exercices

1. Lire la répartition (niveau 200). Un Service a quatre points d'accès. Quelles probabilités s'écrivent sur les quatre règles de KUBE-SVC-..., et pourquoi la dernière n'en a-t-elle pas ?

Solution

Avec N = 4, la règle i porte 1/(N-i+1) : 1/4, 1/3, 1/2, et la dernière n'a aucune condition. Probabilités cumulées : 1/4 ; 3/4 × 1/3 = 1/4 ; 3/4 × 2/3 × 1/2 = 1/4 ; 3/4 × 2/3 × 1/2 × 1 = 1/4. La dernière règle n'a pas de probabilité parce qu'elle est atteinte seulement si les trois précédentes n'ont pas été prises : il ne reste que cette option, elle est donc prise à coup sûr.

2. Choisir le mode (niveau 300). Lyneko prépare un cluster auto-géré de 12 nœuds (noyau 6.x) avec environ 150 Services. L'équipe hésite entre iptables, nftables, ipvs et Cilium en remplacement. Quelle recommandation, et quels tests avant de la valider ?

Solution

À cette échelle, les quatre options sont performantes : le critère est l'exploitation. ipvs est exclu (déprécié, retrait annoncé en 1.43). Entre iptables et nftables, nftables est préférable pour un cluster neuf (stable depuis 1.33, avenir du projet, noyau suffisant), à condition de tester les cas de localhost sur NodePort. Cilium en remplacement est pertinent si Cilium est de toute façon le plugin (c'est le cas sur Kapsule) et que l'on veut l'observabilité et les politiques étendues ; sinon c'est un surcoût de complexité. Tests : charge réaliste avec mesure de latence, bascule d'un pod (pas d'erreurs avec arrêt gracieux), comportement des NodePorts et du répartiteur, supervision de conntrack.

3. Le DNS qui perd un paquet sur dix (niveau 300). Des pods voient des résolutions DNS qui prennent 5 secondes, de façon intermittente, surtout en rafale. conntrack -S montre un insert_failed qui augmente sur le nœud. Expliquez le mécanisme et deux remèdes.

Solution

Le résolveur envoie les requêtes A et AAAA presque simultanément depuis le même socket UDP, donc avec le même quintuplet. Les deux paquets créent deux entrées de conntrack en concurrence, car la traduction vers l'adresse de CoreDNS (DNAT du Service kube-dns) n'est pas encore inscrite pour le premier : l'une des insertions échoue (insert_failed), le paquet est abandonné, et le résolveur attend son délai de 5 secondes avant de réessayer. Remèdes : NodeLocal DNSCache (le pod interroge un cache local sans DNAT, et le cache parle à CoreDNS en TCP), ou l'option de résolveur single-request-reopen ou use-vc dans dnsConfig (requêtes séquentielles ou en TCP). Le premier est le remède standard en production (leçon 5).

4. Les règles qui ne sont plus là (niveau 200). Sur un nœud d'un cluster Kapsule, iptables-save -t nat | grep KUBE-SVC ne renvoie rien alors que les Services fonctionnent. Est-ce inquiétant ? Comment le vérifier ?

Solution

Pas forcément : si Cilium remplace kube-proxy, il n'y a pas de chaînes KUBE-SVC, les Services sont traduits en eBPF. Vérifications : kubectl -n kube-system get daemonset kube-proxy (absent), kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep KubeProxyReplacement (actif), et cilium-dbg service list qui doit lister vos Services et leurs points d'accès. Si, au contraire, ni kube-proxy ni Cilium en remplacement ne tournent, alors les Services ne fonctionneraient pas : ce serait inquiétant.

Récapitulatif

  • kube-proxy observe Services et EndpointSlices et programme le noyau de chaque nœud ; il n'est sur le chemin d'aucun paquet, et les règles continuent à fonctionner s'il s'arrête.
  • En iptables : KUBE-SERVICES (entrée) → KUBE-SVC-* (répartition par probabilités 1/(N-i+1)) → KUBE-SEP-* (DNAT) ; parcours séquentiel, réécriture complète à chaque synchronisation.
  • nftables (stable depuis 1.33, noyau 5.13 ou plus) : tables de correspondance et mises à jour incrémentales, meilleure échelle ; attention aux NodePorts sur localhost. ipvs est déprécié (retrait annoncé en 1.43).
  • Cilium peut remplacer kube-proxy (kubeProxyReplacement=true, adresse directe du serveur d'API), en eBPF ; diagnostic par cilium-dbg, pas par iptables.
  • conntrack : table pleine, entrées UDP périmées, course sur le DNAT en UDP (insert_failed) sont les pièges ; à superviser.
  • sessionAffinity, trafficDistribution et internalTrafficPolicy modifient le choix du pod, de la simple préférence (trafficDistribution) à la contrainte (internalTrafficPolicy: Local).

Pour aller plus loin

  • La page Virtual IPs and Service Proxies de la documentation de Kubernetes, qui décrit les modes et leurs différences.
  • La référence de configuration de kube-proxy (KubeProxyConfiguration), pour les paramètres conntrack, iptables et nftables.
  • La page Kubernetes Without kube-proxy de Cilium, pour les prérequis et la validation.
  • La leçon suivante, Le DNS du cluster, où conntrack revient en force avec les timeouts de 5 secondes.
Voir ma constellation →

Sources