概要
本篇主要介绍一下分布式环境中搜索的两阶段执行过程.
两阶段搜索过程
回顾我们之前的 CRUD 操作, 因为只对单个文档进行处理, 文档的唯一性很容易确定, 并且很容易知道是此文档在哪个 node, 哪个 shard 中.
但搜索比 CRUD 复杂, 符合搜索条件的文档, 可能散落在各个 node, 各个 shard 中, 我们需要找到匹配的文档, 并且把从各个 node, 各个 shard 返回的结果进行汇总, 排序, 组成一个最终的结果排序列表, 才算完成一个搜索过程. 我们将按两阶段的方式对这个过程进行讲解.
查询阶段
假定我们的 ES 集群有三个 node,number_of_primary_shards 为 3,replica shard 为 1, 我们执行一个这样的查询请求:
- GET /music/children/_search
- {
- "from": 980,
- "size": 20
- }
查询阶段的过程示意图如下:
Java 客户端发起查询请求, 接受请求的 node-1 成为 Coordinate Node(协调者), 该 node 会创建一个 priority queue, 长度为 from + size 即 1000.
Coordinate Node 将请求分发到所有的 primary shard 或 replica shard 中, 每个 shard 在本地创建一个同样大小的 priority queue, 长度也为 from + size, 用于存储该 shard 执行查询的结果.
每个 shard 将各自 priority queue 的元素返回给 Coordinate Node, 元素内只包含文档的 ID 和排序值(如_score),Coordinate Node 将合并所有的元素到自己的 priority queue 中, 并完成排序动作, 最终根据 from,size 值对结果进行截取.
补充说明:
哪个 node 接收客户端的请求, 该 node 就会成为 Coordinate Node.
Coordinate Node 转发请求时, 会根据负载均衡算法分配到同一分片的 primary shard 或 replica shard 上, 为什么说 replica 值设置得大一些可以增加系统吞吐量的原理就在这里, Coordinate Node 的查询请求负载均衡算法会轮询所有的可用 shard, 并发场景时就会有更多的硬件资源 (CPU, 内存, IO) 会参与其中, 系统整体的吞吐量就能提升.
此查询过程 Coordinate Node 得到是轻量级的元素信息, 只包含文档 ID 和_score 这些信息, 这样可以减轻网络负载, 因为分页过程中, 大部分的数据是会丢弃掉的.
取回阶段
在完成了查询阶段后, 此时 Coordinate Node 已经得到查询的列表, 但列表内的元素只有文档 ID 和_score 信息, 并无实际的_source 内容, 取回阶段就是根据文档 ID, 取到完整的文档对象的过程. 如下图所示:
Coordinate Node 根据 from,size 信息截取要取回文档的 ID, 如{"from": 980, "size": 20}, 则取第 981 到第 1000 这 20 条数据, 其余丢弃, from/size 为空则默认取前 10 条, 向其他 shard 发出 mget 请求.
shard 接收到请求后, 根据_source 参数 (可选) 加载文档信息, 返回给 Coordinate Node.
一旦所有的 shard 都返回了结果, Coordinate Node 将结果返回给客户端.
前面几篇有提到 deep paging 的问题, 我们在这里又复习一遍, 使用 from 和 size 进行分页时, 传递信息给 Coordinate Node 的每个 shard, 都创建了一个 from + size 长度的队列, 并且 Coordinate Node 需要对所有传过来的数据进行排序, 工作量为 number_of_shards * (from + size), 然后从里面挑出 size 数量的文档, 如果 from 值特别大, 那么会带来极大的硬件资源浪费, 鉴于此原因, 强烈建议不要使用深分页.
不过深分页操作很少符合人的行为, 翻几页还看不到想要的结果, 人的第一反应是换一个搜索条件, 只有机器人或爬虫才这么不知疲倦地一直翻页直到服务器崩溃.
preference 设置
查询时使用 preference 参数, 可以影响哪些 shard 可以用来执行搜索操作, 6.1.0 版本后, 许多参数值已声明为弃用, 我们挑几个目前还在使用的简单介绍一下:
_only_local: 只搜索当前 node 中的 shard
_local: 优先搜索当前 node 中的 shard, 搜不到再去其他的 shard
_prefer_nodes:abc,xyz: 优先从指定的 abc/xyz 节点上搜索, 如果两个节点都有存在数据的 shard, 随机从里面挑一个节点执行搜索
_only_nodes:abc,xyz,...: 只在符合通配 abc,xyz 名称的节点上搜索, 如果多个节点都有存在数据的 shard, 随机从里面挑一个节点执行搜索
_shards:2,3: 指定 shard 进行搜索, 这个条件如与其他条件搭配使用, 此条件要写在前面, 如_shards:2,3|_local
自定义字符串: 一般用 sessionid 或 userid
bouncing results 问题
假如两个文档有相同的字段值, 并且时间戳也一样, 如果按时间戳字段来排序, 由于请求是在所有可用的 shard 上轮询的, 可能存在一种情况: 这两个文档记录在不同的 shard 之间保存的顺序不相同. 结果就是同一个条件的查询, 如果执行多次, 分配在 primary shard 得到的是一种顺序, 分配在 replica shard 又是另一个顺序, 这个就是所谓的 bouncing results 问题.
如何避免: 让同一个用户始终使用同一个 shard, 就可以避免这种问题, 常见的做法是 preference 设置为 sessionid 或 userid, 如:
- GET /music/children/_search?preference=10086
- {
- "from": 980,
- "size": 20
- }
超时问题
我们回顾查询阶段和取回阶段, 必须所有的操作都完成了, 才给客户端返回结果, 如果中途有 shard 在执行特别重的任务, 导致查询很慢怎么办? 会拖慢整个集群吗?
如果是高并发场景, 那极有可能, 因为某一个节点慢, 整个查询请求堆积, 拖死集群都有可能.
为了防止这一情况, 我们使用 timeout 参数, 告诉 shard 允许处理数据的最大时间, 时间一到, 执行关门动作, 能有多少数据返回多少数据, 剩下的不要了, 这样可以确保集群是稳定运行的, 如下图所示:
routing
在设计大规模数据搜索时, 我们为了实现数据集中性, 索引时会按一定规则将数据进行存储, 比如订单数据, 我们会按 userid 为 route key, 每个 userid 的订单数据, 都放在同一个 shard 上, 既然存储时使用了 route key, 那么搜索时同样使用 route key, 可以让查询只搜索相关的 shard, 如:
- GET /music/children/_search?routing=10086
- {
- "from": 980,
- "size": 20
- }
这样由于精准到具体的 shard, 可以极大的缩小搜索范围, 数据量越大, 效果越明显.
搜索类型
默认的搜索类型是 query_then_fetch, 我们还可以选择 dfs_query_then_fetch, 这个有预查询阶段, 可以从所有相关 shard 中获取词频来计算全局词频, 可以提升 revelance sort 精准度.
scroll 游标查询
如果我们要把大批量的数据从 ES 集群中取出, 用来执行一些计算, 一次性取完肯定不合适, IO 压力过大, 性能容易出问题, 分页查询又容易造成 deep paging 的问题. 一般推荐使用 scroll 查询, 一批一批的查, 直到所有数据都查询完.
原理
scroll 查询会先做查询初始化, 然后再批量地拉取结果, 有点像数据库的 cursor.
scroll 查询会取某个时间点的快照数据, 查询初始化后索引上的数据发生了变化, 快照数据还是原来的, 有点像数据库的索引视图.
scroll 查询用字段_doc 排序, 去掉了全局排序, 性能比较高.
scroll 查询要设置过期时间, 每次搜索在这个时间内完成即可.
示例
我们假定每次取 10 条数据, 时间窗口为 1 秒
请求如下:
- GET /music/children/_search?scroll=1s
- {
- "size": 10
- }
响应如下(结果有删减):
- {
- "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABJQFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASUhZBMXMxdXVzN1RwdURTaVQ0eEZMT29RAAAAAAAAElMWQTFzMXV1czdUcHVEU2lUNHhGTE9vUQAAAAAAABJUFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASURZBMXMxdXVzN1RwdURTaVQ0eEZMT29R",
- "took": 2,
- "timed_out": false,
- "_shards": {
- "total": 5,
- "successful": 5,
- "skipped": 0,
- "failed": 0
- },
- "hits": {
- "total": 4,
- "max_score": 1,
- "hits": [
- {
- "_index": "music",
- "_type": "children",
- "_id": "2",
- "_score": 1,
- "_source": {
- "name": "wake me, shark me",
- "content": "don't let me sleep too late, gonna get up brightly early in the morning",
- "language": "english",
- "length": "55",
- "likes": 0,
- "author": "John Smith"
- }
- }
- ]
- }
- }
注意那个 scroll_id, 下次再查询时, 只要带上这个就行了
- GET /_search/scroll
- {
- "scroll": "1s",
- "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABJQFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASUhZBMXMxdXVzN1RwdURTaVQ0eEZMT29RAAAAAAAAElMWQTFzMXV1czdUcHVEU2lUNHhGTE9vUQAAAAAAABJUFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASURZBMXMxdXVzN1RwdURTaVQ0eEZMT29R"
- }
每次的查询, 都把最新的 scroll_id 带上, 直到数据查询完成为止.
scroll 查询看起来像分页, 但使用场景不一样, 分页主要是按页展示数据, 主要受众是人, scroll 一批一批的获取数据, 主要受众一般是数据分析的系统, 是给系统用的.
性能也不同, 前面我们了解后, 分页查询随着页数的加深, 压力越来越大, 而 scroll 是基于_doc 排序的数据处理, 特别适用于大批量数据的获取分析.
小结
本篇详细介绍了查询的两阶段过程, 以及能够影响查询行为的一些参数设置, 历经多个版本迭代, 有些 preference 参数已经不用了, 了解一下就行, 另外介绍了 bouncing results 产生的原理及规避办法, 最后介绍了一下大批量数据查询利器 scroll 的简单用法.
专注 Java 高并发, 分布式架构, 更多技术干货分享与心得, 请关注公众号: Java 架构社区
可以扫左边二维码添加好友, 邀请你加入 Java 架构社区微信群共同探讨技术
来源: https://www.cnblogs.com/huangying2124/p/12208276.html