修改ES返回字段方式提升性能

2022-04-13 18:09:30 浏览数 (1)

背景

最近我们在公司内尝试用ES替换老旧的Solr, 在性能对比测试的环节, 发现ES竟然比Solr慢了非常多, 响应时间是Solr的两三倍, 然后开始各种排查, 最后发现ES的响应时间竟然随着request.size的增加呈线性增加, 这说明大部分时间都耗在了获取返回字段上面. 而我们目前在召回时并未获取很多字段, 只获取了UID(我们自己定义的一个基于docvalues列存的字段)和score. 按照ES的query-then-fetch召回模式来说, score应该是在query阶段生成, 在fetch阶段应该只需要读取UID, 而UID是基于列存的, 没有理由会随着request.size的增加而线性增长.

因此有一个初步的猜想, 就是执行fetch阶段时可能不符合我们的预期.

阅读官方文档

让我们来看看官方文档里提供的获取字段的几种方式.

  1. _source: 需要把doc的整个原始文档解开, 然后取需要的字段.
  2. fields: 类似于_source, 只是取出field后会按照mapping来解析格式化.
  3. docvalue_fields: 可以用来取支持docvalue的字段(不需要显示指定, 支持的数据类型默认会存docvalue), 避免读取整个_source.
    1. 支持keyword, 数字, date, 不支持text.
    2. 不支持嵌套对象.
  4. stored_fields: 可以用来取支持store的字段(需要显示指定store=true), 一般不推荐使用, 比起这个方式更推荐用_source.
    1. 只支持显示指定store=true的字段, 很不方便.
    2. 不支持嵌套对象
    3. 可以完全禁用storefields: `"stored_fields": "_none"`, 禁用的话_source也不能访问了, 因为_source本质也是一个store field.
  5. script_fields: 通过script获取自定义字段. 可以通过读取_source和docvalue两种方式获取字段.
    1. 通过_source获取: params['_source']['my_field'].
    2. 通过docvalue获取: doc['my_field'].value.

测试不同的获取字段方式

测试: 在返回4000条文档的UID,score属性的测试中, 配置不同的返回字段参数的响应时间如下:

代码语言:txt复制
"_source":{
	"include":["UID"]
},

120ms

代码语言:txt复制
"fields":["UID"],
"_source":false,

110ms

代码语言:txt复制
"docvalue_fields":["UID"],
"_source":false,

110ms

代码语言:txt复制
"docvalue_fields":["UID"],
"stored_fields": "_none_",
"_source":false,

20ms

结论

很显然, 使用"stored_fields": "_none_"的响应时间相比简单的使用_source要减少100ms, 性能要提升5倍多.

疑问

根据官方文档的说法, "stored_fields": "_none_" 是完全禁掉了包括_source在内的store字段.

目前还有两个疑问:

  1. 为什么当设置了"_source":false的时候性能无明显提升呢? 难道即便这样设置, ES依然会从硬盘上读取_source吗? 这听起来不是很合理啊.
  2. 因为是query_then_fetch的模式, 这样在fetch阶段, 每个shard需要获取字段的文档数应该接近size/shard_size, 假设有20个shard, 那么平均每个shard只需要获取4000/20=200个文档, 并且多个shard是并发执行的, 这个过程会增加100ms那么多时间吗?

阅读源码解释疑问

为什么当设置了"_source":false的时候性能无明显提升呢? 难道即便这样设置, ES依然会从硬盘上读取_source吗? 这听起来不是很合理啊.

通过阅读源码知道, 当设置了"_source":false的时候, ES确实没有读取_source, 但是会默认读取两个字段: _id和_routing, 这两个字段是ES内置的, 正常情况下无法查看其字段类型, 但是我们可以通过Luke工具查看:

luke.pngluke.png

通过查看我们得知, 这两个字段是仅索引的, 既没有存docvalues也没有存stored. 那么ES是如何读取的呢? 答案是通过fielddata cache. 第一次试图召回_id字段的时候, ES会根据其倒排索引结构, 在堆内存中构建fielddata cache并缓存. fielddata cache就是把倒排索引结构反转为正排索引, 这样一来就相当于在内存中构建了_id字段的列存. 缺陷是第一次请求因为要构建fielddata cache会慢.

因此仅仅设置"_source":false是不够的, 如果不需要召回_id和_routing的话, 应该设置"stored_fields": "_none_". 而且官方文档其实也指出了这一点:

doc.pngdoc.png

因为是query_then_fetch的模式, 这样在fetch阶段, 每个shard需要获取字段的文档数应该接近size/shard_size, 假设有20个shard, 那么平均每个shard只需要获取4000/20=200个文档, 并且多个shard是并发执行的, 这个过程会增加100ms那么多时间吗?

这个问题暂时还没搞清楚.

0 人点赞