背景與現象
同一個 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