Quarkus 如何用于在 OpenShift 上部署应用程序

这篇文章提供了一些关于我在专业环境中部署 Kubernetes 应用程序时遇到的一个特殊挑战的反馈,以及我们如何使用 Quarkus 提供一个满足我们目标的解决方案。

挑战

几年来,我一直参与一个 Kubernetes 项目,旨在:

  • 更轻松、更快速地集成新技术(无论是应用程序框架还是中间件产品)。

  • 通过在这些服务之上创建一个逻辑部署抽象来降低部署分布式相关服务的管理负担;类似于 umbrella chart 模式。

  • 限制应用团队部署组件所需的工作量,并高度标准化 Kubernetes 部署对象。

  • 过渡到混合云部署模式。

除了这些目标之外,还有一个与应用程序可能按租户部署相关的约束(即,由于各种原因,它们不是多租户的,其中一些与监管相关)。根据应用程序的不同,可能有几十个租户,这将转化为每个租户部署一次应用程序(因此,如果一个应用程序有 10 个租户,则将在生产环境中运行 10 个进程)。但是,如果进程需要物理分离,则通常某些配置部分可以在某些租户和/或 IT 环境和/或地理区域之间根据不同的业务规则共享。高度简化和分解的配置位将是开发团队真正简化的来源,也是部署过程真正复杂性的来源。

一些预测表明,在项目结束时,生产环境中的不同集群将包含 10,000 个 Pod。

首次尝试

鉴于这种复杂性,我们创建了自己的配置和部署工具。初始版本基于 Ansible 并在 Tower 工作流程中运行。由于各种原因,我们决定在一年前用 Java 重写它,主要是为了:

  • 获得更好的性能。

  • 使用能够应对算法复杂性的高级语言。

  • 获得对单元和集成测试的更好支持。

  • 使用高质量的开发工具(例如 IDE)提高生产力。

然而,我们担心的一件事是内存消耗、CPU 使用率和启动时间,因为对于给定的应用程序,每个租户都在其自己的部署进程中隔离部署。因此,我们决定在开始 Java 重写时,不依赖任何应用程序框架,因为如果它们确实提供了生产力和标准化,那么它们提供的抽象会带来资源成本。因此,该程序是用纯 Java 编写的,并且在大小和用例方面都足够小,我们可以摆脱它,前提是我们依赖一些模式(例如,手动基于构造函数的注入)来实现简洁的代码。这个程序叫做 ocpdeploy

当我们远离 Tower 时,我们决定通过在 Tekton 管道中将 ocpdeploy 作为任务运行来利用我们的 Kubernetes 基础设施。这为我们提供了可重复性,一种部署不同应用程序的方式,甚至可以使用不同的 ocpdeploy 版本部署同一应用程序的不同租户。使用 java(任何其他高级语言都可以很好地适应)为我们提供了高水平的生产力和可维护的代码,同时能够实现用于配置处理的复杂算法,并通过我们广泛的回归测试套件提高了 ocpdeploy 发布的质量水平。

一切都很好……直到我们开始同时部署多个租户和/或不同的应用程序。我们将 ocpdeploy 容器的大小调整为 200 millicores280 Mb 的 RAM。对于某些应用程序,大约有 30 个租户,它们将全部并行部署。这意味着 30 个 Pod,相当于 6 个内核8 Gb 的 RAM。这似乎是可以承受的,但部署往往在营业时间之后完成,许多人在晚上 7 点到 8 点之间完成。我们开始担心,如果我们在启动时对集群造成影响的 SpringBoot 或 WildFly 应用程序之上并行运行多个部署,那么 ocpdeploy 本身会产生什么影响。

进入 Quarkus 世界

