本系列的每个对象都有不同寿命:源码树变化不等于已部署镜像改变;Pod 重建不等于数据卷重建;HTTP 请求成功也不能证明读写结果符合预期。综合实验必须让一条请求沿同一个 digest、容器 ID、Pod UID、宿主 PID、namespace/cgroup、网络路径和卷挂载回溯到源码,而不是把不同实验的截图接成故事。

只用一份 probe-app

从干净的实验目录取独立源码包并校验 SHA,固定基础镜像 digest,按13构建真实镜像,按12分发到受控 registry,再按26将 digest 部署到自建 kind 节点。通过 Service 访问 /identity 取得进程信息,通过 /state 写入一段最多 4096 字节的合成文本;从 Pod UID 到容器 ID、宿主 PID,查 namespace inode、cgroup、网络地址/路由及卷的 mountinfo,最后在同一卷上重建 Pod 后复读内容。每一步记录输入、时间、原始响应与校验值,不能从“yaml 配好了”跳到“持久化成功”。

正常案例需要请求 JSON 中的 PID 和 UID 与节点对象一一对应,合成数据重建后可验证;边界案例让容器只能在 127.0.0.1 监听或改掉卷挂载,前者的服务入口不可达,后者“相同 URL 返回的数据”可能来自另一可写层。对照若缺任何中间对象,都须在证据链上标一个断点,不填预测 PID 或 digest。验证清单、本篇独立 Pod/Service/PVC 清单与 两轮取证一致性检查给出可复跑入口,专项字节校验防止附件失配;本机没有 registry、运行时/kind、卷与真实容器网络,完整端到端验收 NOT_RUN。本机 00/10 的独立小实验不能合并成一次全链路记录。

构建输入与分发身份不能拼在一起

源码包的 SHA-256 只能断言“手上这一份附件未变”,不能从这个摘要直接计算构建后的镜像 digest:基础镜像、Dockerfile 选项、构建上下文、平台以及构建器都会参与制品生成。取证从一次独占的空构建目录开始,把共用包解开并核对每个成员;使用本轮固定的基础镜像平台摘要构建受控探针,记录完整构建命令、工具版本、构建上下文与 tag。不要引用某次历史镜像同名标签补齐这一步。同一源文件在重跑后生成不同镜像时,必须先核对具体差异,不用“源哈希相同”宣布制品一致。

推送到自有 registry 后,再从本轮成功推送的结果取出 manifest/index digest,分别记录 registry 名称和目标平台。这是12 的分发协议验证点:本地 image ID 往往标识镜像配置对象,不是远端仓库发布的 manifest 摘要;docker image inspect 中过去留下的 RepoDigests 也可能不属于本次 push。Pod 模板里写入带 digest 的完整引用,提交后还需在实际调度节点确认已拉到所选 manifest 和对应 imageID。如果节点无法解析该平台或凭据不对,链条在 image pull 阶段中止,此时写出具体拉取错误,不给后面的运行时和网络栏填臆想对象。清洁环境与热缓存环境也应分开记录,不能把“不再下载 layer”误写为“镜像没有被验证”。

index digest 相等不保证两台不同架构节点运行了同一套 layer。需要按实际节点的 OS、架构与 variant 对照 index 中的 descriptor,再把选中的 manifest、config 和镜像 rootfs 关联到构建记录。若只有一台 kind 节点,验收范围也只覆盖该平台;另一个架构的选择不应从多平台 YAML 推断。镜像签名或来源证明为“可以信任这个制品”的独立依据,运行时通过 seccomp/capabilities/fsGroup 配置为“进程被怎样约束”的独立证据,两者不能互作替身。来源证明需要固定签名身份和准入策略,但本篇的主线仍是这一次实际服务响应,不以 33 的签名样例取代真实拉取与请求。

从 Pod UID 走到真正处理请求的进程

