容器 29:Pod IP 与 Service 后端如何关联
四份对象信息各管哪一段
Pod status 的 IP 表示运行时/CNI 所配置网络视图里的地址;单机 veth/bridge 机制已在 22–24 讨论。它不证明另一台节点能路由到此 IP,也不保证上层进程在正确接口和端口监听。Service spec 给客户端一个稳定的入口和端口映射;ClusterIP 通常是虚拟服务地址而不是运行着 probe-app 的第三台机器。EndpointSlice 存放 Service 当前的后端地址/端口与条件,控制器据 selector 与 Pod 状态更新它们。集群 DNS 把名字解析成可供客户端使用的地址;返回 DNS 答案没有检查后端应用是否能读写。
以本篇清单为例,两只后端 c29-backend-a/b 都标记 app=c29-backend,Service c29-svc 按此标签选它们;客户端 Pod 在同一自建 namespace,用 c29-svc 或 c29-svc.c29-dedicated.svc... 查询 DNS。正常的 ClusterIP Service 解析通常返回入口 IP,Headless Service 才按其特殊配置返回后端记录;不能把两种解析模式混为一谈。Service 端口 18080 与 Pod 中程序监听的 18080 是分别声明和实现的:端口配置写错时 DNS 仍可成功,却不产生正确的 TCP 连接。任何把 “DNS 已好” 直接译作 “HTTP 已好” 的结论,都跳过了入口选择和应用两层。
短名称的解析还与客户端 Pod 的 namespace、dnsPolicy 和 /etc/resolv.conf 的搜索域有关。从本篇 c29-allowed 里查询 c29-svc 与在另一个 namespace 里查询同一个短名称,不一定指向同一 Service;解析失败可能是域名范围错误,也可能是 CoreDNS 访问失败。真正取证时保留客户端的 namespace、resolv.conf、短名与带 namespace 的名称各自解析结果,不把控制终端上运行的 nslookup 输出当作 Pod 内答案。解析得到入口地址之后,仍须发起 TCP/HTTP 请求,并把这一请求的源 Pod UID 与网络空间记录下来。
EndpointSlice 中可能同时包含 ready 与 not-ready、terminating 的 endpoint。应核对每个 endpoint 的 targetRef.uid 是否指向本次真实 Pod UID,地址和端口是否对应它的 status.podIP/监听端口,而不是数一数对象里有两个地址就宣布有两个可服务后端。控制器同步有延迟,节点的实际转发表也可能晚于 API 对象可见;对一次具体请求,保存它开始时的 Pod/EndpointSlice JSON、后端真实响应和客户端异常,才有机会判断是在入口前丢失,还是已进入后端却遇到业务错误。
一个 Service 可以由不止一个 EndpointSlice 表示,更新过程中还可能短暂看到旧 Slice 已有终止中的地址、新 Slice 新增替代地址。不能盯住某个固定 Slice 名称取一次 JSON 后,说 Service 后端“没有变化”;应该按 kubernetes.io/service-name 标签枚举这一 Service 的所有 Slice,记录同一采样窗口中每个地址的条件和 targetRef。targetRef 缺失或 endpoint 非 Pod 来源时,不能强行把那个 IP 配给 c29-backend-a。28 的 ready 路径与本篇数据面入口之间有传播间隔,控制面在 T1 时的选择也不保证 T1 同刻每个节点都采用了相同转发表;只能用实际数据描述时间边界。
分三跳记录一次成功请求
在两只 Pod 内让同一份 probe-app 源码包 绑定 0.0.0.0:18080 并提供 /identity。第一跳从专用客户端直接请求 http://<Pod A IP>:18080/identity 与 Pod B IP,分别保存返回 PID、net namespace inode 和 cgroup;不同 Pod 可能都看到自己的 pid=1,所以不能只靠 PID 数字判定是同一后端。第二跳从客户端读取 socket.getaddrinfo("c29-svc", 18080) 的真实答案,确认 Service spec 的 selector、ClusterIP 和 EndpointSlice 两只 Pod 的 UID/条件。第三跳请求 Service 名 http://c29-svc:18080/identity,把返回的 PID 与 namespace inode 同先前两份直连结果比对,记录究竟哪只后端处理了本次请求。
直接请求两只 Pod IP 是在建立一张短期“响应身份字典”:返回的 /identity 对象与当时的 Pod UID、containerID 成对保存,之后才能解释 Service 请求命中的进程。Pod 一旦重启或重建,旧字典必须失效,即便其显示名称或 IP 恰好被重新分配,namespace inode、容器 ID 与请求时间仍可能变化。Service 并不自动在响应里附上 Pod UID,因此若实际代理只返回业务数据而没有身份字段,还需在应用日志或可信观测链里明确后端 ID;做不到就不应宣称某次请求已被 A 而非 B 处理。仅凭返回 200,不包含来源身份信息。
这个链只能支持“这些请求实际到达了某个后端”。Service 选择不等于两只 Pod 应严格各处理一半请求;连接跟踪、HTTP 连接复用、session affinity 和节点路由都会影响样本。探针应用使用 Python 简单 HTTPServer,不提供生产负载均衡统计,连续访问有限次若都落到 A,不能据此认定 B 永远不接流量;反过来若两端各有一次响应,也不能证明所有客户端和所有节点的服务路径均正确。要研究转发算法,先固定所用 Service 实现、客户端源/连接行为与统计窗口,本篇只核查身份和路径。
这里的传输协议也要固定。Kubernetes Service 可以为不同端口和协议分别声明映射,本篇只讨论 TCP 18080。DNS 查询通常先为客户端提供 Service 名的地址;真实连接建立需要经过 TCP SYN、目标 Service 的端口映射,随后是应用的 HTTP 请求/响应。即使 DNS 在控制平面更新,客户端本地缓存可能仍保存旧答案;即使 EndpointSlice 改变,已建立连接也不会像一个全新 SYN 那样重新选择后端。要复现“更新后请求仍命中旧 Pod”,先说明是在复用旧连接还是创建新连接,并抓取当次四元组/五元组,避免把两个有不同前提的结果相互否定。
Service 数据面的实现不是写死的 iptables
Kubernetes Service API 描述入口与后端,不要求任何节点都必须用同一种实现。固定阅读定点 Kubernetes v1.33.4 的 pkg/proxy/iptables/proxier.go 能说明 iptables 模式怎样维护规则,但目标 kind 节点也可能用其它 kube-proxy 模式,或由替代网络实现接管 Service 转发。只有先读节点 kube-proxy/插件的真实模式、版本、规则或 BPF 程序状态,才能谈这一次 SYN 如何选中后端;不能从读过一份定点源码倒推出云端已存在对应 iptables 链。本云端没有 kind、kube-proxy 或 ip,相应命令和原始表项全部留空。
即便 VM 确实采用 iptables 模式,链里某条目的与 Service 相符的规则也只证明当时配置的一个侧面,不能单靠规则存在断定程序正常:对应的 EndpointSlice 可能已过期,conntrack 可能让旧连接沿先前映射继续,node-local DNS 可能返回另一个地址,网络策略可能在不同钩子位置丢弃流量。核对一条失败请求时应从客户端提供真实源 Pod IP、目的 ClusterIP:端口、协议、开始时刻,再查该节点的模式和后端选择。若节点使用 IPVS 或 eBPF,仍需记录相同输入但更换观测对象,不能把没有 iptables 链当作 Service “未安装”的证据。
若 Service 访问失败、Pod IP 直连成功,排查顺序先看两只 Pod 是否仍 Ready、EndpointSlice 是否列出正确地址/端口,再看 DNS 解析和客户端发向的目标;两者都正确后才查运行节点的 Service 转发模式、conntrack 和 NetworkPolicy。没有后端时有些实现立即拒绝,有些路径可能超时;只凭客户端一个 timeout 不能说某条 iptables 规则丢了包。若 DNS 名称拼错、客户端在错误 namespace,或后端程序只监听 127.0.0.1,改 Service 转发规则并不能修复根因。22 的 veth 正常也不证明 29 的虚拟入口已经生效。
排障时还要把采样窗口压到足够小:先固定请求发起的客户端 Pod UID 和所在节点,再在同一时间窗保存 Service、所有 EndpointSlice 与两只后端的 UID/Ready 状态。若期间有 Pod 重建,原地址可能被释放并重新分配;在重建后拿新 Pod 的状态反推之前那次超时,会把两个不同后端当成同一个。定位这种竞态需要保留请求的原始时间与对象 resourceVersion,不能只截一张最终看起来正常的资源列表。
跨节点时还要补 25 的 underlay/VXLAN 路径。EndpointSlice 正确引用远端 Pod IP,但节点之间路由、封装 MTU、ACL 或 CNI 回程失败,请求一样可能在后端前消失。两端 Pod 都在同一 kind 节点做出的 200,不具备“跨主机也通”的外推资格;kind 的多 node 容器还可能实际运行在同一台 Linux VM 上,不等于 25 的双独立 VM。迁移到真多节点时,按客户端节点、后端节点、node IP、Pod IP 与封装端点分开抓包取证,不能拿控制面的两个 IP 字段填满数据面路径。
一个返回码能否对应一次具体转发还依赖响应记录粒度。若客户端库自动重试,上层只报最终成功,第一次到 A 失败、第二次到 B 成功可能被折叠成一个 200;此时 probe_app.py 的单次身份结果仅标识成功的那次响应。需要保留客户端重试配置、每次连接的新旧五元组和实际异常,不宜用“只要重试成功,Service 就没故障”跳过后端失败。若代理在 Kubernetes Service 前另有 L7 负载均衡,分析时再加代理上下游日志,避免把代理重试当成 kube-proxy 的选择逻辑。
策略为什么“对象创建成功”仍可能完全没有效果
Kubernetes NetworkPolicy 定义选中 Pod 的允许规则;API 可以存储对象,实际过滤需要所选插件明确支持并且在这些节点上已启用。c29-policies.yaml 的第一条在同篇两个后端上声明空 ingress,第二条只允许同 namespace 标了 role=c29-allowed 的客户端访问 TCP 18080。默认拒绝与允许是并集,不是后创建的一条覆盖先创建的一条。另一只 role=c29-blocked 的客户端在策略生效后应无法从同一网络视图访问两个后端 Pod IP,也应无法通过 Service 名实现有效连接;但节点对 Service DNAT 与策略匹配的时序/错误形态仍由实际插件实现决定,不能预填一个“理应 HTTP 403”的应用状态码。
策略在网络层过滤,与应用身份认证是两件事。blocked Pod 若因 NetworkPolicy 被拒,probe-app 根本可能看不到这个 HTTP 请求,自然也不能期待它返回 403 或留下应用请求日志;一条 HTTP 403 反而提示有组件收到了请求并主动给出应用层回应,应再查那是代理还是 probe-app。allowed Pod 已被允许也不意味着它具备写入 /state 的权限或能拿到业务认证令牌。本篇故意选只读 /identity,仅证明在这一端口/来源上的连通与身份,不能把允许规则解释成完整访问控制设计,更不把 namespace 隔离误认为跨租户安全沙箱。
NetworkPolicy 的 podSelector 若不配 namespaceSelector,源 Pod 范围是该策略所在 namespace 内匹配标签者。本篇两客户端故意放在同一专用 namespace,确保测试的是 Pod 身份标签差异;在别的 namespace 有同名 role=c29-allowed 不自动被允许。本篇只选 policyTypes: Ingress,并未限制客户端向 DNS 发请求的 egress,所以“被禁止客户端仍能解析 Service”不是策略失效。若实验要求控制跨 namespace 或外部 IP,需增加另一套明确的策略规则和独立来源对照,不把此 YAML 的成功当成所有来源隔离已经验证。
矩阵每一行都应注明客户端从哪个 Pod、哪个 network namespace 发起,不能在节点宿主上运行同一 curl 后直接把它算成 blocked Pod 的请求:hostNetwork、宿主进程、节点到 Pod 的流量与普通 Pod→Pod 策略适用条件可能不同。Service 经 NAT 后策略引擎看到的来源地址也可能受实现与流量路径影响;如果某插件无法说明本次 Pod 来源如何匹配标签,结论应收窄为“这个插件版本、这条请求路径下观测到拒绝/允许”,不把它泛化为所有入口的保证。DNS 解析请求也来自不同目的,能从 CoreDNS 拿到地址只说明名字解析成功。
插件若不支持策略,即便 kubectl apply -f c29-policies.yaml 退出 0,客户端也可能继续访问后端;这不能拿来证明 NetworkPolicy 规范规定允许该连接。应先固定插件版本、数据面/策略 agent 配置和实现源码,再在 VM 中验证它确实在目标节点下发规则。反过来,如果 blocked 客户端超时,也不能单靠超时证明是插件拒绝:直连基线、allowed 客户端同目标、后端 Pod Ready 和 DNS 解析都必须在同一窗口成立,才能缩小到策略。若还有其它命名空间/集群层的安全组或默认策略,隔离变量失败,就不能把结果完全归因于本篇两个对象。
策略更新也需要观察窗口。首次 kubectl get networkpolicy 看见两条对象后,节点代理未必已同步,立即运行客户端可能观察到旧转发表;按固定 3 秒超时重试有限轮,记录策略创建 UTC、插件日志的变更时间与每次请求退出码。不能为得到预期“blocked 一定失败”无限循环直到某一刻碰巧超时;也不能把旧 TCP 连接复用时的持续成功视为新连接策略必定失效。A/B IP 与 Service 三个目标应分别建立新连接,用同一客户端来源、同一 Pod UID、同一错误统计口径,前后对照才有可解释的因果关系。
把正反路径写成矩阵
两后端、Service 与双客户端先创建一套**没有本篇策略**的基线,记录四只 Pod 的 UID、真实 IP、容器 ID、端点条件。allowed/blocked 两只客户端各对 A IP、B IP、Service 名发请求:在插件正常且无其它限制时,三列均应可达。之后才施加两个同篇策略,在相同 Pod UID/地址、客户端、超时和负载下重跑。按约定,允许客户端在三列仍能访问,被拒客户端在三列不能访问;每一格需要真实 HTTP 状态/异常、命令退出码与 UTC,不能以 YAML 匹配关系代替客户端观测。正例必须先于故障注入,否则坏了一个 Pod 或镜像时“拒绝矩阵”会被无关故障污染。负例至少包含两种互不等价的症状:第一种是 readiness 变 false,EndpointSlice 对应后端 ready 变 false,但直接 Pod IP 的应用还可能答 200;第二种是 NetworkPolicy 使选中的来源被拒,此时两后端仍可 Ready,allowed 客户端照常请求成功。若两者都表现为“Service 请求失败”,要回到控制面 endpoint 条件和策略来源矩阵,不可只加一个允许规则碰运气。独立坏就绪 Pod 清单提供与 A/B 相同应用摘要的第三只 Pod,唯独 readiness 路径指向不存在的 URL;它是新 Pod而非让某只现存 Pod 原地切换就绪,应用本身的 /identity 仍可直接回应。先取证并删掉该 Pod,再施加策略矩阵,避免两个负例同时存在而混淆原因。
在这种负例中,EndpointSlice 仍可能记录第三只 Pod 的地址但 conditions.ready=false;所以“它出现在列表”与“Service 新连接会选它”不是同一断言。应把第三只 Pod 的 targetRef.uid、readiness 事件与直接请求的 /identity 对上,再用 Service 请求确认它没有被正常可用后端集合选中。若 c29-notready 的 Python 进程在配置 /ready 探针之前就没启动,连直接请求也失败,问题落在镜像或进程阶段而不是预设的就绪差异。删除该 Pod 时,A/B 不应因为它消失而重建;如有重建事件,先保留身份变化再跑策略对照,不把两轮流量揉在一起。
删掉两条自建策略后,再从 blocked 客户端重试同一 Service/Pod IP URL,检查是否真正恢复。若策略规则已删但客户端仍失败,可能有其它规则、数据面同步延迟、旧连接状态或新出现的 Pod 故障;保留这个真实反例而不是直接删除所有 namespace 清场。配置恢复与请求恢复各自需要单独证据;禁止在共享宿主上 flush 全局防火墙或删除其它团队的 NetworkPolicy 来“使示例通过”。
删除操作的范围同样是实验结果的一部分。策略对象通过名字和本篇 namespace 限定,客户端/后端 Pod 在清理前最后一次读取 UID 和 containerID;若某次创建失败,并没有对应对象,清理只跳过它而不去碰同名其它命名空间。Service 删除后 DNS 缓存可能仍短时保存旧 ClusterIP,所以不能靠“客户端还解析得出旧 IP”推断 API 中的 Service 尚在;以 API 对象与新连接请求区分状态。只有再确认本篇两个策略不存在、Pod 和 EndpointSlice 从所选 namespace 消失,且实验前基线网络未改变,才可声称这一轮自建资源已回收。
迁移判断:输入不通先问它经过了什么
“DNS 有答案”应从名称/作用域转向 Service spec 与后端;“直连两个 Pod 均正常而 Service 不通”应转向 EndpointSlice 条件、Service 端口与该节点的转发实现;“allowed 正常 blocked 失败”要复核插件版本与标签匹配,检查后端是否仍 Ready;“所有入口时好时坏”才比较节点路由、MTU、重试与后端应用日志。对每一步先找能被直接采样的对象,才选择假设,不能用同一个名词“网络问题”涵盖 DNS、代理、CNI、策略与应用五个不同阶段。
迁移到另一种 CNI 或关闭 kube-proxy 时,这套顺序仍以 API 身份为起点,但实现断言必须重新验证:Service 是 iptables、nftables、IPVS 还是 eBPF,策略在 Pod ingress、veth、宿主 tc 或其它位置执行,是否支持原生 NetworkPolicy,都要根据实际安装版本回答。NodePort、外部网关和 hostNetwork 客户端会增加来源地址/NAT 和安全边界,不能把本篇同 namespace 的 ClusterIP 允许/拒绝矩阵直接推广到它们。控制面的 DOC_VERIFIED 和页面 STATIC_CHECKED 只证明概念与材料存在;没有真实插件及请求原始数据,数据面仍 NOT_RUN。
写结论之前可以用四问自检:这次请求的源身份是谁、DNS 的名称在哪个作用域解析、目标 Service 在哪个节点由哪种数据面实现、后端 UID 与应用响应是否来自同一个时间窗?四问里只要有一问缺原始证据,就把结论限制在已观察到的那一层。例如只拿到 EndpointSlice 可以说控制面当前列出哪些后端,却不能说包实际抵达;只拿到客户端一次 200 可以说请求成功,却不能说策略插件对另外的来源强制拒绝。范围写窄一点并非保守措辞,而是避免把两个无法等同的系统状态合成一个结论。
限域取证、拒绝矩阵与清理入口 指定了客户端、Pod IP/Service 两条路径、3 秒超时及逐项退出记录;YAML 字节校验和通用包分别可验证附件。当前云端没有 kind、支持策略的插件、可运行镜像 digest 或两个节点之间的网络,直连、DNS、Service 及策略场景一律未运行,不能用页面生成成功抵扣。| 症状 | 下一步核查 | 不可直接推断 |
|---|---|---|
| DNS 回答 ClusterIP、请求失败 | EndpointSlice、Service 端口、客户端连接 | DNS 解析即业务成功 |
| Pod IP 直连成功、Service 失败 | EndpointSlice UID/ready 与转发实现 | 本机 veth 必定已坏 |
| 后端 Ready、blocked 不通 | 策略插件版本与来源矩阵 | 一定是应用 HTTP 403 |
| Policy 对象存在、所有人都能访问 | 实际策略插件/规则及直连基线 | 策略规范保证放行 |
练习
- 先预测在专用集群把一只后端的 readiness 固定失败后,EndpointSlice 的
targetRef.uid、Service 响应后端身份与直接 Pod IP 请求各会怎样变化;用真实响应核对 API 对象,不把 Pod phase 误写成 Service 可用性结论。 - 在明确支持 NetworkPolicy 的专用插件中先写出 allowed/blocked × A IP/B IP/Service 的 2×3 矩阵,再施加同篇策略、保存六格真实退出码与恢复后的响应。若插件未知,说明为何
kubectl get networkpolicy的结果不能填入此矩阵。
系列总目录:从进程隔离到运行时与编排。

