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

欢迎!请访问关于页面以获取更多关于这个工作原理的信息。

0
Clojure

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

这段时间在多个方面给我们带来了问题:

  • 本地开发时,开发者的生产力降低。
  • 我们的 CI 过程很慢——我们自己的测试大约需要一分钟来运行,但由于测试套件首先加载代码,所以运行测试的时间更接近三分钟,而且我们还有 90 秒的加载时间。
  • 我们的部署时间和平均恢复时间减少了——部署应用程序的新版本大约需要 20 分钟,因为我们执行 Kubernetes 容器的滚动重启,每组容器启动需要 90 秒。

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

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

过去有人建议做 AOT 编译,但我发现 AOT 编译 Never 标称的效果,并引入了奇怪的错误。它还要求代码以“AOT-aware”的方式编写,包括所有库。

2 答案

+2
by
选择 by
 
最佳答案

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

我们过去将应用程序uberjars作为源代码构建,但在生产中也遭受了启动时间缓慢的问题,因此我们在构建uberjar时转向了AOT编译,这使得我们的生产启动时间从最差的几个应用程序一分钟来降低到仅仅几秒钟。

至于CI,为了运行测试,它们必须被加载和编译,因此你实际上无法避免在某个地方这段时间。你唯一真正可做的选择就是转到增量测试,并且只为更改的代码或依赖于更改代码的代码运行测试。对于我们来说,这是转换到Polylith的未来的一个承诺之一,因为其内置的测试运行器基于Git标签计算这一点—但这个好处只有在你有“一切”都在Polylith中才会出现。

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

在REPL中工作时对初始加载时间有很大影响,但随时间推移,由于越来越多代码的更改,它会“漂移”。然而,在工作中,我们倾向于拥有长时间运行的REPL,并在修改时评估所有代码,因此首次加载的慢问题不是那么烦人,我不需要经常打扰classes。当我提到“长时间运行的REPL”时,我的意思是几周到有时几个月。我的主要工作REPL已经运行了十天,唯一的原因是停电,我不得不重启我的开发机器。

by
谢谢Sean。

您压缩了很多不安全的 AOT 代码的问题吗?按照 Alex 的建议,我试图在我们的应用中进行一些 AOT,但我遇到了一堆问题。

其中有几个很隐蔽:我们有一个命名空间定义了两个符号,Identity->Foo 和 identity->foo(唯一的不同点在于大小写)。这两个符号名被转换成了两个文件,由于大小写不敏感的文件系统冲突(多亏了 Apple)。

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

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

我将尽量抵制对只在不同大小写上存在差异的符号、使用 intern 或在编译时评估出特定环境值的形式... :| 进行评论。
+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运行时会加载这个文件。

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

据说,有人看到在大型的应用中加载时间有所改善,减少了10秒甚至更长。你将看到什么严重依赖于命名空间的数量和宏的使用强度。
...