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

欢迎!请参阅 关于 页面,了解更多信息。

0
Clojure

我们的主要 Clojure 应用在 CircleCI 上加载时间大约为 90 秒,在 my MacBook Pro(3.5GHz i7)上。在生产环境中,它在我们的 Kubernetes Pod 中的加载时间相似。

这个问题使我们多个方面出现问题

  • 本地开发时开发人员的生产力降低
  • 我们的 CI 流程很慢 - 我们自己的测试执行大约需要 1 分钟,但由于测试套件首先加载代码,所以执行测试的时间接近 3 分钟,我们还有 90 秒的加载时间。
  • 我们的部署时间和平均恢复时间减少 - 部署新版本的应用程序大约需要 20 分钟,因为我们进行 Kubernetes Pod 的滚动重启,每个 Pod 组启动需要 90 秒。

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

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

过去有人建议进行 AOT 编译,但我发现 AOT 编译从来没有达到宣传的效果,并且引入了奇怪的错误。它还要求代码以“AOT-aware”方式编写,包括所有库。

2 答案

+2
头像
被选中
 
最佳回答

在生产中进行滚动重启时,将AOT编译作为您应用程序JAR构建过程的一部分可以加快速度(构建JAR文件将明显变慢——您必须付出编译时间的“代价”)。

我们以前用源构建我们的uberjar应用,但我们也因生产中的启动时间变慢而受苦,所以我们切换到uberjar构建期间的AOT编译,这使我们从最差应用的约一分钟降低到仅几秒钟的生产启动时间。

至于持续集成(CI),为了运行测试,它们必须被加载和编译,因此您基本上无法避免这种时间耗费。您唯一的实际选项将是转向增量测试,并且只对变更或依赖于已变更代码的代码运行测试。对我们来说,这将是切换到Polylith的未来承诺之一,因为它内置的测试启动器可以根据git标签分析这个问题——但只有在您拥有“一切”后,这个好处才会体现出来。

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

在REPL中进行工作时,初始加载时间会有很大的差异,但随着时间的推移,由于越来越多的代码被更改,它“漂移”。然而,在工作中,我们倾向于保持相当长久的REPL,并在修改代码时评估所有代码,因此慢速首次加载并不是那么烦人,我并不总是使用classes这个方法。而且当我说“长久的REPL”时,意思是有时甚至长达几周甚至几个月。我的主要工作REPL到现在已经运行了十天,而且它之所以“这么短”,仅仅是因为我遇到了停电,不得不重启我的开发机器。

头像
谢谢Sean。

您对不安全的AOT代码有多少问题?根据Alex的建议,我尝试在我们的应用中做了一些AOT,但我遇到了一些问题。

有些已经很神秘了:我们有一个命名空间,该命名空间定义了两个符号,Identity->Foo和identity->foo(唯一的区别在于大写字母)。这两个符号名称焊接到两个文件中,在大小写不敏感的文件系统上发生冲突(多亏了苹果)。

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

在应用生命周期中此点启用AOT将是一个风险——它需要对大量代码进行审核。
by
我们没有任何代码导致与AOT有关的问题。

我将尽量抵制只对大小写不同的符号进行评论,使用intern,或者在编译时评估为特定环境值的def形式... :|
+1
by

每次您加载时,都会编译所有源代码。AOT编译允许您提前一次这样做,因此它是这项工作的工具。许多人使用AOT来此目的并且没有问题,因此我认为您只是对FUD作出反应。

您也可以在开发时间有策略地使用它——请参阅https://clojure.org/guides/dev_startup_time

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

by
当我说"AOT-aware"时,我的意思是顶级表单在代码编译时而不是在代码加载时评估。这意味着,例如,任何顶级`def`从环境拉取值在CI机器上这样做,而不是从生产机器上。
我认为大多数库都不会这样做(因为它对使用AOT的大多数人来说都是不利的),如果你发现了什么人会这样做,那么这值得关注。
嗨Alex,感谢你的贡献。

今天我花了一些时间带着新的乐观尝试使用AOT。遇到的一个问题是宏如何在编译单元间跟踪。

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

如果我随后的修改了那个文件中的宏,Clojure运行时会没有跟踪到这个变化(它会查看源文件和类文件的修改时间戳),在加载我的文件时将加载过时的类文件。

这可能导致我在本地开发时从另一个开发者那里拉取代码时出现问题,如果在CI中尝试在构建之间缓存编译后的类文件,也可能出现问题。

由于我遇到了一些与AOT不兼容的代码结构,而我没有在可用的时间内对这些代码进行重构,所以我还没有计算出我们可以通过AOT获得的加载时间节省。
Clojure将如果源文件较新于类文件时将其加载。因此,当你“拉取代码”时,通常你得到一个比缓存中更新的文件时间戳,Clojure运行时会加载这个文件。

因此,您有时会遇到过时的缓存并需要重新编译或进行一定程度上的缓存无效化。当您更新外部依赖项时,也会遇到同样的问题。然而,大部分时间里,大多数缓存应该保持稳定。

据我所知,有些人报告,大型应用程序的加载时间得到了大幅提高,比如减少了10秒或更多,甚至达到了一分钟以上。你所看到的效果将很大程度上取决于命名空间的数量和宏使用的强度。
...