2024 Clojure状态调查!中分享你的想法。

欢迎!请查看关于页面以了解更多关于如何使用本系统的信息。

0
Clojure

我们的CircleCI主Clojure应用在我MacBook Pro(3.5ghz i7)上加载大约需要90秒。在生产环境中,我们的Kubernetes容器以类似的速度加载。

这段时间让我们在多个方面遇到了问题

  • 本地开发中开发者的生产率降低
  • 我们的CI过程缓慢 - 我们自己的测试需要约一分钟来运行,但运行测试的时间更接近三分钟,因为测试套件首先加载代码,我们有90秒的加载时间。
  • 我们的部署时间和平均恢复时间都减少了 - 部署新版本的应用程序需要大约20分钟,因为我们进行Kubernetes容器的滚动重启,每个容器组需要90秒才能启动。

我们主应用程序的启动时间比其他应用程序慢得多 - 我们更现代、更精简的Clojure服务启动仍然需要大约30秒。Clojure的缓慢加载时间影响了我们的大部分团队和 сервисов,但主要是负责主应用程序的团队感受到了这种痛苦。它之所以运行较慢,是因为它有比其他应用程序更多的依赖关系,因为它在一个Clojure应用程序中嵌入了许多子系统。

存在哪些提高加载时间的策略?

在过去,人们建议进行AOT编译,但我发现AOT编译从未按预期工作,并引入了奇怪的错误。它还要求以“AOT感知”的方式编写代码,包括所有库。

2 个答案

+2
作者:
选择 选择者:
 
最佳答案

在生产中滚动重启时,在构建应用程序 JAR 的过程中作为 AOT 编译的一部分可以加快这个过程(构建 JAR 会有所缓慢——你必须在某个地方支付编译时间的“罚金”)。

我们曾经将我们的应用程序 uberjars 作为源代码构建,但我们也在生产中发现启动时间较慢,因此我们在构建 uberjar 的过程中使用了 AOT 编译,这将最坏应用程序的生产启动时间从一分钟左右降低到几秒钟。

至于 CI,为了运行测试,它们必须被加载和编译,因此你实际上无法避免这部分时间。你唯一真正可选的是转向增量测试,只为更改了代码或依赖于更改了代码的部分运行测试。对于我们来说,这是转向 Polylith 的未来发展承诺之一,因为其内置的测试运行程序会根据 git 标签进行处理——但这只能在“一切都在 Polylith 中”时才会有这样的好处。

Alex 提到了加快开发加载的文档,我在工作中通过在类路径上创建一个本地的 classes 文件夹并定期为我们的源代码库中的每个主要“应用程序”手动启动一个 compile 步骤来使用这种方法。这意味着自上次编译以来没有更改的任何代码将仅使用现有的 .class 文件,无需在加载时通过 Clojure 编译过程——但如果 代码更改的话,它在加载时仍然需要编译。

在与 REPL 一起工作时,它对初始加载时间有很大影响,但随着时间的推移,由于越来越多的代码发生变化,它会“漂移”。然而,在工作时,我们倾向于使用生命期较长的 REPL,并修改代码时评估所有代码,因此缓慢首次加载问题并不那么讨厌,我并不总是使用 classes 事物。我所说的“生命周期较长的 REPL”是指持续的几个星期,有时甚至几个月。我主要的作业 REPL 已经运行了十天,它之所以这么“短”,是因为我遇到了停电,不得不重启开发机器。

作者:
谢谢你,Sean。

你有没有遇到过很多非 AOT 安全的代码问题?在 Alex 的建议下,我试图对我们的应用程序进行一些 AOT 编译,但遇到了一些问题。

有些问题是模糊的:我们有一个命名空间定义了两个符号,Identity->Foo和identity->foo(唯一的不同之处在于大写字母)。这两个符号名称合并到两个文件中,在忽略大小写的文件系统上冲突(多亏了苹果)。

我们还有一个宏,它展开成调用`intern`以将新变量添加到命名空间的格式。这与AOT代码不兼容——当编译的类被加载时,变量不存在。

在本应用的生命周期这个阶段启用AOT将是一个风险——它需要审计大量代码。
我们没有任何代码会导致AOT有问题。

我将尽力抵制对仅在大小写上有所不同的符号、使用intern或者评估生成环境特定值的def形式的评论…… :|

每次加载时,你都在编译所有源代码。AOT编译允许你事先这样做,所以这就是完成这项工作的工具。很多人使用AOT来完成这项任务且没有任何问题,所以我认为你只是在这里对FUD做出反应。

你还可以在生产时间有战略性地使用它——参见https://clojure.org/guides/dev_startup_time

我不知道你所说的AOT-aware是什么意思。

当我说“AOT-aware”时,我的意思是顶层形式在代码编译时评估,而不是在代码加载时评估。这意味着,例如,任何从环境获取值的顶层`def`都是在CI机器上而不是从生产机器上完成的。
by
我认为大多数库都没有这么做(因为对许多需要进行AOT编译的人来说这是不利的)并且如果你发现有些库确实这样做,那将值得提出一个相关问题。
by
嗨,Alex,感谢你的贡献。

今天我花了一些时间,用一种新的乐观态度来考虑使用AOT。我遇到的一个问题是,如何在编译单元之间跟踪宏。

如果我为使用在另一个文件中定义的宏的文件进行编译,那么据我所知,我的文件被编译成一个没有跟踪宏依赖关系的类文件。

如果随后我修改了另一个文件中的宏,Clojure运行时将无法跟踪这种修改(它查看源文件和类文件的修改时间戳),当我加载我的文件时,将加载过时的类文件。

这可能在本地开发时,当我拉取其他开发者更改的代码时造成问题,以及在CI中,如果我在构建之间尝试缓存编译后的类文件时也是如此。

由于我遇到了一些与AOT不兼容的代码结构,而我无法在可用的时间内对这些代码进行重构以与您的AOT加载系统兼容,因此我尚未能够计算出使用AOT可以获得加载时间的提升。
by
Clojure将加载源文件,如果它比类文件新。所以当你“拉取代码”时,你可能会得到一个比缓存中更新的文件,Clojure运行时会加载它。

所以,的确有可能有时会有过时的缓存,需要重新编译或进行一些缓存失效操作。如果你更新了外部依赖,你也会遇到同样的问题。然而,大多数时候,大多数的缓存应该保持稳定。

据我所知,人们看到大型应用程序的加载时间有所改善,减少了10秒甚至更多,甚至超过一分钟。你所看到的效果将很大程度上取决于命名空间的数量和宏使用的强度。
...