Kubernetes API 分配的 Pod UID 是这一轮 Pod 对象身份,不是 Container ID。只有把 Pod UID、调度节点名、sandbox ID 和业务容器 ID 在相同时间窗口关联,才能进入 CRI 节点视角;kubectl get pod 只提供集群侧元数据。按26 的节点路径进入实际调度的 kind 节点,在该节点用 crictl pods --id/inspect 找 sandbox,再由 Pod status 中业务 containerID 对照 crictl inspect 或所选 runtime 的容器元数据,最后取得宿主 PID。不能从本机执行 crictl 查一个偶然的默认 socket 后直接给出宿主 PID:kind 的节点容器与本机 shell 的 PID namespace 可能不同,PID 的数字必须明确属于哪一层。节点 runtime、shim 和 OCI runtime 三种对象各有自己的身份,shim PID 也不能顶替业务 PID。

为防进程退出后的 PID 重用,至少对宿主 PID 采两次 /proc/<pid>/stat 字段 22 的启动 tick,并保存请求前后的 Pod UID 和 containerID。如果读取期间容器已经重启,应重做关联,而不是把旧 PID 的 cgroup 填进新 Pod 的记录。/identity 返回的 pid 是服务进程在自己的 PID namespace 里的值;namespace.pid、namespace.net 等字符串描述容器内读取到的视图。用节点宿主 /proc/<pid>/ns/* 的 inode 与响应字段对照,仍要记录是谁打开的 /proc:如果宿主和进程 namespace 不同,数字 PID 可以不同,但同一个运行进程在同一时刻的 namespace inode 应能互相核对。cgroup 路径也要说明取自宿主 /proc 还是容器内部,若本机 cgroup v1 与目标 VM v2 不同,不拿两份文件字段混合计算资源使用。

在服务健康为 200 前,容器可能已经被 runtime 创建;在服务成功处理一次请求后,它也可能立刻退出。采样要覆盖容器 ID、PID 启动 tick、HTTP 开始/结束和事件时间线,才能排除拿到上一次已经终止的实例。解析日志时保留标准输出原始行和命令返回码;crictl API 成功、Pod Running、EndpointSlice Ready、应用返回 200 是四个逐步更强但并不等价的断言。不能拿到 sandbox ID 时,依然可以报告 Kubernetes 外部请求结果,却必须把“Pod UID 到宿主 PID”断在这一层,不能标成完整链路通过。

服务名字、路由和端点各对应哪一跳

客户端请求 http://probe:18080/identity 时,首先记录发起请求的独立客户端 Pod UID、网络命名空间与服务 DNS 的实际答复;客户端在同一个 Pod 里访问 127.0.0.1 只验证本地应用,不是 Service 入口。再从 API 获取 Service 的 clusterIP、端口和 EndpointSlice:当前 ready 后端应包含本轮服务 Pod UID 与 Pod IP,而非已经停止的旧 UID。DNS 能得到 Service 地址仍不足以证明端点存在;Pod IP 能直接访问也不足以证明 Service 转发已打通。要把一次真实 HTTP 200 归入本轮探针,记录请求 URI、状态码、响应体身份、Service 后端集合和时间区间,必要时在自建 Pod 的 netns 内抓有界报文。别因为 kubectl exec 能访问应用而跳过入口测试。

网卡和路由的证据从真正处理请求的 Pod 所在节点取,而不假定所有 kind 节点具有相同 CNI 配置。通过 CRI 取本次 sandbox 的 netns 视角,核对容器内 /proc/self/ns/net、Pod IP、接口与路由;在客户端视角核对到 Service 的目标,必要时采节点数据面的对应五元组。CNI 的 ADD 成功只是接口配置结果,/identity 走通还依赖 DNS、服务发现、数据面转发及服务进程监听。NetworkPolicy 仅当实际选用的插件执行策略才提供拒绝边界;若 kind 默认网络不能执行该策略,只能报告没有进行拒绝实验,不能把“当前客户端碰巧能通”写作已验证策略正确。失败时先区分解析错误、无后端、链路路由错误、过滤拒绝、应用 404/503,每一层用该层原始输出来证伪,不依据客户端统一的超时报文猜原因。

若本轮 Pod 被删除再由 Deployment 替换,DNS 名与 Service IP 可能保持不变,但 EndpointSlice 的目标 UID/IP 会改变。客户端持有的长连接可能还指向旧连接目标,甚至在短暂传播窗口内仍得到一次旧 Pod 的响应;第二轮应明确关闭旧连接、重新发起请求,并再次读取 EndpointSlice 与响应的身份。只有这两处和新容器 ID 及新 PID 对得上,才能把第二轮响应算作“新 Pod 恢复服务”。旧进程仍在 graceful termination 阶段时尤其要保存同一秒内的两个 UID 和请求结束时刻,不能只截最后一张端点列表来反推此前请求实际去了哪一个后端。29 的 Pod IP 与 Service给出了更窄的网络验证,37 要把这段放回制品和数据链条。

同一卷的证据要跨两代 Pod 连续

应用的 --state-dir 必须落在显式 volumeMount 下。部署前保存 PVC UID、PV UID 与 CSI driver/volumeHandle;之后在容器内核对挂载点和运行用户 UID/GID,并在节点对应 Pod UID 的挂载记录中查这次使用的后端。写入一段固定、非敏感的短文本时,保存 POST 的请求字节与响应状态,再 GET 读回,确认并非仅执行了 HTTP handler。仅看到 volume 名同为 state 没用:每次创建的新空 PVC 可以叫同一个名字,mountPath 也可能在 bad 配置中消失而落回容器可写层。若采用36 的交付清单作输入,观察的不变项应是 PVC UID/PV handle,而不是重建前后 Pod UID。

新空卷和已有卷要分开。镜像在切换到 65532 前已经准备好了状态目录的所有权,但新的 CSI 卷挂载在此处时,镜像层目录可能被覆盖。fsGroup 是否生效取决于所选 CSI 驱动及配置,必须在容器里验证实际文件系统属主与可写权限;第一次写失败不能通过把进程改成 root 代替原实验。复用已有 PVC 时先记录其内容和权限,不能把里面恰好已有的合成字符串算本次成功写入。只读卷是独立负例,若写入竟成功,首先检查路径是否落在另一个可写层;不能把只读请求的 YAML 属性当成操作系统已经拒绝的证据。新卷写入、已有卷读回、只读拒绝各有自己的观测和状态栏。

用相同 Deployment 的控制器删除这一次自建 Pod,等待新 Pod UID、容器 ID 和宿主 PID 都更换;不删除 PVC/PV,再从独立客户端经 Service 读取 /state。第二轮若读到原文本,还要对照同一个 PVC UID/CSI handle 和两次请求各自对应的 Pod UID,才能说本次文件在 Pod 重建后仍被同一个后端读取。若值丢失,先查是否挂错卷、恢复前曾错误删除 PVC、节点无法挂载、探针改写了 state-dir 或服务访问到了旧 Pod。若新 Pod Pending 而 PVC Bound,单看控制面绑定状态不能把后端挂载算作成功。这个实验没有模拟节点停机,也没有覆盖断电期间的未落盘写入;38 才分析恢复与数据结果。

从权限声明到内核约束有两个检查点

这条链不要求把自写探针视为不可信代码,但仍应核对实际运行身份和约束:Pod spec 的 runAsUser: 65532 是期望值,/identity 返回的 UID 是进程视角,节点上 /proc/<host-pid>/status 的 UID、能力位和所属 cgroup 才能补足宿主视角。若启用了 user namespace,还要按读取者所在层解释 uid_map 第二列,不能把 namespace 里的 65532 原样当宿主 uid。运行在 root 身份的 shell 能写入卷,并不能证明非 root 探针也能写入。反过来,应用 UID 正确也不表示能力、seccomp、LSM、只读根和设备权限都符合策略;分别保存选定 runtime 的有效配置和有界拒绝例,缺哪层就把哪层留空。

Pod 可以设置 allowPrivilegeEscalation: false、drop capabilities 和 RuntimeDefault seccomp,但这些字段的生效要在所选节点版本与运行时实现中核对。教学用的 /state 写入是最小权限正例:应用需要目标目录可写,不应因此给整个容器特权模式或任意宿主路径写权限。如果 UID 65532 失败,可以先在新卷/已有卷/只读卷三种条件下核对挂载、所有权与访问拒绝;为了让实验通过而临时取消 seccomp 或更改节点全局防护,会把题目从“按最小权限交付”换成另一个系统。若缺可委派的 cgroup v2,资源边界也只能引用 06–07 的文档和静态配置,不借本机普通进程负载声称 Pod CPU/内存限制已生效。

镜像来源与运行权限不能互相抵消。仓库中确有 sha256 manifest,不能证明当前节点已拉取那份制品;拉到签名正确镜像,不能证明它没有因挂载错误读取别人的数据;服务返回合成状态,也不能证明长期进程可在资源压迫下持续服务。本篇把这三类证明放进一份验收矩阵:分发身份、运行时身份、安全/存储路径,每一格都给原始命令、版本和结果。如果准入策略在本次环境不存在,就报告“没有准入验证”而非把本地 cosign verify 文本充当节点放行事件。任何超出可信探针的外部程序都不得用本篇教学配置直接处理。

负例要使第一个断点可辨认

第一类只改应用监听地址:新版本以 127.0.0.1 绑定时,在同一 Pod 内请求可能正常,经 Service 的客户端请求却失败。先确认新 Pod 的镜像、Pod IP 和进程都属于这次变更,抓两侧的 HTTP 与 EndpointSlice,避免“客户端失败”被错误归因为镜像或 CSI。不能在配置坏例里同时切换镜像 digest、PVC 或 NetworkPolicy;否则一条失败请求对应多个可能原因。若从客户端仍能访问,先检查是否误命中旧 Pod、旧连接未关闭或变更未触发新 Pod,而不是伪造一个理论上应出现的拒绝码。实际负例必须在独占测试集群中创建、删除自己的对象。

第二类只改状态目录挂载,保持镜像 digest、监听、Service 和客户端不变。若探针镜像里的 /tmp/containers-state 本身可写,第一次 POST/GET 即使返回 200,也可能只写进这代容器自己的可写层。检查 mountinfo 与该路径的设备/挂载源,在删除此 Pod 后用新 UID 的 Pod 再读;若内容消失,才可讨论“未持久化”,还必须排除进入了别的卷或请求路由到了旧 Pod。这一坏例与只读卷的写入拒绝不是同一回事:一个可能成功但不跨重建,另一个应在写入时失败。即使这两种结果都满足预期,也不能宣称能在节点丢盘、网络分区或数据库半写事务中恢复数据。

第三类是观测失败而非故障注入:在 Pod 启动后先后取了两份 kubectl get,中间容器可能重启,旧容器 ID、后来读到的 PID 和当前 HTTP 200 互不对应。为了发现这一种假阳性,强制在请求前后各采一次 Pod UID/containerID、宿主 PID 的启动 tick,并核对请求日志的时间与 Pod restartCount。如果两次身份发生变化,就重新采集链条,不把零散字段拼成一次验证通过。该反例不需要破坏任何网络或存储,但能暴露“只有静态 YAML 和最后一次响应”这种验收设计的漏洞。已有 00 的普通进程 PID 实验可以解释风险,却无法代替节点上的这次实际交叉核对。

原始记录如何成为可反驳的结论

实验记录应从“这一条具体请求”出发。保存请求发起与结束的 UTC、客户端身份、目标 URI、实际 HTTP 状态和未经整理的响应体;探针日志里若有同一请求的处理记录,再以服务端 Pod UID、containerID、宿主 PID/启动 tick 关联到节点。在新 Pod 首次回复之前,可先从 API 保存 Deployment 的 revision、Pod spec 与 status、EndpointSlice 和 PVC/PV;运行后记录容器内 /identity、/proc/self/mountinfo、node /proc/<pid>/cgroup 和 runtime 状态。所有命令附执行位置:客户端 Pod、服务 Pod、实际调度节点或操作者 VM。即使两个字段名字都叫 PID,脱离采集位置也没有可比性。

每项证据还要有产生者和质量标记。规范规定 OCI image 的 digest 如何指向字节,说明字段格式;Kubernetes API 给出对象在读取时的状态,说明控制面认为谁应该运行;运行时节点查出的 PID/挂载与应用 HTTP 是实际观察;单纯模拟脚本的返回只能证明采集程序可调用。若缺 CSI 驱动,把卷写读列为 NOT_RUN;若只有 service 请求、没有同一节点 CRI 记录,就将“宿主 PID 归属”列为 NOT_RUN,不把整个实验一次性写成全绿。来源版本同时包含 OCI v1.1.1 规范、实际 runtime/CRI 插件、CNI/CSI 的安装版本和 kubectl/节点架构;网页上的固定 tag 不能替现场版本。原始 stderr、非零退出码和超时次数一样保存,防止成功截图抹掉中间失败。

要判断一个结果是不是异常,需要在实验开始前写明不变量,而不是运行后挑选有利于自己的字段。本篇至少要求前后两次请求分别归属同一可追溯的镜像引用;一次正例的 Pod UID/containerID/宿主 PID 在请求前后不突变;两代 Pod 有不同 UID,PVC UID 与可核的后端 volumeHandle 保持;同一合成字符串从实际服务路径写入,再从替代 Pod 服务路径读出;错误监听案例允许 Pod 运行却经 Service 不可达。任何不变量冲突先保存原始输出并停在那一环排障,不能因为其他四环已通过就降低条件。反过来,在少一项实测时也可诚实报告已被证实的较窄结论,例如“这次 Pod 的 Service /identity 可达”,而不是全链路成功。

POST 成功还要分清响应与文件内容。探针的服务端在完整读完声明的请求体后才写状态文件,残缺 body 不应被当作本次文本;客户端只见连接重置时,不能按发送方已经写出的字节数推定持久化完成。写入轮要保留请求的 Content-Length、实际发送字节数、HTTP 响应或错误,再由新连接 GET 状态确认。退出或 Pod 替换恰好发生在请求中途时,记录请求是否完成、旧 PID 的 TERM/停止时刻以及重建后的卷读值,不把一次传输失败伪装成一次已确认写入。

独立实验如何结束

仅能在独占 VM/私有 registry/独占 kind 集群操作本篇对象:先给项目、namespace、镜像 tag 与工作目录设本轮唯一前缀,确认不存在后才创建。kind 有自己的节点视角,进入节点采 CRI 和 /proc 时要记录节点名称/容器 ID;不要在共享宿主上猜 cgroup 路径后改限制、重启系统 daemon 或拦截宿主防火墙。现有共用探针的请求最大 4096 字节,状态只存合成文本;如需诊断真实网络,抓包只限本次 Pod 的 netns、自建 veth 和有界时间窗。终止时先停本轮客户端请求、保存最终 Pod/PVC/Service 与日志;删除本轮负例 Pod 或配置,不删除其他 namespace 的对象。若要验证 PVC 的生命周期,先保留原卷完成重建读回,再由操作者确认测试数据可丢弃并核对 StorageClass 的 reclaimPolicy,不能靠删除 namespace 猜测后端数据是否擦除。

在此云端缺 Docker、registry、kind 与 CSI,无法把若干互不相关的本地结果拼成一轮请求证据。即便以后专用 VM 上完成一次成功,也只覆盖固定版本、单个平台和这一个短文件:它不会自动证明用户代码对真实故障原子持久、网络策略已覆盖所有源地址、或其他镜像也能通过签名准入。下一篇38将在同一条已完成的链上分别注入资源、网络和节点故障,衡量哪些请求失败以及数据是否保留;没有 37 的稳定基线,38 的“恢复”就没有可核对的起点。

现象 沿链追溯 能否成立
HTTP 200 响应体与请求 ID、Pod UID 不保证写盘持久化
UID 与容器 ID 对不上 Pod 重建/滚动更新 切勿拼接两代日志
卷上仍有数据 mount 源、文件校验 需证明同一卷

练习

  1. 先预测同名 Pod 重建后哪些 ID 必换、哪些 digest 可以保持;逐项读取,标记能复用与会重建的对象。
  2. 先预测把 state-dir 改到容器可写层后请求仍返回 200,但 Pod 重建后的数据会怎样;记录文件、挂载源与请求输出核实。

上一篇:36:交付与回退;下一篇:38:故障与业务结果。

系列总目录:从进程隔离到运行时与编排。

参考资料