本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

服务器端应用很棒,你应该使用它

服务器端应用(Server-side apply)(SSA)现已正式发布(GA)了几个版本,我发现自己在许多对话中,都在建议不同情况下的个人/团队使用它。所以,我想在此写下其中的一些原因。

SSA 的显而易见(和不那么明显)的好处

从各种方式切换到服务器端应用,你会获得一系列改进/便利!

  • 与客户端应用(即普通的 kubectl apply)相比
    • 当你意外地与另一个参与者争夺某个字段的值时,系统会提示你存在冲突!
    • --dry-run 结合使用时,不会有意外运行客户端试运行(dry run)而非服务器端试运行的风险。
  • 与手动编写补丁(patch)相比
    • SSA 补丁格式编写起来非常自然,没有奇怪的语法。它就是一个普通的对象,但你可以(并且应该)省略任何你不关心的字段。
    • 旧的补丁格式(“战略性合并补丁”)是临时性的,并且仍然存在一些错误;JSON-patch 和 JSON merge-patch 无法处理 Kubernetes API 中一些常见的情况,即包含应根据“名称”或其他标识字段进行递归合并的项目的列表。
    • 现在还有很棒的 Go 语言库支持,可以用来以编程方式构建 apply 调用!
    • 你可以使用 SSA 将字段设置为 null 来明确删除那些你并不“拥有”的字段,这使其成为所有旧补丁格式的功能完备的替代品。
  • 与外部调用 kubectl 相比
    • 你可以从任何语言中使用 apply API 调用,而无需外部调用 kubectl!
    • 如上所述,Go 库现在有专门的机制来简化这一过程。
  • 与 GET-modify-PUT 模式相比
    • (这一部分比较复杂,如果你从未编写过控制器,可以跳过!)
    • 要正确使用 GET-modify-PUT,你必须处理并重试写入失败的情况,即在你的 GET 和 PUT 操作之间,有其他人修改了该对象。这种情况发生时,被称为“乐观并发失败”。
    • SSA 将这个任务交给了服务器——你只需在有冲突时重试,而且你可能遇到的冲突都是有意义的,比如当你实际上试图从系统中的另一个参与者那里夺取一个字段时。
    • 换句话说,如果 10 个参与者同时进行 GET-modify-PUT 循环,其中 9 个会遇到乐观并发失败并需要重试,然后是 8 个,以此类推,最坏情况下总共可能有多达 50 次 GET-PUT 尝试(即 N 个参与者同时进行更改时,有 0.5N^2 次 GET 和 PUT 调用)。如果参与者改用 SSA,并且这些更改实际上并没有在特定字段上发生冲突,那么所有的更改都可以按任何顺序进行。此外,SSA 的更改通常可以在完全没有 GET 调用的情况下完成。对于 N 个参与者来说,这只需要 N 个 apply 请求,这是一个巨大的改进!

我该如何使用 SSA?

用户

使用 kubectl apply --server-side!我们(SIG API Machinery)希望很快能将此设为默认值,并完全移除“客户端”应用!

控制器作者

这里主要分为两类,但对于这两类,在使用 SSA 时,你可能都应该选择 force conflicts(强制解决冲突)。这是因为你的控制器可能不知道当系统中某个其他实体对某个特定字段有不同期望时该怎么做。(不过,请参阅 CI/CD 系统部分!)

使用 GET-modify-PUT 序列或 PATCH 的控制器

这类控制器会 GET 一个对象(可能来自 watch),对其进行修改,然后 PUT 回去以写入其更改。有时它会构建一个自定义的 PATCH,但语义是相同的。大多数现有的控制器(尤其是树内控制器)都像这样工作。

如果你的控制器是完美的,那很好!你不需要改变它。但如果你确实想改变它,你可以利用新的客户端库的 extract 工作流——即 get 现有对象,提取你现有的期望,进行修改,然后重新 apply。对于许多之前在计算最小 API 更改的控制器来说,这将是对现有实现的一个小更新。

这个工作流避免了意外尝试拥有对象中每个字段的失败模式,这种情况发生在你只是 GET 对象、进行更改、然后 apply 时。(请注意,服务器会注意到你这样做了,并拒绝你的更改!)

重构型控制器

在 SSA 出现之前,这类控制器基本上是不可能实现的。其思想是(每当有东西变化等时)从头开始重构控制器希望对象具有的字段,然后将更改 apply 到服务器,让服务器来计算结果。我现在建议新的控制器都以这种方式开始——说出你希望一个对象是什么样子,比说出你希望它如何变化要不那么繁琐。

客户端库默认支持这种操作方法。

唯一的缺点是,你可能最终会向 API 服务器发送不必要的 apply 请求,即使对象实际上已经符合你控制器的期望。如果这种情况偶尔发生,问题不大,但对于吞吐量极高的控制器,这可能会给集群——特别是 API 服务器——带来性能问题。无操作的写入不会被写入存储(etcd)或广播给任何观察者,所以这其实不是什么大问题。如果你仍然担心这个问题,今天你可以使用上一节解释的方法,或者你也可以暂时继续这样做,并等待一个额外的客户端机制来抑制零更改的 apply。

