尝试修复Elasticsearch中出现的“Too many dynamic script compilations”错误

背景

有个业务接口使用es template模板搜索,模板比较复杂。同时在代码中使用java的velocity模板引擎来解析填充搜索关键词。最早使用Elasticsearch 5没有出现动态编译报错,在升级到Elasticsearch 7.10.2版本后,会偶发出现超过动态编译数量限制的错误,如下:

{
"type": "circuit_breaking_exception",
"reason": "[script] Too many dynamic script compilations within, max: [75/5m];
please use indexed, or scripts with parameters instead;
this limit can be changed by the [script.context.template.max_compilations_rate] setting",
"bytes_wanted": 0,
"bytes_limit": 0,
"durability": "TRANSIENT"
}

Elasticsearch在执行查询时达到脚本编译限制,抛出circuit_breaking_exception错误,并且查询不会运行。这个错误通常是由于在短时间内编译了太多的动态脚本而导致的。需要优化代码以减少编译次数,或者增加编译次数的限制。可以尝试将动态脚本缓存起来,以便在需要时可以重复使用,从而减少编译次数。

第一次尝试:优化脚本

先修复脚本,避免使用大量的重复动态脚本编译操作。尽量使用已经编译过的脚本或使用带参数的脚本,以减少编译次数。比如在脚本中用到的值尽量用 params的方式传递,官方的解释

image

官方文档中提到一个情况:Elasticsearch遇到一个新脚本时,将对其进行编译并将编译后的版本存储在缓存中。编译可能是一个繁重的过程。如果我们有相同逻辑的脚本只是参数不一样的时候,应以参数形式传递变量,而不是将值硬编码到脚本本身中。

有这么一个例子:

{
"lang": "painless",
"source": "long degree =doc['taobao_degree'].value;if(doc['taobao_degree'].value == 0L) degree = 1;else return 10 * 100"
}

优化后:

{
"lang": "painless",
"source": "long degree =doc['taobao_degree'].value;if(doc['taobao_degree'].value == 0L) degree = params.a;else return params.b * params.c",
"params": {
"a": 1,
"b": 10,
"c": 100
}
}

优化初期,报错减少。在重新上线几天后又开始报错

第二次尝试:调整max_compilations_rate

注意,如果设置太大,会影响集群性能,在最坏的情况下甚至可能导致集群崩溃。要根据业务的实际需求,进行优化和调整

先查看节点的脚本缓存统计信息:

GET /_nodes/stats?metric=script&filter_path=nodes.*.script.* 

返回结果中的数据是该节点自上次重启后统计的:

{
"nodes" : {
"78R-ajd7Rc-lFoM6tyRufA" : {
"script" : {
"compilations" : 2304,
"cache_evictions" : 2162,
"compilation_limit_triggered" : 0
}
},
"S7lQHk0NRPOAyeD6dCQOog" : {
"script" : {
"compilations" : 2288,
"cache_evictions" : 2172,
"compilation_limit_triggered" : 0
}
},
"ags0eEdjQ4--p0J2-vxSmw" : {
"script" : {
"compilations" : 2368,
"cache_evictions" : 2214,
"compilation_limit_triggered" : 0
}
}
}
}

细看一下返回的信息:

官方解释

  • compilations:脚本引擎在执行脚本时进行的编译次数。这个指标可以了解脚本执行的活跃程度,以及是否需要优化脚本以减少编译开销。
  • cache_evictions:查询缓存被驱逐(evicted)的次数。当缓存达到上限时,Elasticsearch 会根据最近使用频率等因素驱逐部分缓存项。这个指标可以了解缓存利用率和缓存策略是否需要调整。
  • compilation_limit_triggered:脚本编译次数超过了配置的限制次数。Elasticsearch 对脚本编译次数设有上限,以防止过多的编译开销影响系统性能。当编译次数超过限制时,Elasticsearch 会拒绝执行该脚本,并记录一个 compilation_limit_triggered 事件。这个指标可以帮助了解是否需要调整脚本编译的限制,或者优化脚本以减少编译次数。

如果compilations和cache_evictions 的数值很大或者不断增加,这可能表明缓存正在不断变化,说明节点的缓存太小;

如果compilation_limit_triggered的数值很大,说明脚本动态编译过于频繁,或者脚本太复杂,编译难度负载太高

7.8之前版本

Elasticsearch 7.8 及更早版本每分钟最多编译 15 个内联脚本。然后,这些编译后的脚本将存储在脚本缓存中,默认情况下最多可以存储 100 个脚本

可以动态设置script.max_compilations_rate:

PUT _cluster/settings
{
  "persistent": {
    "script.max_compilations_rate": "250/5m"
  }
}

也可以在elasticsearch.yml中配置script.cache.max_size: 250(需要集群重启才能生效)。但是这2种配置方式在7.9版本之后就失效了

7.9之后版本

在7.9之后直接执行动态设置语句,会报错:Context cache settings [script.context.template.max_compilations_rate] requires [script.max_compilations_rate] to be [use-context],要先开启上下文。从 7.9 开始,默认情况下,脚本根据执行的上下文进行存储。上下文允许为 Elasticsearch 可能执行的不同类型的脚本设置不同的默认值。默认会启用上下文。但是,如果未启用上下文(由于某种原因),则可以使用以下命令启用:

PUT _cluster/settings
{
    "persistent": {
        "script.max_compilations_rate": "use-context"
    }
}

查看当前集群的脚本配置:

GET /_nodes/stats?filter_path=nodes.*.script_cache.contexts

返回结果:

{
"nodes": {
"78R-ajd7Rc-lFoM6tyRufA": {
"script_cache": {
"contexts": [
{
"context": "aggs",
"compilations": 0,
"cache_evictions": 0,
"compilation_limit_triggered": 0
},
{
"context": "analysis",
"compilations": 0,
"cache_evictions": 0,
"compilation_limit_triggered": 0
},
{
"context": "bucket_aggregation",
"compilations": 0,
"cache_evictions": 0,
"compilation_limit_triggered": 0
},
{
"context": "field",
"compilations": 0,
"cache_evictions": 0,
"compilation_limit_triggered": 0
},
{
"context": "filter",
"compilations": 8,
"cache_evictions": 0,
"compilation_limit_triggered": 0
},
.....
]
}
}
}
}

返回结果中就包含许多可用的上下文配置,比如“template”、“aggs”、“filter”等等,其中的参数含义和7.8版本之前都一样。默认情况下每 5 分钟最多可以编译 75 个脚本。具体的脚本说明可以看看官方文档

动态设置,根据报错信息,是template编译超出限制:

PUT /_cluster/settings
{
  "persistent": {
    "script.context.template.max_compilations_rate": "150/5m"
  }
}

同样,在elasticsearch.yml中配置script.context.$CONTEXT.cache_max_size=150,这里$CONTEXT=template

最后

顺便提一下在动态设置时有用到的persistent和transient的区别:

  • transient 临时:这些设置在集群重启之前一直会生效。一旦整个集群重启,这些设置就会被清除。
  • persistent 永久:这些设置永久保存,除非再次被手动修改。是将修改持久化到文件中,重启之后也不影响。