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

欢迎!请参阅关于页面,了解有关该工作的更多信息。

0
Clojure by

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

这给我们带来了多个方面的问题

  • 本地的本地开发中,开发者的生产力降低
  • 我们的CI过程很慢——测试本身大约需要1分钟时间运行,但由于测试套件需要加载代码,所以运行测试的时间接近3分钟,而且还包含那个90秒的加载时间。
  • 我们的部署时间和平均恢复时间减少——部署新版本的应用程序大约需要20分钟,因为我们以滚动重启Kubernetes Pod的方式部署,每个Pod组启动需要90秒。

我们主要应用的开机时间比其他应用慢——我们更现代、更精简的Clojure服务仍然需要大约30秒来启动。Clojure的慢速加载时间影响我们的大部分团队和服务,但是对于主要应用的工作团队来说,这种痛苦是直接的。这主要是因为它比其他应用有更多的依赖项,因为它将许多子系统内嵌在单个Clojure应用中。

有哪些提高加载时间的方法?

过去有人建议做AOT编译,但我发现AOT编译从未像预期的那样起作用,还会引入奇怪的bug。它还需要以“AOT-aware”的方式编写代码,包括所有库。

2 答案

+2

已选择
 
最佳回答

在生产中进行滚动重启时,将Ahead-of-Time(AOT)编译作为应用程序JAR构建过程的一部分将加快这个过程(构建JAR会慢得多——你必须为编译时间“惩罚”付费)。

我们之前以源代码的形式构建我们的应用uberjar,但我们也遭受了生产中缓慢的启动时间,因此我们切换到在uberjar构建过程中进行AOT编译,这使得我们的生产启动时间从最差应用程序的约一分钟降低到仅几秒钟。

至于持续集成/持续部署(CI),为了运行测试,测试必须被加载和编译,因此你实际上无法避免这段时间 某处 的开销。你唯一真正的选择将是转向增量测试,并且只为更改的代码或依赖于更改的代码运行测试。对于我们来说,这是切换到Polylith的未来的一个承诺,因为它的内置测试运行器根据git标签找到这一点——但这只有在你有“所有”代码在Polylith中时才会带来好处。

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

在REPL(读/eval/output)环境中工作时,这会对初始加载时间产生很大影响,但随着时间的推移,它会“漂移”,因为越来越多的代码发生变化。然而,在工作中,我们倾向于拥有较长时间的生命周期REPL,并像修改代码一样从头评估所有代码,因此第一次缓慢加载的问题并不那么令人讨厌,而且我不总是在做classes这件事情。当我提到“长时间运行的REPL”时,我是指几周,有时甚至是几个月。目前我主要的工作REPL已经运行了十天了,它这么“短”的唯一原因是我遭遇了电力中断,不得不重启我的开发机器。

感谢Sean。

您的代码中有很多不是AOT安全的代码问题吗?根据Alex的建议,我尝试在应用程序中做一些AOT,但我遇到了一堆问题。

有些内容晦涩难懂:我们有一个命名空间,定义了两个符号,Identity->Foo 和 identity->foo(两者唯一的不同在于字母的大小写)。这两个符号名称被错误地混合到了两个文件中,而这在大小写不敏感的文件系统中发生了冲突(感谢苹果)。

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

在应用程序的生命周期这个阶段启用 AOT 可能会有风险 - 这需要审计大量的代码。
by
我们没有引起 AOT 问题代码。

我将尝试避免对只在大写或小写字符上不同的符号、使用 intern 或在编译时评估为特定环境值的形式的注释…… :|
+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加载系统兼容,所以我还没有能够计算使用AOT可能获得的负载时间增益。
Clojure将加载源文件,如果它比类文件新。所以当你“拉取代码”时,你很可能得到一个比缓存中更新的文件时间戳,Clojure运行时会加载那个文件。

因此,确实有可能你有时会有一个过时的缓存,需要重新编译或进行一些缓存失效。如果你更新了外部依赖,你也会有同样的问题。然而,大多数时间,大多数缓存应该是稳定的。

据我所知,有人看到了大型应用程序的加载时间改善,减少了10秒或甚至超过一分钟的加载时间。你会看到什么很大程度上取决于命名空间的数量和宏使用的强度。
...