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

服务端应用是很好的,你应该使用它

服务器端应用 (SSA) 现在已经在几个版本中 GA 了,我发现自己经常在很多对话中推荐人们/团队在各种情况下使用它。所以我想写下其中的一些原因。

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

从各种东西切换到服务器端应用所获得的一系列改进/优点!

  • 与客户端应用(即普通的kubectl apply)相比
    • 当你意外地与另一个参与者争夺字段的值时,系统会给你冲突!
    • 当与--dry-run结合使用时,不会有意外运行客户端干运行而不是服务器端干运行的风险。
  • 与手动滚动补丁相比
    • SSA 补丁格式编写起来非常自然,没有奇怪的语法。它只是一个常规的对象,但你可以(并且应该)省略任何你不在意的字段。
    • 旧的补丁格式(“战略合并补丁”)是临时的,仍然存在一些错误;JSON 补丁和 JSON 合并补丁无法处理 Kubernetes API 中常见的一些情况,即应该基于“名称”或其他标识字段递归合并的列表。
    • 现在还有很好的go 语言库支持,可以以编程方式构建 apply 调用!
    • 你可以使用 SSA 通过将字段设置为 null 来显式删除你不“拥有”的字段,这使其成为所有旧补丁格式的完整替代方案。
  • 与调用 kubectl 相比
  • 与 GET-modify-PUT 相比
    • (这个比较复杂,如果你从未编写过控制器,可以跳过它!)
    • 要正确使用 GET-modify-PUT,你必须处理并重试写入失败,以防其他人以任何方式在你执行 GET 和 PUT 之间修改了对象。当发生这种情况时,这是一个“乐观并发失败”。
    • SSA 将此任务卸载到服务器 - 只有在发生冲突时才必须重试,而你可以获得的冲突都是有意义的,例如当你实际上试图从系统中的另一个参与者那里夺取一个字段时。
    • 换句话说,如果 10 个参与者同时执行 GET-modify-PUT 循环,9 个将获得乐观并发失败,必须重试,然后是 8 个,依此类推,在最坏的情况下最多进行 50 次总 GET-PUT 尝试(对于 N 个同时进行更改的参与者,这是 .5N^2 次 GET 和 PUT 调用)。如果参与者改用 SSA,并且更改实际上不会在特定字段上发生冲突,那么所有更改都可以按任意顺序进行。此外,SSA 更改通常可以在没有 GET 调用的情况下完成。对于 N 个参与者,这只有 N 个 apply 请求,这是一个巨大的改进!

如何使用 SSA?

用户

使用kubectl apply --server-side!很快,我们(SIG API Machinery)希望使其成为默认设置并完全删除“客户端”应用!

控制器作者

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

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

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

如果你的控制器是完美的,那太好了!你不需要更改它。但是,如果你想更改它,你可以利用新的客户端库的提取工作流程 - 即,get 现有对象,提取你现有的期望,进行修改,然后重新 apply。对于许多正在计算尽可能小的 API 更改的控制器,这将是对现有实现的少量更新。

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

重建控制器

这种控制器在 SSA 之前实际上是不可能的。这里的想法是(每当发生更改等时)从头开始重建对象的字段,因为控制器希望它们是这样的,然后将更改 apply 到服务器,让它弄清楚结果。我现在建议新的控制器从这种方式开始——说出你希望对象看起来的样子,比说出你希望它如何更改要容易得多。

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

唯一的缺点是,你可能会最终向 API 服务器发送不需要的 apply 请求,即使实际上该对象已经与你的控制器的期望匹配。如果偶尔发生这种情况,则无关紧要,但对于吞吐量极高的控制器,这可能会导致集群的性能问题 - 特别是 API 服务器。空操作写入不会写入存储 (etcd) 或广播给任何观察者,因此实际上没什么大不了的。如果你仍然担心这一点,那么你今天可以使用上一节中解释的方法,或者你可以暂时仍然这样做,并等待额外的客户端机制来抑制零更改 apply。

为了解决这个缺点,为什么不 GET 对象,并且仅在对象需要时才发送你的 apply?令人惊讶的是,它没有多大帮助 - 对于 API 服务器来说,空操作 apply 并不比额外的 GET 多做很多工作;并且更改的 apply 比具有先前 GET 的相同 apply 便宜。更糟糕的是,由于它是一个分布式系统,因此在你的 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 系统的(人类)管理员观察到错误,并手动强制应用有问题的清单。然后 CI/CD 系统将能够成功应用清单并成为共同所有者。

    可选:然后管理员应用一个空白清单(仅包含对象类型/命名空间/名称),以放弃他们成为管理者的任何字段。如果省略此步骤,则管理员很有可能最终拥有字段并导致未来不必要的冲突。

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

  • 接受热修复:有问题的更改的作者看到冲突,并编辑他们的更改以接受生产环境中运行的值。

  • 接受然后拒绝:与接受选项相同,但在应用该清单后,并且 CI/CD 队列再次拥有所有内容(因此没有冲突),重新应用原始清单。

  • 我还可以想象 CI/CD 系统允许你以某种方式将清单标记为“强制冲突”——如果对此有需求,我们可以考虑制定更标准化的方法来做到这一点。一个严格的版本,让你准确声明你打算强制哪些冲突,将需要 API 服务器的支持;在没有它的情况下,你可以创建一个只有字段子集的第二个清单。

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

    如果这听起来像是你希望为自己的控制器采用的方法,请来与 SIG API Machinery 讨论!

祝你apply愉快!