背景
最近我们在公司内尝试用ES替换老旧的Solr, 在性能对比测试的环节, 发现ES竟然比Solr慢了非常多, 响应时间是Solr的两三倍, 然后开始各种排查, 最后发现ES的响应时间竟然随着request.size的增加呈线性增加, 这说明大部分时间都耗在了获取返回字段上面. 而我们目前在召回时并未获取很多字段, 只获取了UID(我们自己定义的一个基于docvalues列存的字段)和score. 按照ES的query-then-fetch召回模式来说, score应该是在query阶段生成, 在fetch阶段应该只需要读取UID, 而UID是基于列存的, 没有理由会随着request.size的增加而线性增长.
因此有一个初步的猜想, 就是执行fetch阶段时可能不符合我们的预期.
阅读官方文档
让我们来看看官方文档里提供的获取字段的几种方式.
- _source: 需要把doc的整个原始文档解开, 然后取需要的字段.
- fields: 类似于_source, 只是取出field后会按照mapping来解析格式化.
- docvalue_fields: 可以用来取支持docvalue的字段(不需要显示指定, 支持的数据类型默认会存docvalue), 避免读取整个_source.
- 支持keyword, 数字, date, 不支持text.
- 不支持嵌套对象.
- stored_fields: 可以用来取支持store的字段(需要显示指定store=true), 一般不推荐使用, 比起这个方式更推荐用_source.
- 只支持显示指定store=true的字段, 很不方便.
- 不支持嵌套对象
- 可以完全禁用storefields: `"stored_fields": "_none"`, 禁用的话_source也不能访问了, 因为_source本质也是一个store field.
- script_fields: 通过script获取自定义字段. 可以通过读取_source和docvalue两种方式获取字段.
- 通过_source获取:
params['_source']['my_field']
. - 通过docvalue获取:
doc['my_field'].value
.
- 通过_source获取:
测试不同的获取字段方式
测试: 在返回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字段.
目前还有两个疑问:
- 为什么当设置了
"_source":false
的时候性能无明显提升呢? 难道即便这样设置, ES依然会从硬盘上读取_source吗? 这听起来不是很合理啊. - 因为是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工具查看:
通过查看我们得知, 这两个字段是仅索引的, 既没有存docvalues也没有存stored. 那么ES是如何读取的呢? 答案是通过fielddata cache. 第一次试图召回_id字段的时候, ES会根据其倒排索引结构, 在堆内存中构建fielddata cache并缓存. fielddata cache就是把倒排索引结构反转为正排索引, 这样一来就相当于在内存中构建了_id字段的列存. 缺陷是第一次请求因为要构建fielddata cache会慢.
因此仅仅设置"_source":false
是不够的, 如果不需要召回_id和_routing的话, 应该设置"stored_fields": "_none_"
. 而且官方文档其实也指出了这一点:
因为是query_then_fetch的模式, 这样在fetch阶段, 每个shard需要获取字段的文档数应该接近size/shard_size, 假设有20个shard, 那么平均每个shard只需要获取4000/20=200个文档, 并且多个shard是并发执行的, 这个过程会增加100ms那么多时间吗?
这个问题暂时还没搞清楚.