当前位置: 首页 > ai >正文

Kubelet 探针如何选择 IP:status.PodIP 溯源与“同 Pod 两个 IP“现象解析

背景与现象

同一个 Pod 的 readiness 和 liveness 探针日志显示连接的 IP 不一致(例如 10.10.6.10:999910.10.6.32:9999)。本文从 kubelet 源码入手,解释探针目标 IP 的来源、为何会出现两个不同 IP,并给出建议与验证方法。

在如下探针配置下:

readinessProbe:initialDelaySeconds: 5periodSeconds: 10tcpSocket:port: 9999timeoutSeconds: 1
livenessProbe:initialDelaySeconds: 60periodSeconds: 15tcpSocket:port: 9999timeoutSeconds: 1

结论摘要

  • tcpSocket.host 未显式设置时,kubelet 探针使用“当下缓存”的 status.PodIP 作为目标主机。

  • readiness 与 liveness 是两个独立的 worker,按各自的周期、初始延迟读取 kubelet 的状态缓存。如果 Pod 在两次读取之间被重建(sandbox 重建、CNI 重新分配 IP),两者可能各自命中旧/新 IP,因此日志出现两个不同 IP。

源码调用链(关键片段)

  • 探针(TCP)如何选择主机:若 TCPSocket.Host 为空,使用 status.PodIP

if p.TCPSocket != nil {port, err := extractPort(p.TCPSocket.Port, container)if err != nil {return probe.Unknown, "", err}host := p.TCPSocket.Hostif host == "" {host = status.PodIP}klog.V(4).InfoS("TCP-Probe Host", "host", host, "port", port, "timeout", timeout)return pb.tcp.Probe(host, port, timeout)
}
  • 探针在每次执行前从 status manager 读取“当下缓存”的 v1.PodStatus

status, ok := w.probeManager.statusManager.GetPodStatus(w.pod.UID)
if !ok {// Either the pod has not been created yet, or it was already deleted.klog.V(3).InfoS("No status for pod", "pod", klog.KObj(w.pod))return true
}
  • status manager 的读取接口(返回缓存的 v1.PodStatus):

func (m *manager) GetPodStatus(uid types.UID) (v1.PodStatus, bool) {m.podStatusesLock.RLock()defer m.podStatusesLock.RUnlock()status, ok := m.podStatuses[types.UID(m.podManager.TranslatePodUID(uid))]return status.status, ok
}
  • kubelet 如何生成 PodIPs/PodIP 并写入 v1.PodStatus(先排序,再取首个作为 PodIP):

podIPs = kl.sortPodIPs(podIPs)
for _, ip := range podIPs {apiPodStatus.PodIPs = append(apiPodStatus.PodIPs, v1.PodIP{IP: ip})
}
if len(apiPodStatus.PodIPs) > 0 {apiPodStatus.PodIP = apiPodStatus.PodIPs[0].IP
}
  • Pod 的 IP 列表来自 CRI 报告的 sandbox 网络状态:

func (m *kubeGenericRuntimeManager) determinePodSandboxIPs(podNamespace, podName string, podSandbox *runtimeapi.PodSandboxStatus) []string {podIPs := make([]string, 0)if podSandbox.Network == nil {klog.InfoS("Pod Sandbox status doesn't have network information, cannot report IPs", "pod", klog.KRef(podNamespace, podName))return podIPs}if len(podSandbox.Network.Ip) != 0 {if net.ParseIP(podSandbox.Network.Ip) == nil {klog.InfoS("Pod Sandbox reported an unparseable primary IP", "pod", klog.KRef(podNamespace, podName), "IP", podSandbox.Network.Ip)return nil}podIPs = append(podIPs, podSandbox.Network.Ip)}for _, podIP := range podSandbox.Network.AdditionalIps {if nil == net.ParseIP(podIP.Ip) {klog.InfoS("Pod Sandbox reported an unparseable additional IP", "pod", klog.KRef(podNamespace, podName), "IP", podIP.Ip)return nil}podIPs = append(podIPs, podIP.Ip)}return podIPs
}
  • 当 sandbox 变化时,kubelet 会覆盖当前的 podIPs

if !kubecontainer.IsHostNetworkPod(pod) {// Overwrite the podIPs passed in the pod status, since we just started the pod sandbox.podIPs = m.determinePodSandboxIPs(pod.Namespace, pod.Name, podSandboxStatus)klog.V(4).InfoS("Determined the ip for pod after sandbox changed", "IPs", podIPs, "pod", klog.KObj(pod))
}
  • 仅从“最新且 READY 的” sandbox 读取 IP:

// Only get pod IP from latest sandbox
if idx == 0 && podSandboxStatus.State == runtimeapi.PodSandboxState_SANDBOX_READY {podIPs = m.determinePodSandboxIPs(namespace, name, podSandboxStatus)
}
  • TCP 探针最终发起连接的位置:

