应对慢计划的四个旋钮及其代价
一句话总结
缩短计划时间的旋钮有四个——关闭刷新、并行度、缩小目标范围、拆分状态。四个都要付出某种代价,所以要先测量,再选择可以损失的东西。
为什么这是个问题——缓慢的计划是安全问题
一旦 plan 开始要花 3 分钟,人们的行为就会改变。改了一行,不想等 3 分钟,就想着“这是显而易见的变更”,直接敲 apply。这个习惯一旦养成,“应用前先看会改变什么”这条纪律本身就消失了。事故发生在那之后。
所以计划性能不是便利问题,而是能否坚持这条纪律的问题。不过旋钮不能随便拧。必须清楚每个旋钮究竟放弃了什么。
工作原理
一次计划大体上做三件事。读取配置并构造图(很快);逐个确认状态中记录的内容在实际中是否仍然保持原样(通常大部分时间花在这里);再把结果与配置比较,生成变更列表(很快)。在大型状态下之所以慢,几乎总是因为中间这一步。
1. 关闭刷新。整个跳过中间这一步。缩短得最多,但看不到外部发生的变化。即使有人在控制台上手动修改过,计划也会说“没有变更”。只有在需要紧急回退,或者刚刚亲手应用过、状态确定是最新的情况下,才值得使用。
2. 并行度。决定同时进行的操作数量。计划的内容完全不变,只有时间不同。调高是否总是更快呢?并不是——如果对方的 API 设置了速率限制,重试会增加,反而变慢。有时也需要调低(限制严格的 API、共用账号)。
3. 缩小目标范围。只把指定的地址以及它所依赖的内容放进计划。速度确实更快,但这个计划不能代表整个配置。工具每次制定计划时也会给出警告。OpenTofu 文档写明,只应在从失误中恢复或绕开工具局限这类例外情况下使用这个选项。如果在日常工作中用到了它,那说明的不是性能问题,而是设计问题。
4. 拆分状态。这是根本的解决办法。把每天变化的层和几乎不变的层放在不同的状态中,每个人只需要等待自己的那一层。总时间之和反而可能增加,但一个人的等待时间会减少,附带地,锁竞争和事故影响范围也会缩小。代价是各层之间必须用输出连接,这种连接就成了契约。
再加上保存的计划(-out)。在 CI 中把计划和应用分开运行时会用到它,有一条规则:保存的计划以当时的那个状态为前提。如果期间状态发生了变化,应用就会被拒绝。看起来不方便,但这恰恰是保证“评审过的那个计划才会被应用”的机制。
在现场相遇的样子
最常见的错误是不测量就选择。从“太慢了,把 refresh 关掉吧”开始,半年之后漂移不断累积,却没有人知道。测一测,原因通常更具体——某个数据源每次列出几千条,或者某个模块占了状态的一半。
第二种是只测一次就下结论。第一次运行时缓存是空的,所以很慢。需要养成在相同条件下测两次、取第二次数字的习惯。
第三种是只累积测量记录。同一个标签堆了好几行,以后就无法知道哪一行是最新的。记录也要做到幂等——同一个标签要覆盖写入。
此外还有一个实际工作中经常被忽视的事实:时间因机器而异,但计划的内容不会不同。把保存的计划用 JSON 打开,统计变更项的数量,无论在哪台机器上运行,都会得到相同的数字。所以“缩小目标范围,计划就只剩那么多”“关闭刷新就看不到漂移”这类论断,应该用这个数字而不是时间来证明。
第四,如果有一条标准能分清哪里是性能问题,从哪里开始是设计问题,沟通就会容易得多。拧一个旋钮就变得可以忍受,那是性能问题。如果日常工作必须靠缩小目标范围才能进行,那已经是设计问题,答案不是选项,而是拆分状态。如果不做这个区分,团队就会把权宜之计固化成标准流程。
最后,不要忘记测量本身的成本。把计划测十次,就要花十次的时间。所以在实际工作中,只测量并记录修改前后的一对。记录里不光留下数字,还要把当时使用的命令完整留下——半年后会有人问“这个数字是在什么条件下得出的”,没有命令就回答不了这个问题,只能重新测一遍。
下一项实验要做什么
在 /root/tfa-perf 中创建包含 300 个实例的状态,先做好测量脚本。然后在工具之外删除一个文件制造漂移,观察关闭刷新与开启刷新时计划所包含的变更数有什么不同,测量并行度和缩小目标范围,再把状态拆成两份重新测量。最后确认保存的计划会因状态变化而被拒绝,并把测得的数字整理成表格和结论。