尝试修复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的方式传递,官方的解释:
官方文档中提到一个情况: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 永久:这些设置永久保存,除非再次被手动修改。是将修改持久化到文件中,重启之后也不影响。