因此,不到一年前,我们决定在 ocpdeploy 上启动一个 Quarkus POC,目标是将其部署为 GraalVM 可执行文件。我们遇到的主要挑战与我们使用的某些库中缺少对 GraalVM 的支持有关:FreeMarker、SMBJ(一个实现服务器消息块 SMB2 和 SMB3 协议的 Java 客户端库)、Fabric8 OpenShift 客户端(在实例化 OpenShift 特定 CRD 时)以及一个将日志发送到专有集中式日志系统的自制 Java 代理。

幸运的是,Quarkus 中已经存在其他部分,例如对命令模式应用程序的支持、REST 客户端、Vault 扩展以及对 Fabric8 Kubernetes 客户端的支持,这为添加到核心的新 OpenShift 客户端扩展提供了一个很好的基础。仅 Kubernetes Client 扩展就对该项目产生了巨大的推动作用,因为当我们开始时,我们没有 Argo 可用,因此我们必须自己实现 applyprune

对于 FreeMarker,我们向 Quarkiverse 推送了一个新的扩展,其代码很大程度上受到了 ppalagacarlosthe19916 的工作的启发。对于 SMBJ,我们在内部 Quarkiverse 中创建了一个扩展。对于我们的日志客户端,我能够从 quarkus-logging-gelf 扩展中获得一些灵感,并创建了一个额外的内部扩展。

通过 Quarkus 项目提供的非常好的文档和框架中的支持(例如 processorsrecorders),可以更轻松地让库在本地运行:在 Quarkus 扩展中支持本地模式

当事情变得有点棘手时(SMBJ 比我们想象的更棘手),我们得到了 GraalVM 跟踪代理 的一些帮助。

以下是我们碰巧使用的扩展列表:cdi、config-yaml、freemarker、hibernate-validator、kubernetes-client、openshift-client、rest-client、restclient-jackson、vault(加上 SMBJ 和我们的内部集中式日志系统的内部扩展)。

最终,我们让所有必需的库都能以本机模式运行,并且我们可以将精力转向将应用程序迁移为看起来像 真正的 Quarkus 应用程序

  • 使用注入,并在必要时使用限定符和生产者。

  • 重写测试以使用不同的模拟方法,包括新的(当时)QuarkusTest profiles

这使我们能够在各种情况下通过 Kubernetes yaml 生成、快照重放 提供广泛的测试,从而将所有配置转换为符合 MP config 的配置。

基准测试

然后,我们运行了一些基准测试,以评估与旧版本相比的资源消耗。我们并没有真正担心启动时间。我们知道它会非常好。只要它保持在几秒钟的窗口内(我们在纯 Java 版本中体验到),我们就可以接受。但是内存和 CPU 消耗是另一回事。整个练习的动机是希望获得一些真正的收益。

使用纯 Java 版本,我们能够将容器压缩到 180 Mb(低于该值,它要么会 OOM,要么 GC 会降低性能)。对于本机版本,我们能够低至 50 Mb

