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

欢迎!请查看关于页面以了解更多关于它是如何工作的信息。

+13
多方法

再现内存泄漏的一个简单方法

Clojure 1.10.2
user=> (defmulti f identity)
#'user/f
user=> (defmethod f :default [x] nil)
#object[clojure.lang.MultiFn 0x4bb8855f "clojure.lang.MultiFn@4bb8855f"]
user=> (dotimes [_ 100000] (f (byte-array 1000000)))

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

如果您调整数字以免出现 OOM 错误,您将看到持续的内存泄漏。

罪魁祸首似乎是 MultiFn.findAndCacheBestMethod 为与默认方法匹配的每个值都缓存。

1 个答案

+1

我不知道这更多是泄露还是对分派值数量有限制的假设。

曾经,我们没有缓存导致 :default 情况的分派值,这使得 :default 非常慢(这是多方法长时间被考虑为“慢”的主要原因)。在 1.8 中,http://dev.clojure.org/jira/browse/CLJ-1429 改变了这一点,这给了许多真实的程序巨大的性能提升。

也许有一个折衷的方案,但我不确定。

在这种情况下,使用协议是否是避免缓存溢出的更好选择?或者也许可以手动实现`case`,完全避免使用多方法以及协议?
我的担忧,与其说是由于开发者的失误导致的意外泄漏,不如说是关于允许(可能恶意或高度随机的)用户输入击中多方法所可能引起实际问题的实质性。

我相信大多数Clojure程序员都非常小心,并且如果明确表明这种缓存正在进行,那么人们就会三思而后行关于保留`:default`方法或寻找关闭多方法输入的其他方式 - 但并不明显。我已经专业使用Clojure多年了,但不知道`:default`匹配是被缓存的。

例如,memoize将引起类似的问题,但我觉得在那个情况下(你可能认为这是显而易见的),而多方法(multi-methods)对我来说则有些微妙。

我不知道是否有解决方案,当然,快速是很重要的,而且在几乎所有的情况下,我都确信它是没有问题的。
假设这是可能的,使用clojure.core.cache中的LRU或其他缓存作为defmulti选项能否解决这个问题?(未缓存的“慢”的多方法查找是否比安全bounded缓存的开销更大?)
需要注意的是,这种缓存发生在分派函数的输出上,而不是多方法或分派函数的输入上。最常用的分派函数有 `type`、`class` 或用于从映射中提取字段的 关键字 等。虽然它们都有无界的范围,但它们的范围通常比域更有限。我不记得有人在过去5年多的时间里报道过这个问题。协议也使用缓存(每个调用点)实际上(除了ns/var表之外),这些是Clojure运行时中的两个主要状态性内容。

你可以添加很多可能控制的东西,与memoize非常相似。在后一种情况下,我们将它推向了core.memoize/core.cache,并保留了核心中的简单版本。我不是为 / 或反对此事进行辩护,需要一些评估。创建 https://clojure.atlassian.net/browse/CLJ-2626
by
完全有道理,派生值应该不多。但有时派生值并不是开发者所预期的。

在我使用关键字作为分派函数时,意外在生产环境中遇到了内存泄漏。我的意图是根据第一个参数字段关联的值进行分派,但不幸的是,当找不到关键字时,作为函数的关键字将多方法的第二个参数错误地处理为派生值。

以下是一种简单的复制方法

```
(defmulti foo :type)
(defmethod foo :default [_ _] nil)
(foo {} (Math/random))
```

我的意图是得到
```
(defmulti foo (fn [x y]
                (:type x)))
```

注意,我的有缺陷的代码没有任何功能性错误,只是由于多方法的缓存机制造成的内存泄漏。
by
尽管它适用于分派的结果,但这些结果可能是作为外部世界或其他程序的输入提供的,除非程序员已经确保这一点,否则不能假定这些结果是有界的。我认为这与协议缓存相当不同,因为类型与值不同,想象一个以计数为依据分派的多方法,你可以为0、1、2进行专门化,然后留出 :default 以处理其他情况,或者一个 :type 字段,其值在HTML表单中选择,:default 可以作为无效选择的回退。
by
仅为清单中添加另一个可能导致人们困惑的意外问题。最近有一个服务在升级一些依赖项后出现了内存泄漏。问题组合为 plumatic schema + ring swagger + compojure api。在这个提交中,schema 改变以返回一个匿名函数:[链接](https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0) 以及 ring swagger 使用 `identity` 作为分派函数 [链接](https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178),因此当在 compojure api 调用中的查询参数中使用一个映射时,它最终会在每个请求中将匿名函数放入方法缓存中,用于 `ring.swagger.coerce/custom-matcher`(和时间匹配器)。我们通过降级 schema 并提高迁移到 spec/malli 的优先级来解决了我们的问题,但我只是想加上一条注释,即野外的有些库使用 `identity` 作为多方法分派函数,这似乎是一个我们应该注意的红牌。
by
我不知道这件事,ring-swagger 是我造成的。很高兴在那里修复它。
...