请在2024 Clojure状态调查!中分享您的观点。

欢迎!请参阅关于页面以了解此功能的更多信息。

0
Clojure

我们在CircleCI上的主Clojure应用程序在我的MacBook Pro(3.5 GHz i7)上加载需要约90秒。在Kubernetes容器中运行时,加载时间相似。

这个时间在我们多个方面都带来了问题

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

我们的主应用程序的启动时间比其他应用程序慢 - 我们更现代、更精简的Clojure服务仍然需要约30秒来启动。Clojure的缓慢加载时间影响了我们的大多数团队和服务,但主应用程序的团队受到的痛苦更为明显。它之所以较慢,是因为它的依赖项比其他应用程序更多,因为一个Clojure应用程序中嵌入了多个子系统。

有哪些策略可以改进加载时间?

过去有人建议执行AOT编译,但我发现自己发现AOT编译根本不起作用,而且引入了奇怪的错误。它还要求代码以“AOT-aware”的方式编写,包括所有库。

2 条答案

+2

被选中
 
最佳答案

在部署中执行滚动重启时,将AOT编译作为应用程序JAR构建过程的组成部分将加快该过程(构建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形式发表评论... :|
+1

每次加载时,您都在编译所有的源代码。AOT编译允许您提前一次性完成这一步骤,因此它是这项工作的工具。许多人使用AOT来完成这个目的,并且没有遇到任何问题,所以我觉得您只是对FUD做出了反应。

您也可以在开发时策略性地使用它——见https://clojure.org/guides/dev_startup_time

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

当我说“AOT-aware”时,我的意思是顶层形式在代码编译时而不是在代码加载时进行评估。这意味着,例如,任何从环境中获取值的顶层`def`都会在CI机器上执行,而不是在生产机器上。
我认为大多数库都不会这么做(因为这会给许多使用AOT的人带来麻烦),并且如果你发现有什么不同的做法,那么这值得成为一个问题。
嗨Alex,感谢你的意见。

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

如果我编译一个使用在另一个文件中定义的宏的文件,那么据我所知,我的文件将编译到没有跟踪宏依赖的类文件。

如果我对另一个文件中的宏进行后续修改,Clojure运行时无法跟踪更改(它查看源文件和类文件的修改时间戳)并且在我加载我的文件时将加载过时的类文件。

这可能在本地开发中如果提取了其他开发者的代码时的本地环境中引起问题,以及在CI中如果尝试在构建之间缓存编译后的类文件时也会有问题。

由于我遇到了一些与AOT不兼容的代码结构,而且我没有足够的时间将这些代码重构为与您的AOT加载系统集成,所以我尚未能够计算出AOT可能带来的加载时间上的收益。
Clojure会在源文件比类文件新的时候加载源文件。所以当你“提取代码”,假定你会得到一个比缓存中的新文件,Clojure运行时将会加载该文件。

因此,有时候你可能会遇到陈旧的缓存,需要重新编译或进行一些程度的缓存失效。如果你更新外部依赖,你也会遇到同样的问题。然而,大多数时候,大多数缓存应该保持稳定。

据经验,我听说有人看到大型应用程序的加载时间有了秒乃至分钟的改进。你所看到的效果在很大程度上取决于命名空间数量和使用宏的强度。
...