然后,我们使用不同的配置执行了几次运行(plain Java 180 MbQuarkus native 180 MbQuarkus jvm 180 MbQuarkus native 50 Mb,从 50 millicores request=limit 开始,一直到 100150……

50 millicores 时,Quarkus native 180 Mb 将在 20 秒内运行,Quarkus native 50 Mb 在 60 秒内运行,另外两个在 300 秒内运行。这意味着如果我们允许 180 Mbocpdeploy,我们可以从 300 秒到 20 秒。提高了 x15 倍。

250 millicores 时,我们在 5 秒内运行了 Quarkus native 180 Mb,在 10 秒内运行了 Quarkus native 50 Mb,另外两个在 40 秒左右运行。

我们还搜索了需要多少 CPU 才能在 60 秒内完成部署。

对于 Quarkus native 50 Mb,它是 50 millicores,对于 plain Java 180 MbQuarkus jvm 180 Mb,它是 240 millicores (Quarkus native 180 Mb 超出范围,因为如第一个测试中所讨论的,其“最差”结果为 20 秒)。这意味着如果时间是我们的约束,我们可以通过迁移到 Quarkus native 从 240 millicores 降至 50 millicores,同时从 180 Mb 降至 50 Mb

Quarkus native 180 Mb50 Mb 之间的比较也很有趣,因为它表明,通过上下推动内存和 CPU 旋钮,我们可以处理用例执行持续时间。然后由我们决定执行时间和资源消耗之间的最佳平衡点在哪里。

我们所做的最后一个有趣的观察是 plain Java 180 Mb 的结果与 Quarkus jvm 180 Mb 的结果几乎相同。这意味着应用程序框架(提供可维护性和生产力)的成本在我们的例子中为 0。这就像拥有蛋糕并同时吃掉它一样。在我们的例子中,我们并不介意缓慢的执行,只要我们可以在内存和 CPU 上节省大量空间,我们就可以实现这一点。

不同限制(以毫核为单位)下的程序执行时间(秒)

plain Java (180Mb)(纯 Java (180Mb)) Quarkus Native (180Mb)(Quarkus 本机 (180Mb)) Quarkus JVM (180Mb)(Quarkus JVM (180Mb)) Quarkus Native (50Mb)(Quarkus 本机 (50Mb))

50m

283

18

306

61

100m

95

9

120

32

250m

38

4

43

11

500m

17

3

21

8

1000m

11

3

11

5

1500m

9

3

8

5

2000m

7

3

7

5

无限制

8

3

6

5

results

一些问题

除了在 GraalVM 中运行 ad hoc 库的挑战之外,我们还遇到了一些意外的行为或小的痛点,例如:

  • 缺少有关未知应用程序配置属性的警告 #14889

  • 如果在 src/main/resources/application.yaml 中设置,Config pojo 上的默认值应表现相同 #13423

  • 如果提供了 application.yaml 但没有 quarkus-config-yaml 依赖项,则发出警告或失败 #13227

  • 升级到 MP Rest Client 2.0 #10520,我们一直在等待它以获得“跟随重定向”

  • 我希望有一种通过注释为每个测试定义模拟替代方案的方法,但这已在 此线程 中得到解答

  • 无法在运行时而不是构建时提供证书(GraalVM 限制)#3091

  • src/main/resources/application.properties 在测试中的可见性,如 此线程 中讨论的那样;从那时起,已经完成了大量工作,例如“使用 AbstractLocationConfigSourceLoader 加载 application.properties 和 application.yaml”#15282,因此我需要重新检查

  • 使用空值覆盖可选配置属性,如 此线程 中讨论的那样

  • Quarkus YAML 配置键被隐式转义 #11744

  • application.properties 包含在存档中

  • 如果提供了 application.yaml 但没有 quarkus-config-yaml 依赖项,则发出警告或失败 #13227

这似乎有点多,但实际上这些要点都不是主要问题,也不是我们无法解决的问题。一些问题与配置有关,部分原因是该程序作为 Tekton 任务运行,并且在 Tekton 中定义可选参数的灵活性有限。我只列出了我们必须解决的这些问题。当我们通过 google 组中的答案或实际修复取得进展时,许多问题或疑问实际上得到了解决。

本机构建的 CI

另一个挑战与本机构建时间、内存和 CPU 使用率有关。我们采取的方法是仅在 master 上生成本机可执行文件,并在功能分支上以 jvm 模式运行测试。如果需要,我们仍然可以选择针对特定功能分支中的本机模式进行测试。但是,除非我们集成一个新的库,否则让 jvm 测试通过就足以让我们确信我们将在本机模式下具有相同的行为。

我们应用的另一个技巧是,如果触发了新的构建,则取消 master 上的当前构建;这样我们就不会同时运行多个本机构建。从理论上讲,由于完成了另一个提交而导致构建被终止可能会令人沮丧。但在实践中,这不是问题,因为 PR 合并到 master 上的速率仍然很低。

但是,如果我们想增加内部开发的 Quarkus 应用程序的数量,那么肯定有一些关于调整构建基础设施大小的问题。

与原始解决方案相比,唯一的缺点是我们的回归测试套件运行速度较慢,主要是因为我们的测试在特定配置的上下文中生成资源。而且由于我们使用的是 MP config,因此每次我们要测试不同的配置时都需要启动一个新的 Quarkus 上下文。幸运的是,在 Quarkus 中启动新上下文非常快,但仍然比我们最初的纯 Java 解决方案慢得多。

一切都与社区有关

除了在 Vault、FreeMarker 和 OpenShift Client 扩展方面的工作外,我们还开始在 Quarkus 内外贡献一些 PR,希望加快改进的进程,例如:

  • 为 CRD 对象添加 appendResourceVersionInObject #2365(已合并)

  • 添加 cert-manager 扩展支持 #2930(已合并)

  • 在 ci 中使用 cekit 构建 distroless 镜像 #118(已合并)

  • 允许在本机镜像的运行时配置根证书 #3091(Christian Wimmer 在 Oracle 分配 teshull 来为 GraalVM 21.3 版本处理此问题)

值得注意的是,Quarkus 社区一直是成功的巨大因素,通过:

  • 回答 google group、zulip 或其他方式(例如 stackoverflow)上的问题。

  • 及时调查问题,并根据情况提供修复、应用正确方法的指导或解决方法。

  • 对改进持开放态度(例如,关于配置)。

  • 快速审查 PR 并促进贡献。

  • 以高频率发布。

接管

我们只花了几个月的时间完成了迁移,并且我们在去年年底开始使用我们由 Quarkus 提供支持的 ocpdeploy 部署我们的第一个 OpenShift 应用程序。从那时起,它被用于运行数百个部署。我们能够以有限的努力跟上新发布的步伐。我们遇到了很少的错误,通常会在下一个版本中及时修复。Quarkus 提供的应用程序框架使我们能够更好地组织代码,使其更易于维护,并且易于使用模拟功能和测试配置文件进行测试。同样有趣的是,ocpdeploy 的开发是由不是 Quarkus 开发人员,甚至不是强大的 Java 专家的团队成员完成的。这表明该框架足够轻量级,一旦整体结构(例如组件、测试、配置、ci)就位,我们就可以忘记它。

我们的程序并非业务关键型程序,因为如果它不起作用,业务不会停止。但它是部署我们所有 OpenShift 微服务的唯一手段。鉴于内部开发的步伐,它被认为是价值链中的关键部分。ocpdeploy 显然不是很复杂,我们只触及了 Quarkus 可以完成的工作的表面,但它对我们有效,表明它可以处理许多不同的用例。

总之,我认为 Quarkus 的强大卖点是:

  • 一个充满活力的社区,积极倾听反馈并欢迎贡献

  • 以高速开发的项目

  • 正确的定位(云原生,开发者喜悦,……)和多功能性

  • 当规范与 Quarkus 值背道而驰时,愿意务实地稍微弯曲规范

  • 具有紧凑的核心和扩展的架构,允许快速扩展,这将孕育创新

  • 默认情况下支持本机模式,但也对 jvm 模式进行了一些改进

  • 通过核心扩展、universe 扩展(例如 camel)和 Quarkiverse 快速增长的生态系统

  • 开发模式(本地和远程)

  • 一种用于开发扩展的框架,有助于实现诸如构建时初始化之类的模式

我看到的主要挑战与 10 或 15 年前的应用商店类似,是要确保扩展生态系统在数量上增长而又不牺牲质量。或者至少提供一种对扩展进行评级的方法,以便构建业务关键型应用程序的人员能够确保他们对技术的投资是可靠的。换句话说,除了扩展范围之外,还要深入研究。我看到的另一个挑战是逐步填补企业准备就绪的空白;例如,完成有时今天部分开发的解决方案,并确保为最常见的用例提供清晰和成熟的解决方案。

这就是 Quarkus 将赢得企业的青睐的方式。我可以看到这一点正在进行中。

我对该项目充满希望。迫不及待地想扩展用例。