背景与现象
同一个 Pod 的 readiness 和 liveness 探针日志显示连接的 IP 不一致(例如 10.10.6.10:9999
与 10.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 探针设置更宽松的
timeoutSeconds
和failureThreshold
,降低瞬时抖动影响。显式 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