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

欢迎!请查看关于页面获取更多关于如何使用的详细信息。

0
Clojure

我们在 CircleCI 的主要 Clojure 应用程序在 my MacBook Pro(3.5gz i7)上加载大约需要 90 秒。在 Kubernetes 容器中的生产环境中也花费了相似的时间来加载。

这段时间在我们的多个领域造成问题

  • 当地开发生产力下降
  • 我们的 CI 流程缓慢 - 我们自己的测试本身需要大约一分种的时间运行,但由于测试套件首先加载代码,以及我们的加载时间为 90 秒,运行测试的时间接近三分钟。
  • 我们的部署时间和平均恢复时间减少 - 部署应用程序的新版本大约需要 20 分钟,因为我们对 Kubernetes 容器进行滚动重启,每个容器组需要 90 秒来启动。

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

有哪些策略可以提高加载时间?

过去有人建议进行 AOT 编译,但我发现 AOT 编译从未像广告中那样工作,并引入了奇怪的错误。它还要求以“AOT-aware”的方式编写所有代码(包括所有库)。

2 个答案

+2

被选择
 
最佳答案

在生产环境中进行滚动重启时,将AOT编译作为应用JAR构建过程的一部分可以加快这个速度(构建JAR将会显著变慢——你必须支付编译时间的"惩罚")。

我们以前以源代码的形式构建我们的app uberjars,但在生产中也面临着启动速度慢的问题,所以我们在uberjar构建时切换到AOT编译,这降低了我们在生产环境下最差的app的启动时间,从大约一分钟减少到仅有几秒。

至于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会在源文件 newer(更新)于类文件时才加载源文件。所以当你“获取代码”时,你很可能会得到一个比你在缓存中有的更新的文件时间戳,Clojure运行时会去加载那个文件。

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

据我所知,人们已经在大型应用程序上看到加载时间的改进,10秒或甚至一分钟以上的减少。你会看到什么取决于命名空间的数量以及宏的使用强度。
...