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

欢迎!请查阅关于页面以了解有关此工作方式的一些更多信息。

+13
Multimethods

一种简单的复现方式

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`并避免多方法及协议?
by
我的关注点不是开发者错误导致的意外泄露,而是用户输入(可能是有恶意的或高度随机的)如果被允许触发多方法,可能会引发真实问题。

我相信大多数Clojure程序员都非常谨慎,如果明确指出正在执行缓存,那么人在思考是否拥有:默认方法或找到其他方式将输入关闭给多方法时会三思而后行——但并不明确。我已专业使用Clojure多年了,不知道:默认匹配是被缓存的。

例如,memoize会导致相同类型的问题,但在那种情况下更清楚(你可以说非常明显),而对于多方法来说,它对我而言过于微妙。

我不知道是否有解决方案,但我肯定速度很重要,而且几乎所有时候我都确信这是可以的。
by
假设这是可能的,使用clojure.core.cache中的LRU或其他缓存作为defmulti选项能否解决问题?(“慢”的无缓存多方法搜索速度是否比安全受限缓存的开销要慢?)
by
需要注意的是,这种缓存发生在派发函数的输出上,而不是多方法或派发函数的输入上。最常用的派发函数是像`type`、`class`或用于从映射中提取字段的键之类的函数。虽然它们都有未限定的范围,但它们通常比领域更受限制。我不记得在更改后的5多年里,有人报告过这个问题。协议也是如此,使用缓存(每个调用点),实际上(除了ns/var表之外),这些是Clojure运行时中的两个主要状态性事物。

你可以添加许多潜在的控件,这与memoize非常相似。在后者中,我们将其推到core.memoize/core.cache中,并保留了核心中的简单版本。我不支持或反对任何这些,需要一些评估。创建了https://clojure.atlassian.net/browse/CLJ-2626
完全有道理,认为不应该有太多不同的分发值。但有时候分发值并不是开发者所预期的。

我在生产环境中意外遇到内存泄露,这是在使用关键字作为分发函数时发生的。我的意图是按照第一个参数中字段的值进行分发,但错误地处理了关键字,将其作为函数对待第二个参数作为分发值。

下面是一个简单的示例

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

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

请注意,我的有误的代码并没有引起任何功能性异常行为,只是由于多方法的缓存机制导致的内存泄露。
尽管它适用于分发结果,但结果可能作为外部世界或无法认为有界(除非程序员已确认)的其他程序输入。我认为这与协议缓存因类型比价值更不同,想象一个按计数分发多方法的,可以为0、1、2 specializes,然后为其他情况留出:default,或者是一个在HTML表单上选定的:value字段的类型,:default可以作为无效选择的回退。
只是再加一个可能导致人们出现问题的小例子到问题列表中。最近一个服务出现内存泄漏,是因为升级了一些依赖项引起的。问题的组合是 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 时,如果查询参数中使用了 map,最终会在每个请求中把匿名函数放入 `ring.swagger.coerce/custom-matcher`(以及 `time-matcher`)的方法缓存中。我们通过降级 schema 并提高迁移到 spec/malli 的优先级解决了这个问题,但我只是想加一条备注,提示一些库在野外使用 `identity` 作为多方法调度函数,这似乎是一个我们应该注意的红旗。
by
我之前不知道这个情况,ring-swagger 是我自己做的。很高兴去修复那里的问题。
...