请分享您的想法,参与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-aware"的方式编写,包括所有库。

2 个答案

+2
 
最佳答案

在生产中,将AOT编译作为应用程序JAR构建过程的一部分,将加快滚动重启速度(构建JAR将大大减慢 -- 你必须为编译时间“处罚”买单)。

我们以前将应用程序uberjar构建为源码,但在生产中启动时间很慢,所以我们改为在uberjar构建过程中使用AOT编译,这将最差应用程序的生产启动时间从一分多钟降低到几秒钟。

至于CI,为了运行测试,必须加载和编译它们,所以你真的无法避免这段时间在某个地方。你的唯一实际选择是进行增量测试,仅针对已更改的代码或依赖于已更改代码的代码运行测试。对我们来说,这是切换到Polylith的未来的承诺之一,因为其内置的测试运行者基于git标签计算出这一点 -- 但这种好处只有在你有“一切”在Polylith中时才会出现。

Alex提到了关于加快开发加载的文档,我 workspace 中的 classes 文件夹,并定期为我 repo 中主要的“应用程序”手动启动一个compile步骤。这意味着自上次编译以来没有更改的任何代码将只使用现有的.class文件,而不必在加载时经过Clojure编译过程 -- 但任何更改的代码在加载时仍需要编译。

在REPL中工作对初始加载时间有很大影响,但随时间推移,它随着越来越多的代码更改而“漂移”。然而,在我们的工作中,我们通常会有相当长久的REPL,并且在我们修改它时评估所有代码,所以慢速首次加载问题并不那么令人烦恼,我并不总是用classes东西。当我说“长久的REPL”时,我是指数周,有时甚至是数月。我的主要工作REPL现在已经运行了十天,它这么“短”的唯一原因是发生了电力故障,我不得不重起我的开发机器。

谢谢Sean。

你的代码有多少不是AOT安全的?根据Alex的建议,我已经尝试在我们应用程序上进行一些AOT,但我遇到了一堆问题。

有些问题很隐蔽:我们有一个命名空间定义了两个符号,Identity->Foo和identity->foo(唯一的区别在于大写字母)。这两个符号名在大小写不敏感的文件系统中混淆到两个文件中(感谢苹果)。

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

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

我将尽力避免评论只在不同大小写上的符号,使用intern,或者评估编译时为特定环境值的def形式... :|
+1

每次你加载都在编译所有源代码。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秒或更多的时间减少。你所看到的将取决于名称空间的数量以及宏的使用强度。
...