本篇主要介绍一下分布式环境中搜索的两阶段执行过程。java
回顾咱们以前的CRUD操做,由于只对单个文档进行处理,文档的惟一性很容易肯定,而且很容易知道是此文档在哪一个node,哪一个shard中。node
但搜索比CRUD复杂,符合搜索条件的文档,可能散落在各个node、各个shard中,咱们须要找到匹配的文档,而且把从各个node,各个shard返回的结果进行汇总、排序,组成一个最终的结果排序列表,才算完成一个搜索过程。咱们将按两阶段的方式对这个过程进行讲解。算法
假定咱们的ES集群有三个node,number_of_primary_shards为3,replica shard为1,咱们执行一个这样的查询请求:数据库
GET /music/children/_search { "from": 980, "size": 20 }
查询阶段的过程示意图以下:服务器
补充说明:微信
在完成了查询阶段后,此时Coordinate Node已经获得查询的列表,但列表内的元素只有文档ID和_score信息,并没有实际的_source内容,取回阶段就是根据文档ID,取到完整的文档对象的过程。以下图所示:网络
前面几篇有提到deep paging的问题,咱们在这里又复习一遍,使用from和size进行分页时,传递信息给Coordinate Node的每一个shard,都建立了一个from + size长度的队列,而且Coordinate Node须要对全部传过来的数据进行排序,工做量为number_of_shards * (from + size),而后从里面挑出size数量的文档,若是from值特别大,那么会带来极大的硬件资源浪费,鉴于此缘由,强烈建议不要使用深分页。session
不过深分页操做不多符合人的行为,翻几页还看不到想要的结果,人的第一反应是换一个搜索条件,只有机器人或爬虫才这么不知疲倦地一直翻页直到服务器崩溃。架构
查询时使用preference参数,能够影响哪些shard能够用来执行搜索操做,6.1.0版本后,许多参数值已声明为弃用,咱们挑几个目前还在使用的简单介绍一下:并发
假如两个文档有相同的字段值,而且时间戳也同样,若是按时间戳字段来排序,因为请求是在全部可用的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容许处理数据的最大时间,时间一到,执行关门动做,能有多少数据返回多少数据,剩下的不要了,这样能够确保集群是稳定运行的,以下图所示:
在设计大规模数据搜索时,咱们为了实现数据集中性,索引时会按必定规则将数据进行存储,好比订单数据,咱们会按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精准度。
若是咱们要把大批量的数据从ES集群中取出,用来执行一些计算,一次性取完确定不合适,IO压力过大,性能容易出问题,分页查询又容易形成deep paging的问题。通常推荐使用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架构社区微信群共同探讨技术