三个集群同时出了问题,症状却完全不像。
第一个是训练集群。它在几千个节点的规模上待了几个月,一次新的训练任务落地,节点数直接冲到一万五千。Kube-apiserver 开始吃内存,Etcd 开始报警。
第二个集群从来没超过几千个节点,日子却难过得多。这是一个 agent 平台,它的对象是"活"的:会话被创建、挂起、恢复、销毁,每分钟发生数千次,每一次状态转换都是一次写入。
第三个集群既不大也不快:一个平台集群,装了两百个 operator,数万个对象,大部分时间就静静待着,而每一个 apiserver 都持有它们的完整副本。
前两个是 AI 负载,第三个不是——这恰恰是最不重要的一点。它们是在三个不同的方向上撞到了天花板,修好其中一个,对另外两个毫无帮助。
去找现成的指导,能找到的东西不多。把集群做到 5000 节点的建议满地都是,再往上,或者往旁边走,互联网就摊手了。
于是你加 apiserver。这是 kOps 里有文档的选项,也是大多数人会给出的标准答案。它会让问题变得更糟,因为你每加一个 apiserver,就复制了一份完整的 watch 缓存。你买到了请求处理能力,付出的代价是你本来就不够用的内存。
这些都不是研究项目。远超 5000 节点的集群今天就在生产环境里跑着,用的是原版上游组件。没有人靠一个魔改分支走到那一步,他们靠的是对"每个存储该放什么"想得很清楚。
规模不是难点,缺的是装配说明书。
三种规模,三条不同的轴
问一个控制面需要多大,答案通常以节点数返回。第二个和第三个集群就是这样被按一个它们并不存在的问题去调优的。
节点绑定型。训练和强化学习集群:规模大、同构,压力来自每一个 kubelet 的定期上报。在 10 万节点、10 秒心跳的条件下,仅存活检测一项就产生大约每秒 1 万次写入;在实际测量中,租约续期约占写入 QPS 的 70%。这个控制面做的大部分事情,就是节点永远在礼貌地说"我还在"。
变更绑定型。agent 平台:规模不大,但一天里极其密集。把大量 agent 会话打包到较小的 worker pod 池上的系统,会把每个 actor 的生命周期表达成自定义资源,于是最忙、最短命的那些对象是 CRD,写入速率和你拥有多少台机器毫无关系。
状态绑定型。平台集群:种类多、对象多,大部分在睡觉。成本不在写入,而在常驻字节数乘以每一个持有它们的 apiserver。这是"加 apiserver"这个条件反射真正造成伤害的那条轴。
三条轴彼此独立:你可以把任意一条拉满,另外两条趴在地上。一份只认节点数的配方,会兴高采烈地调错东西。
接缝与配方
接缝是你可以把 Kubernetes 拆开的地方:两个组件之间的一份契约,通常位于网络边界,并且是可替换的,所以你能换掉它背后的东西,而不用打补丁改上游。
把它想成裁缝眼里的衣服接缝:腰上收一点,肩上放一点,还是同一件衣服。在不是接缝的地方下剪刀,你得到的是一块很贵的抹布。
配方是围绕这些接缝构建的、经过规模验证的配置:可执行的配置,要么通过规模测试,要么失败,CI 会在它们不再成立的那天告诉你。
四条接缝。第一条是存储契约,决定哪种存储持有哪类资源,缓解变更压力。不是所有集群状态都值得同样的持久性保证:事件和节点租约是短暂的,leader 选举租约既短暂又话多,它们都不需要和你的 Deployment 一样走持久化、Raft 复制的存储。KEP-4743 定义了存储接口边界,而 --etcd-servers-overrides 今天就能把资源类型路由到不同后端。etcd 不是问题所在,这些也不构成替换它的理由;一个通用的一致性存储,不该在没有业务持久化需求时吸收这些流量。缺口在于:--etcd-servers-overrides 只认识内置资源,所以一个变更绑定型的平台无法把制造写入风暴的那些自定义资源路由出去。
第二条是节点存活契约,决定存活信号如何抵达控制面,缓解节点数压力。两个修法可以叠加:吸收它,在控制面前面加一层面向节点的层级,处理续期,只向上传播真正的状态转换;让它更便宜,把续期走 etcd 的内存租约刷新,而不是一次完整的 Raft 提交。后者落在 etcd 本身,所以对每个集群都有好处,小到三节点的集群也一样。
第三条是分片轴,即控制器、缓存和存储共同约定用来分片的那个字段,三条轴都能缓解。水平扩展意味着每一层都要对"对象怎么切分"达成一致。选错轴,你的控制器就会一辈子去够别人分片里的对象。KEP-5866 引入了 API 流量的哈希范围过滤,这是基础;协调、重分片、以及控制器管理器的分片都不在范围内,这三件都还需要做。
第四条是缓存驻留,即 apiserver 实际在内存里保留什么,缓解状态压力。还记得那个复制了完整 watch 缓存的 apiserver 吗?就是这条接缝,也是状态绑定型集群的全部问题。一个只和调度器打交道的 apiserver 不需要每个对象都常驻,一个双分片原型让每个分片的堆内存下降了大约一半。这里有个下限:把所有可关闭的缓存都关掉,一个 apiserver 每个 RBAC 对象仍然要背大约 0.9 KB,因为授权器的 informer 无论如何都持有它们。缺口在于:驻留控制在上游还不存在,KEP-5866 过滤的是出口,缓存依然存下所有东西。
配方阶梯上,最后那一级是故意留着的:把没人造过的那一级公布出来,它才有用。更大的缺口在索引本身——你按节点数选层级,而节点数是三条轴里唯一有测量数据支撑的那条。没有任何人,包括我们,发布过针对变更绑定型或状态绑定型集群的、经过规模验证的配方。
这些都不是发行版:原版上游二进制,以经过 CI 测试的配置形式交付,这样一个改变结论的版本会先以测试失败的形式出现。
可以推的地方有三处。按 CRD 配置存储,对应第一条接缝,目前还没有 KEP,好让用例来塑造它——如果你的热点对象是自定义资源,把写入速率带到 SIG API Machinery 去。KEP-5866 和驻留扩展,对应第三、第四条接缝,原型分支已在评审中,设计尚未定案。还有没人发布过的配方:两万节点以上、严重的变更压力、或者一个单纯很重的集群,你知道一些别人不知道的东西。
训练集群、agent 平台,还有那个不起眼的平台集群,正在三个不同的方向上撞到天花板,而三者都应该靠原版上游组件走到那里。大部分零件已经摆在台面上了。
热门跟贴