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` 并避免使用多方法和协议?
by
我的担忧,不仅仅是由于开发者错误导致的意外泄露,而是如果允许潜在恶意或高度随机的用户输入击中多方法,这实际上可能引发真正的问题。

我相信大多数Clojure程序员都非常小心,如果明确说明正在进行的缓存,那么在拥有默认方法或找到关闭多方法输入的其他方法时,人们会三思而后行,但并不明确。我已经专业使用Clojure很多年了,我不知道默认匹配是被缓存的。

例如,memoize将引发相同的问题,但我觉得在那个情况下(你可能会说这是显然的)比多方法的情况更明显。多方法对我来说有点太微妙了。

我不知道是否有解决方案,当然,速度很重要,在几乎所有情况下,我相信都是好的。
by
假设这可能是可能的,那么使用clojure.core.cache中的LRU或其他缓存的defmulti选项能否解决这个问题?(“慢”未缓存的multimethod查找是否比安全限定的缓存开销更大?)
by
需要注意的是,这种缓存发生在分派函数的输出上,而不是多方法或分派函数的输入上。最常用的分派函数类似于`type`、`class`或用于从映射中提取字段的keyword。虽然这些都是无界的,但它们通常比域更有限。我不记得有人在其他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来分派,然后让:default来处理其他情况,或者一个(type)字段,它的值是基于HTML表单选择的,:default可以是为无效选择提供的回退。
为了列表中再增加一个可能让人困扰的意外问题案例。近期升级了一些依赖后,一个服务出现了内存泄漏。该问题是由plumatic schema、ring swagger和compojure api结合使用引起的。在这一提交中,schema被修改为返回匿名函数:[https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0](https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0),而ring swagger使用了`identity`作为调度函数:[https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178](https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178)。因此,当在compojure api调用中使用映射作为查询参数时,每请求一次都会在`ring.swagger.coerce/custom-matcher`(和`time-matcher`)方法缓存中放置匿名函数。我们通过降级schema并提高迁移到spec/malli的优先级来解决这个问题。但我想补充一点,有些在野的库使用了`identity`作为多重方法调度函数,这应该是一个我们应该注意的红旗。
我对这个不是很清楚,ring-swagger问题是我干的。我很乐意修复那里的问题。
...