一个看板服务要给沙箱记台账,后台任务每 30 秒扫一次 pod 列表。30 秒这个数不是拍的:沙箱服务创建接口的 TTL 下限是 60 秒,采集间隔必须小于它的一半。我的判断是,轮询采集的间隔该从被观测对象的最短寿命倒推。
间隔的上限由 TTL 的下限定死
那个看板要回答「过去 30 天跑过的沙箱,按框架分,峰值内存是什么分布」。指标本身在时序库里,但它的标签只有 pod 名、节点名、namespace 这几个,没有框架,没有申请的规格。30 天里跑过 75783 个沙箱,当下活着的只有 12 个,已删除的 pod 在集群里查不到任何元数据。只能自己扫、自己存。
扫的频率定在 30 秒。沙箱服务创建接口的 TTL 字段约束是 ge=60,用户能声明的最短存活时间就是 60 秒。采集间隔一旦大于 60 秒,一个只活 60 秒的沙箱整段生命周期都可能落在两次扫描之间,一次都扫不到。取一半是留余量——扫一轮本身要花时间,list 回来几百个 pod 再落库不是瞬间完成。
漏采的代价不对称。长命沙箱有的是机会被扫到,短命的只有一两次。而峰值分析最需要的就是短命的那批:测试型负载往往只活几分钟,它们在一个窄窗口里密集创建又密集销毁,峰值就出在那儿。
这个推导只在一个前提下成立,我是后来才认真想的:60 秒是声明 TTL 的下限,不是实际寿命的下限。一个 exec 型调用跑完就删,实际可能只活十几秒,30 秒间隔对它完全无效。
单靠定时扫不够。另一条机制是 K8s 的 watch,变化的瞬间入账。watch 管实时,定时扫管对账——watch 断线期间的事件永久丢失,而且它只说「谁变了」,不说「当前全量是什么」。
两条一起跑,会出一个各自单跑都不会有的 bug。采集的顺序是先 list 拿当前全量,再把不在名单里的记录累加 miss 计数。一个沙箱如果在 list 取完快照之后才创建,watch 立刻把它入了账,但它不在这一轮的 alive 名单里。照旧累加的话,两轮就够把一个正在运行的沙箱标成已删除。修法是先记快照时刻再发 list,累加时只动 last_seen 早于快照时刻的记录。
连续 2 轮扫不到才写删除时间
一次扫不到不代表它没了。apiserver 抖动、list 超时、连接被重置,都会让某一轮少回来几个。立刻标删,活着的沙箱会在看板上消失一轮再出现一轮。
阈值定 2,一个真被删掉的沙箱若只靠定时扫发现,最多还要在看板上多挂 60 秒。看板是给人看趋势的,60 秒滞后无所谓;活着的东西闪现一下,看的人会去查是不是有 bug。
watch 收到 DELETED 事件时不走这个阈值。阈值是给「这一轮没看见」这种说不准的情况用的,apiserver 直接告诉你它删了,再等两轮只会让看板多显示一个已经不在的沙箱。
整轮 list 返回空要单独挡掉。namespace 里一个 pod 都没有,与 apiserver 异常但没抛异常、返回了个空响应,这两件事在代码里长得一模一样。照着累加,一轮就能把所有在跑的沙箱往删除判定上推一格。空快照直接跳过对账。这条是推演出来的,我没有在真集群上复现过空响应。
删除时间取最后一次确认存在的时刻
达到阈值那一刻写 deleted_at,值取 last_seen,不取 now()。
差别是固定的 60 秒。pod 在第 N 轮之后消失,第 N+1 轮和第 N+2 轮都扫不到,第 N+2 轮才判定。取 now() 的话,每条记录的寿命都多出「阈值 × 采集间隔」,这里是 2 × 30 秒。这个偏差对每条记录都朝同一个方向,而且阈值一改它就跟着变——把阈值调成 3,所有记录的寿命凭空长 30 秒。
last_seen 也不精确。真正的消失时刻落在 last_seen 和 last_seen + 30 秒 之间,它系统性地少算,平均少 15 秒。选它是因为这 30 秒不随参数变,而 now() 多出来的 60 秒会跟着阈值漂。
watch 那条路反过来,取 now()。收到 DELETED 事件的此刻就是它消失的时刻,last_seen 反而是旧的。两条路取不同的值,判据是同一条:取你真正知道的那个时刻。
pod 一删,最准的那条判据就没了
活着的 pod 看 ownerReferences 里的控制器 kind。沙箱由 CRD 控制器创建,owner 一定在,这是控制器写进去的事实。
历史数据没这个可看,只能看 pod 名的形态是不是 <uuid>-<序号>。pod 已经删了,时序库里只剩一个名字,owner 和标签都不在。
两条都要。准的那条只在 pod 还活着的时候存在,而台账要回答的问题,多半是关于已经不在的东西。
名字这条准不准,实测过:那个 namespace 里 12 个沙箱 12/12 命中,198 个非沙箱 pod 一个没误判。别的服务用的是 chat-<雪花id>、<deploy>-<hash>-<hash> 这类形态,与标准 uuid 的 8-4-4-4-12 十六进制不会撞。
只用名字判据出过事。排查挂载问题时,我把另一个服务的一批 pod 当成了自己的——那批 ownerReferences 为空,光靠 namespace 和肉眼看名字区分不出来。pod 还活着的时候有准的字段可用,就不该退回去猜。
12/198 是一个 namespace、一个时间点的结果,对将来不构成保证。别的服务改个命名约定就可能撞上来,而撞上来的表现是看板上多了几个不属于自己的 pod,不会报错。我没有为这条加监控。
两侧的正则必须一字不差。同一份 pattern,一份在 Python 侧过滤,一份拼进 SQL 交给时序库。Python 的 \d 认 Unicode 数字,RE2 只认 ASCII。序号段写 [0-9]+ 而不写 \d+,否则同一个 pod 名会在一处算沙箱、另一处不算。