一个沙箱的容器退出了,它的 Pod IP 几秒内被同一台机器上的新沙箱拿走,发给死沙箱的命令和文件请求全部返回 200,执行在别人的容器里。我的判断是:转发前确认「目标还活着」不够,要确认「还是原来那个」。
一个沙箱读到了另一个沙箱的文件
调用方报上来的是「不同沙箱的文件互相干扰」:这个沙箱里读出来的 pi.txt,和另一个沙箱里那份逐字节一致。
第一反应是文件系统共享,查下来不是。是请求转到了别人那里。
当场做了一次验证:对一个已经死掉的沙箱执行 hostname,返回的是另一个活沙箱的 Pod 名。目标 id 是死的那个,命令跑在活的那个里面,HTTP 状态码 200。
17:24:05 建出沙箱 A,17:24:44 它的容器退出。之后约 35 秒里发给 A 的探活请求都是 502,那个 IP 上没人应答。17:25:19 沙箱 B 在同一台机器上拿到了这个 IP,1 秒后 A 的探活开始返回 200,接下来发给 A 的命令和文件上传也全是 200——都在 B 的容器里执行,B 的输出里能看到两个调用方的命令混在一起。
同一时刻扫了一遍生产集群:22 个沙箱对象的 Pod 已经是 Failed 或 Succeeded,其中 12 个的 IP 已经被别的运行中的 Pod 持有。
引擎按注解里记下的 IP 转发
要出这件事,三个条件得同时成立。
一是沙箱会死。一批镜像的启动命令是 tini -- 包着 bash,bash 没有终端立刻退出,容器跟着结束。这样刚建出来就退出的沙箱,近 24 小时有 137 个,往前两周每天 50 到 131 个。死因不止这一种,当时另有 12 个是被 OOM 杀掉的(exit 137),处在同一个状态。
二是引擎不知道它死了。建沙箱的开源引擎 OpenSandbox 把分到的 Pod IP 写进沙箱对象的注解,转发时读这个注解取 IP,不看 Pod 当前阶段。Pod 死了,注解不变,沙箱对象也不清,对外查状态还答 Allocated。
三是 IP 很快换主。集群的 Calico 回收 IP 之后马上分给下一个调度到这台机器的 Pod。每小时建删约 800 个沙箱的节奏下,这个间隔是几秒。
三条凑到一起,发给死沙箱的请求就打到了新主人身上,而且不会报错:新主人是个正常工作的 execd,收到什么执行什么。
上游后来的版本我也读了,取端点那段仍然是从注解里读出 IP 直接拼地址;另一种工作负载类型读的是对象状态里的 podIPs,同样不看 Pod 阶段。升级不会让这件事自己消失。
活着不等于还是原来那个
先加了一道存活检查:转发前问一次引擎这个沙箱是不是 Running,不是就回 409,答案缓存 3 秒。线上再没出现转错容器的情况。
但这道检查问的是沙箱 id 的状态,转发真正用的是 IP,中间隔着一次 IP 易主,两头都不通知我。所以它证明的事情比需要的少一档:沙箱 id 是 Running,不等于那个 IP 还归它;3 秒缓存窗口里死掉并被复用的,照样放过去。
引擎自己不会按身份转发,它按记下的 IP 转。所以核身份这件事得做在它转发之前:确认它要转去的那个地址,现在还是我以为的那个 Pod。三样一起核——Pod 名、Pod 的 uid(Kubernetes 给每个对象的一次性编号,同名重建也会换一个新的)、这个 Pod 当前的 IP。
缺一样都能被骗过去。只核 Pod 名,控制器补回来的同名 Pod 骗得过;只核 IP,复用这个 IP 的任何 Pod 都骗得过;名字和 uid 都核、唯独不核 IP,就回到了「沙箱活着、IP 已经不是它的」这条路上。
原型上实测的是同名重建这种:绕过服务直接删掉一个沙箱的 Pod,下一次转发 0.012 秒返回 409;控制器随后补回的同名新 Pod(名字一样、uid 换了)没有被转发到。
IP 被别的 Pod 拿走那种没实测,它要靠 IP 分配撞车才造得出来,我没有刻意造。uid 这一项应该挡得住——新主人的 uid 一定不是原来那个。这是从机制上推出来的,不是测出来的。
没人要的防护
收集需求的群里,没有人提过「转发前核对目标身份」。按「群里没人要就不做」的口径,这条该被划掉,而我把它留下了。
没人要的功能不做,因为它的价值只存在于设想里;没人要的防护做不做,看它挡的那件事发没发生过。这件事发生过一次,而且调用方当时报的是「文件互相干扰」——他们看不见转发这一层,只能看见结果不对。换个死因再出一次,我拿到的仍然会是这样一条描述。
代价有两处。一是窗口没关死:核对用的是本地缓存的 Pod 信息,缓存会滞后,节点快满的时候 IP 几秒内就易主,这道核对收窄窗口,不消灭它。二是缓存滞后时会误杀,把活着的沙箱判成「不是原来那个」。
误杀的结果是 409,调用方重试一次;放过的结果是命令跑进别人的容器。两种错不对称,所以往严的一边偏。