为了解决这个缺点,为什么不先 GET 对象,只有在对象需要时才发送你的 apply 请求呢?令人惊讶的是,这帮助不大——一个无操作的 apply 对 API 服务器来说,工作量并不比一个额外的 GET 多多少;而一个会产生变化的 apply 比那个 apply 加上一个前置的 GET 更便宜。更糟的是,由于它是一个分布式系统,你的 GET 和 apply 之间可能会有东西发生变化,从而使你的计算失效。相反,你可以在从缓存中检索到的对象上使用这种优化——这样它确实会减少系统负载(代价是当需要更改且缓存稍微落后时会有延迟)。

CI/CD 系统

持续集成(CI)和/或持续部署(CD)系统是一种特殊的控制器,它做的事情类似于从源代码控制(如 Git 仓库)中读取清单,并自动将它们推送到集群中。也许 CI/CD 过程首先从模板生成清单,然后运行一些测试,接着部署一个更改。通常,是用户将更改推送到源代码控制中,尽管并非总是如此。

有些这样的系统会持续与集群进行协调,其他的可能只在有更改推送到源代码控制系统时才操作。以下考虑对两者都很重要,但对持续协调的类型更为重要。

CI/CD 系统实际上就是控制器,但就 apply 而言,它们更像是用户,并且与其他控制器不同,它们需要注意冲突。原因如下:

  • 抽象地说,CI/CD 系统可以改变任何东西,这意味着它们可能与任何控制器发生冲突。建议控制器强制解决冲突的假设是,控制器只改变有限数量的东西,你可以合理地确定它们不会与其他控制器在这些东西上发生冲突;对于 CI/CD 控制器来说,情况显然并非如此。
  • 具体例子:想象一下 CI/CD 系统希望某个 Deployment 的 .spec.replicas 为 3,因为这是源代码中检查的值;然而,还有一个 HorizontalPodAutoscaler(HPA)也针对同一个 Deployment。HPA 计算一个目标规模,并决定应该有 10 个副本。哪个应该获胜?我刚才说过,大多数控制器——包括 HPA——应该忽略冲突。HPA 不知道它是否被错误地启用了,而且 HPA 也没有方便的方式来通知用户错误。
  • CI/CD 系统遇到冲突的另一个常见原因可能是,它试图覆盖由系统管理员/SRE/值班开发人员放置的紧急修复(手动补丁)。你几乎肯定不希望自动覆盖那个修复。
  • 当然,有时 SRE 会做出意外的更改,或者开发人员做出未经授权的更改——这些你确实希望注意到并覆盖;然而,CI/CD 系统无法区分这两种情况。

希望这能说服你,CI/CD 系统需要错误路径——一种将这些冲突错误反馈给人类的方式;事实上,它们应该已经有了这个,持续集成系统肯定需要某种方式来报告测试失败。但也许我也可以说一些关于人类如何处理错误的事情:

  • 拒绝紧急修复:CI/CD 系统的(人类)管理员观察到错误,并手动强制应用(force-apply)有问题的清单。然后 CI/CD 系统将能够成功应用该清单,并成为共同所有者。

    可选:然后管理员应用一个空白清单(只有对象类型/命名空间/名称)来放弃他们成为管理者的任何字段。如果省略这一步,管理员有可能最终拥有某些字段,并在未来造成不必要的冲突。

    注意:为什么是管理员?我假设通常推送到 CI/CD 系统和/或其源代码控制系统的开发人员可能没有直接推送到集群的权限。

  • 接受紧急修复:相关更改的作者看到冲突,并编辑他们的更改以接受生产环境中运行的值。

  • 先接受后拒绝:如同接受选项一样,但在该清单被应用,且 CI/CD 队列再次拥有一切(因此没有冲突)之后,重新应用原始清单。

  • 我也可以想象 CI/CD 系统允许你以某种方式将清单标记为“强制解决冲突”——如果有这种需求,我们可以考虑制作一种更标准化的方式来做到这一点。一个严谨的版本,让你能准确声明你打算强制解决哪些冲突,将需要 API 服务器的支持;在此之前,你可以制作一个只包含那部分字段的第二个清单。

  • 未来工作:我们可以想象一个特别先进的 CI/CD 系统,它可以解析 metadata.managedFields 数据,以查看它们与谁或什么东西在哪些字段上发生冲突,并决定是否忽略冲突。事实上,这些信息也呈现在任何冲突错误中,尽管可能不是一种易于机器解析的格式。我们(SIG API Machinery)大多没有预料到人们会想要采取这种方法——所以我们很想知道,人们是否实际上想要/需要这种方法所暗示的功能,例如在 apply 时请求覆盖某些冲突而不是其他冲突的能力。

    如果这听起来像是你想要为自己的控制器采取的方法,请来和 SIG API Machinery 谈谈!

祝你 apply 愉快!