func (pr tcpProber) Probe(host string, port int, timeout time.Duration) (probe.Result, string, error) {return DoTCPProbe(net.JoinHostPort(host, strconv.Itoa(port)), timeout)
}
  • hostNetwork 场景:若 PodIP 为空,用节点 IP 初始化 PodIP/PodIPs

s.HostIP = hostIPs[0].String()
if kubecontainer.IsHostNetworkPod(pod) && s.PodIP == "" {s.PodIP = hostIPs[0].String()s.PodIPs = []v1.PodIP{{IP: s.PodIP}}if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && len(hostIPs) == 2 {s.PodIPs = append(s.PodIPs, v1.PodIP{IP: hostIPs[1].String()})}
}

流程图

为什么会出现两个不同的 IP

  • readiness 与 liveness 的 worker 独立运行、定时时间不同;每次探测都“就地读取”缓存中的 v1.PodStatus

  • 若该 Pod 在两次读取之间 sandbox 重建(IP 变化),一个 worker 可能还读到旧 IP,另一个已经读到新 IP,于是日志显示不同地址。

  • 双栈时,sortPodIPs 会按节点 IP 家族偏好排序,导致 PodIP(主 IP)在不同条件下选择不同家族的地址,也会引起切换。

  • timeoutSeconds=1 对 TCP 探针较苛刻,网络抖动时更易出现超时和重试导致的时序差异。

配置与排查建议

  • 合理的时序参数:为 TCP 探针设置更宽松的 timeoutSecondsfailureThreshold,降低瞬时抖动影响。

  • 显式 host(可选):在明确网络拓扑的前提下设置 tcpSocket.host,避免依赖 status.PodIP 切换窗口。

  • 关注 hostNetwork 与双栈:hostNetwork 以节点 IP 为准;双栈可能改变主 IP 选择。

  • 对齐重建时间线:结合 CNI/runtime 与 kubelet 日志,确认 IP 切换是否由 sandbox 重建触发。

FAQ

  • status.PodIP 存在哪里?

    • 在 kubelet 的内存缓存(status manager)中,以 UID -> versionedPodStatus 记录,探针通过 GetPodStatus 读取。

  • 探针为什么不使用“同一时刻”的统一状态?

    • readiness/liveness 分属不同 goroutine,按各自周期读取缓存的快照,没有全局“同一时刻”的合并视图。

  • 非 hostNetwork Pod 的 PodIP 从何而来?

    • 来自 CRI 的 sandbox 网络状态(primary + additional IPs),经 kubelet 排序、选主后写入。

参考文件清单

官方源码https://github.com/kubernetes/kubernetes/tree/release-1.22

  • pkg/kubelet/prober/prober.go

  • pkg/kubelet/prober/worker.go

  • pkg/kubelet/status/status_manager.go

  • pkg/kubelet/kubelet_pods.go

  • pkg/kubelet/kuberuntime/kuberuntime_manager.go

  • pkg/kubelet/kuberuntime/kuberuntime_sandbox.go

  • pkg/probe/tcp/tcp.go

http://www.xdnf.cn/news/17295.html

相关文章:

  • Go 实用指南:如何执行 Skyline 查询(Pareto 最优点筛选)
  • PID学习笔记1
  • 基于springboot+vue开发的校园食堂评价系统【源码+sql+可运行】【50809】
  • 【洛谷题单】--分支结构(三)
  • DigitalProductId解密算法php调试版piddebug.php
  • 七、《Serverless架构:按毫秒计费的成本革命》--从新浪AI推理平台50%效能提升看无服务器本质
  • vscode/trae 的 settings.json 中配置 latex 的一些记录
  • Android--监听软键盘弹出隐藏事件
  • CamX-骁龙相机修改
  • BPMN编辑器技术实现总结AI时代的工作流编辑器
  • 香港服务器容器网络插件的多节点通信性能基准测试
  • 从灵感枯竭到批量产出:无忧秘书创作平台如何重构内容生产者的工作流程?全环节赋能分析
  • 分布式锁详解及 Spring Boot 实战示例
  • K-means聚类学习:原理、实践与API解析
  • 电子电气架构 --- 48伏电气系统架构
  • Docker Desktop 使用操作指南
  • 微服务的好与坏
  • DAY 39 图像数据与显存
  • 移动端开发中类似腾讯Bugly的产品推荐与比较-5款APP异常最终产品推荐-卓伊凡|bigniu
  • 线程池分析与设计
  • 全面了解selenium
  • [linux] Linux:一条指令更新DDNS
  • Docker容器部署discuz论坛与线上商城
  • Uber的MySQL实践(一)——学习笔记
  • python学智能算法(三十五)|SVM-软边界拉格朗日方程乘子非负性理解
  • token过期为了保证安全,refresh token不过期,那么拿到refresh token就可以获取token,不还是不安全吗
  • Java基础-模拟多线程安全问题场景
  • 开发板RK3568和stm32的异同:
  • 深入理解 SwiftUI 布局:VStack、HStack 和表单控件全解析
  • 关于数据结构6-哈希表和5种排序算法