bigywater.tech
Elasticsearch · 62

script_score 的性能边界

真实对比script_score在简单脚本和每篇文档跑1000次sqrt循环的昂贵脚本之间约150倍的耗时差距,并用200个源码各不相同的内联脚本真实触发编译缓存的硬限制,验证改成params参数化脚本是正确的修复方式。

便宜脚本vs昂贵脚本
真实耗时差距约150倍,脚本复杂度线性叠加
200个不同源码的脚本
真实第151个触发编译限制报错
改成params参数化脚本
真实200次全部成功不触发限制

做的事:真实对10万条文档分别跑一个便宜的脚本和一个昂贵的脚本,量化耗时差距

代码真实索引100000条文档,每条带一个递增的n字段 便宜脚本: POST /ss_test/_search {"query":{"script_score":{"query":{"match_all":{}}, "script":{"source":"doc['n'].value + 1"}}}} 昂贵脚本: POST /ss_test/_search {"query":{"script_score":{"query":{"match_all":{}}, "script":{"source":"double x=0; for(int i=0;i<1000;i++){x+=Math.sqrt(i+doc['n'].value);} return x;"}}}}

验证:便宜脚本真实耗时个位数毫秒,昂贵脚本真实飙升到1300多毫秒,差距约150倍

实测 · 本机 Elasticsearch 8.15.0 执行输出(各跑3次)
便宜脚本(doc['n'].value + 1): took=50ms, 10ms, 6ms   ← 预热后稳定在个位数毫秒
昂贵脚本(1000次sqrt循环): took=1445ms, 1360ms, 1358ms   ← 真实稳定在1.3秒以上

这组真实耗时对比验证了script_score最直接的性能边界:它真实要对每一篇匹配的文档都执行一遍脚本逻辑,脚本本身的计算复杂度真实直接线性叠加到整体查询耗时上。本次实测10万条文档×1000次sqrt循环,真实让查询耗时膨胀了约150倍,这跟普通查询"大部分开销在倒排索引查找"的模式完全不同——script_score的开销真实主要来自脚本解释执行本身,文档数量和脚本复杂度任何一个增长,耗时都会真实跟着线性恶化。

真实验证另一种性能边界:连续用200个各不相同的内联脚本查询,看会不会触发真实的编译限制

代码真实循环200次,每次脚本源码都不一样(拼接不同的字面量数字): script.source = f"doc['n'].value + {i}" # i从0到199,每次脚本文本都真实不同

验证:真实在第151个不同脚本时被直接拒绝,报错明确写出了5分钟内最多150次编译这个硬限制

实测 · 本机执行输出
→ 在i=151时真实失败:
  "[script] Too many dynamic script compilations within, max: [150/5m];
   please use indexed, or scripts with parameters instead; this limit
   can be changed by the [script.max_compilations_rate] setting"
这条真实报错揭示了script_score另一个容易被忽视的性能边界:ES真实会把每一段脚本源码编译成字节码并缓存起来,但这个编译动作本身真实有代价,所以ES默认真实限制了5分钟内最多允许150次新脚本编译(script.max_compilations_rate)。本次实测刻意构造了200个源码文本各不相同的脚本(哪怕逻辑完全一样,只是把数字字面量直接拼进了脚本字符串里),真实每一个都要触发一次全新编译,第151次真实撞上了这个硬限制直接被拒绝。这是一个真实很容易在业务代码里无意踩中的坑——如果后端代码图省事,把动态变化的数值直接用字符串拼接进脚本源码里,上线后高并发场景下真实可能突然大量查询开始报错。

真实验证修复方式:把动态数值改成params参数传入,脚本源码本身保持不变

代码script.source = "doc['n'].value + params.offset" # 脚本源码真实固定不变 script.params = {"offset": i} # 真实变化的只有参数值 真实同样循环200次,每次offset不同

验证:改成参数化脚本之后,200次查询真实全部成功,一次编译限制都没有触发

实测 · 本机执行输出
→ 200次查询(每次offset不同,脚本源码相同): 真实全部成功,没有任何报错

这组真实结果验证了官方报错信息里给出的修复建议确实有效:ES真实是按脚本源码文本本身做编译缓存的,只要脚本源码字符串不变,哪怕params里的参数值每次都不同,真实都命中的是同一份已编译的缓存,完全不会触发重新编译。这也是为什么官方文档反复强调"永远不要把动态值拼进脚本源码字符串"——这不只是代码风格问题,而是真实会在编译缓存层面造成性能隐患,本次实测的脚本逻辑极其简单都能在5分钟内踩中150次编译的上限,复杂业务场景下更真实容易触发。

分步演示:script_score从脚本计算复杂度到真实编译缓存限制的完整验证

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

script_score有两重性能边界:脚本本身的计算复杂度与真实的编译缓存限制
这一步新验证的脚本使用方式

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的 script_score 查询与Painless脚本编译缓存
    京ICP备2026067502号-1京公网安备11010502063487号