前言

一条数据是如何落地到对应的shard上的?

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个算法决定的:

shard_num = hash(_routing) % num_primary_shards

其中 _routing 是一个可变值,默认是文档的 _id 的值 ,也可以设置成一个自定义的值。 _routing 通过 hash 函数生成一个数字,然后这个数字再除以 num_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

路由机制

假设你有一个100个分片的索引。当一个请求在集群上执行时会发生什么呢?

1. 这个搜索的请求会被发送到一个节点
2. 接收到这个请求的节点,将这个查询广播到这个索引的每个分片上(可能是主分片,也可能是复本分片)
3. 每个分片执行这个搜索查询并返回结果
4. 结果在通道节点上合并、排序并返回给用户

因为默认情况下,Elasticsearch使用文档的ID(类似于关系数据库中的自增ID),如果插入数据量比较大,文档会平均的分布于所有的分片上,这导致了Elasticsearch不能确定文档的位置,

所以它必须将这个请求广播到所有的N个分片上去执行 这种操作会给集群带来负担,增大了网络的开销;


自定义路由

自定义路由的方式非常简单,只需要在插入数据的时候指定路由的key即可。虽然使用简单,但有许多的细节需要注意。我们从一个例子看起(注:本文关于ES的命令都是在Kibana dev tool中执行的):

// 步骤1:先创建一个名为route_test的索引,该索引有3个shard,0个副本
PUT route_test/
{
"settings": {
"number_of_shards": ,
"number_of_replicas":
}
} // 步骤2:查看shard
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED 230b 172.19.0.2 es7_02
route_test p STARTED 230b 172.19.0.5 es7_01 // 步骤3:插入第1条数据
PUT route_test/_doc/a?refresh
{
"data": "A"
} // 步骤4:查看shard
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED 230b 172.19.0.2 es7_02
route_test p STARTED .3kb 172.19.0.5 es7_01 // 步骤5:插入第2条数据
PUT route_test/_doc/b?refresh
{
"data": "B"
} // 步骤6:查看数据
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED .3kb 172.19.0.2 es7_02
route_test p STARTED .3kb 172.19.0.5 es7_01 // 步骤7:查看此时索引里面的数据
GET route_test/_search
{
"took" : ,
"timed_out" : false,
"_shards" : {
"total" : ,
"successful" : ,
"skipped" : ,
"failed" :
},
"hits" : {
"total" : {
"value" : ,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "a",
"_score" : 1.0,
"_source" : {
"data" : "A"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_source" : {
"data" : "B"
}
}
]
}
}

上面这个例子比较简单,先创建了一个拥有2个shard,0个副本(为了方便观察)的索引 route_test 。创建完之后查看两个shard的信息,此时shard为空,里面没有任何文档( docs 列为0)。接着我们插入了两条数据,每次插完之后,都检查shard的变化。通过对比可以发现 docid=a 的第一条数据写入了0号shard,docid=b 的第二条数据写入了1号 shard。需要注意的是这里的doc id我选用的是字母"a"和"b",而非数字。原因是连续的数字很容易路由到一个shard中去。以上的过程就是不指定routing时候的默认行为。

接着,我们指定routing,看一些有趣的变化:

// 步骤8:插入第3条数据
PUT route_test/_doc/c?routing=key1&refresh
{
"data": "C"
} // 步骤9:查看shard
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED .4kb 172.19.0.2 es7_02
route_test p STARTED .9kb 172.19.0.5 es7_01 // 步骤10:查看索引数据
GET route_test/_search
{
"took" : ,
"timed_out" : false,
"_shards" : {
"total" : ,
"successful" : ,
"skipped" : ,
"failed" :
},
"hits" : {
"total" : {
"value" : ,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "a",
"_score" : 1.0,
"_source" : {
"data" : "A"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "c",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "C"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_source" : {
"data" : "B"
}
}
]
}
}

我们又插入了1条 docid=c 的新数据,但这次我们指定了路由,路由的值是一个字符串"key1". 通过查看shard信息,能看出这条数据路由到了0号shard。也就是说用"key1"做路由时,文档会写入到0号shard。

接着我们使用该路由再插入两条数据,但这两条数据的 docid 分别为之前使用过的 "a"和"b",你猜一下最终结果会是什么样?

// 步骤11:插入 docid=a 的数据,并指定 routing=key1
PUT route_test/_doc/a?routing=key1&refresh
{
"data": "A with routing key1"
} // es的返回信息为:
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "a",
"_version" : ,
"result" : "updated", // 注意此处为updated,之前的三次插入返回都为created
"forced_refresh" : true,
"_shards" : {
"total" : ,
"successful" : ,
"failed" :
},
"_seq_no" : ,
"_primary_term" :
} // 步骤12:查看shard
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED .4kb 172.19.0.2 es7_02
route_test p STARTED .5kb 172.19.0.5 es7_01 // 步骤13:查询索引
GET route_test/_search
{
"took" : ,
"timed_out" : false,
"_shards" : {
"total" : ,
"successful" : ,
"skipped" : ,
"failed" :
},
"hits" : {
"total" : {
"value" : ,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "c",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "C"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "a",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "A with routing key1"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_source" : {
"data" : "B"
}
}
]
}
}

之前 docid=a 的数据就在0号shard中,这次依旧写入到0号shard中了,因为docid重复,所以文档被更新了。然后再插入 docid=b 的数据:

// 步骤14:插入 docid=b的数据,使用key1作为路由字段的值
PUT route_test/_doc/b?routing=key1&refresh
{
"data": "B with routing key1"
} // es返回的信息
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_version" : ,
"result" : "created", // 注意这里不是updated
"forced_refresh" : true,
"_shards" : {
"total" : ,
"successful" : ,
"failed" :
},
"_seq_no" : ,
"_primary_term" :
} // 步骤15:查看shard信息
GET _cat/shards/route_test?v
index shard prirep state docs store ip node
route_test p STARTED .4kb 172.19.0.2 es7_02
route_test p STARTED 11kb 172.19.0.5 es7_01 // 步骤16:查询索引内容
{
"took" : ,
"timed_out" : false,
"_shards" : {
"total" : ,
"successful" : ,
"skipped" : ,
"failed" :
},
"hits" : {
"total" : {
"value" : ,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "c",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "C"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "a",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "A with routing key1"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_routing" : "key1", // 和下面的 id=b 的doc相比,多了一个这个字段
"_source" : {
"data" : "B with routing key1"
}
},
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_source" : {
"data" : "B"
}
}
]
}
}

和步骤11插入docid=a 的那条数据相比,这次这个有些不同,我们来分析一下。步骤11中插入 docid=a 时,es返回的是updated,也就是更新了步骤2中插入的docid为a的数据,步骤12和13中查询的结果也能看出,并没有新增数据,route_test中还是只有3条数据。而步骤14插入 docid=b 的数据时,es返回的是created,也就是新增了一条数据,而不是updated原来docid为b的数据,步骤15和16的确也能看出多了一条数据,现在有4条数据。而且从步骤16查询的结果来看,有两条docid为b的数据,但一个有routing,一个没有。而且也能分析出有routing的在0号shard上面,没有的那个在1号shard上。

这个就是我们自定义routing后会导致的一个问题:docid不再全局唯一。ES shard的实质是Lucene的索引,所以其实每个shard都是一个功能完善的倒排索引。ES能保证docid全局唯一是采用do id作为了路由,所以同样的docid肯定会路由到同一个shard上面,如果出现docid重复,就会update或者抛异常,从而保证了集群内docid唯一标识一个doc。但如果我们换用其它值做routing,那这个就保证不了了,如果用户还需要docid的全局唯一性,那只能自己保证了。因为docid不再全局唯一,所以doc的增删改查API就可能产生问题,比如下面的查询:

GET route_test/_doc/b

// es返回
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_version" : ,
"_seq_no" : ,
"_primary_term" : ,
"found" : true,
"_source" : {
"data" : "B"
}
} GET route_test/_doc/b?routing=key1 // es返回
{
"_index" : "route_test",
"_type" : "_doc",
"_id" : "b",
"_version" : ,
"_seq_no" : ,
"_primary_term" : ,
"_routing" : "key1",
"found" : true,
"_source" : {
"data" : "B with routing key1"
}
}

上面两个查询,虽然指定的docid都是b,但返回的结果是不一样的。所以,如果自定义了routing字段的话,一般doc的增删改查接口都要加上routing参数以保证一致性。注意这里的【一般】指的是查询,并不是所有查询接口都要加上routing。

为此,ES在mapping中提供了一个选项,可以强制检查doc的增删改查接口是否加了routing参数,如果没有加,就会报错。设置方式如下:

PUT <索引名>/
{
"settings": {
"number_of_shards": ,
"number_of_replicas":
},
"mappings": {
"_routing": {
"required": true // 设置为true,则强制检查;false则不检查,默认为false
}
}
}

举个例子:

PUT route_test1/
{
"settings": {
"number_of_shards": ,
"number_of_replicas":
},
"mappings": {
"_routing": {
"required": true
}
}
} // 写入一条数据
PUT route_test1/_doc/b?routing=key1
{
"data": "b with routing"
} // 以下的增删改查都会抱错
GET route_test1/_doc/b
PUT route_test1/_doc/b
{
"data": "B"
}
DELETE route_test1/_doc/b // 错误信息
"error": {
"root_cause": [
{
"type": "routing_missing_exception",
"reason": "routing is required for [route_test1]/[_doc]/[b]",
"index_uuid": "_na_",
"index": "route_test1"
}
],
"type": "routing_missing_exception",
"reason": "routing is required for [route_test1]/[_doc]/[b]",
"index_uuid": "_na_",
"index": "route_test1"
},
"status":
}

当然,很多时候自定义路由是为了减少查询时扫描shard的个数,从而提高查询效率。默认查询接口会搜索所有的shard,但也可以指定routing字段,这样就只会查询routing计算出来的shard,提高查询速度。

使用方式也非常简单,只需在查询语句上面指定routing即可,允许指定多个:

-- 查询所有分区
GET route_test/_search
{
"query": {
"match": {
"data": "b"
}
}
} -- 查询指定分区
GET route_test/_search?routing=key1,key2
{
"query": {
"match": {
"data": "b"
}
}
}

另外,指定routing还有个弊端就是容易造成负载不均衡。所以ES提供了一种机制可以将数据路由到一组shard上面,而不是某一个。只需在创建索引时(也只能在创建时)设置index.routing_partition_size,默认值是1,即只路由到1个shard,可以将其设置为大于1且小于索引shard总数的某个值,就可以路由到一组shard了。值越大,数据越均匀。当然,从设置就能看出来,这个设置是针对单个索引的,可以加入到动态模板中,以对多个索引生效。指定后,shard的计算方式变为:

shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards

对于同一个routing值,hash(_routing)的结果固定的,hash(_id) % routing_partition_size的结果有 routing_partition_size 个可能的值,两个组合在一起,对于同一个routing值的不同doc,也就能计算出 routing_partition_size 可能的shard num了,即一个shard集合。但要注意这样做以后有两个限制:

  1. 索引的mapping中不能再定义join关系的字段,原因是join强制要求关联的doc必须路由到同一个shard,如果采用shard集合,这个是保证不了的。
  2. 索引mapping中_routingrequired必须设置为true。

但是对于第2点我测试了一下,如果不写mapping,是可以的,此时_routingrequired默认值其实是false的。但如果显式的写了,就必须设置为true,否则创建索引会报错。

// 不显式的设置mapping,可以成功创建索引
PUT route_test_3/
{
"settings": {
"number_of_shards": ,
"number_of_replicas": ,
"routing_partition_size":
}
}
// 查询也可以不用带routing,也可以正确执行,增删改也一样
GET route_test_3/_doc/a // 如果显式的设置了mappings域,且required设置为false,创建索引就会失败,必须改为true
PUT route_test_4/
{
"settings": {
"number_of_shards": ,
"number_of_replicas": ,
"routing_partition_size":
},
"mappings": {
"_routing": {
"required": false
}
}
}

不知道这算不算一个bug。

总结

ElasticSearch的routing算是一个高级用法,但的确非常有用。在我们公司的订单数据,就用merchant_no作为routing,这样就能保证同一个商户的数据全部保存到同一个shard去,后面检索的时候,同样使用merchant_no作为routing,就可以精准的从某个shard获取数据了。对于超大数据量的搜索,routing再配合hot&warm的架构,是非常有用的一种解决方案。而且同一种属性的数据写到同一个shard还有很多好处,比如可以提高aggregation的准确性。

注1:本文例子中routing=key1,这里的key1是具体的值,而不是字段名称;

注2:通过JavaAPI创建 IndexRequest 时,通过 routing(java.lang.String routing) 方法指定routing值,注意这里是具体的值,而不是字段名称;

注3:本文的所有测试基于ES 7.1.0版本。

hot&warm的架构,参考我另一篇文章:https://www.cnblogs.com/caoweixiong/p/11988457.html

参考:https://niyanchun.com/routing-in-es.html

ElasticSearch——路由(_routing)机制的更多相关文章

  1. (转)示例化讲解RIP路由更新机制

      目录(?)[+]   以下内容摘自最新上市的“四大金刚”图书之一<Cisco路由器配置与管理完全手册>(第二版)(其它三本分别为<Cisco交换机配置与管理完全手册>(第二 ...

  2. RabbitMQ学习总结(6)——消息的路由分发机制详解

    一.Routing(路由) (using the Java client) 在前面的学习中,构建了一个简单的日志记录系统,能够广播所有的日志给多个接收者,在该部分学习中,将添加一个新的特点,就是可以只 ...

  3. elasticsearch 路由文档到分片

    路由文档到分片 当你索引一个文档,它被存储在单独一个主分片上.Elasticsearch是如何知道文档属于哪个分片的呢?当你创建一个新文档,它是如何知道是应该存储在分片1还是分片2上的呢? 进程不能是 ...

  4. AEAI ESB路由转换机制说明

    1. 背景概述 相信了解数通畅联的人对AEAI ESB并不陌生,其设计器ESBDesigner中内置组件有:路由和转换.数据适配器.协议适配器.协议接入适配器等4类组件,每类组件下面包含各种类型的组件 ...

  5. MVC5路由系统机制详细讲解

    请求一个ASP.NET mvc的网站和以前的web form是有区别的,ASP.NET MVC框架内部给我们提供了路由机制,当IIS接受到一个请求时,会先看是否请求了一个静态资源(.html,css, ...

  6. C# 通过反射实现类似MVC路由的机制

    最近封装了个功能非常类似于MVC的路由.//MVC路由机制先找到Controller Action 什么是反射 反射(Reflection)是.NET中的重要机制,通过放射,可以在运行时获 得.NET ...

  7. ASP.NET MVC5路由系统机制详细讲解

    请求一个ASP.NET mvc的网站和以前的web form是有区别的,ASP.NET MVC框架内部给我们提供了路由机制,当IIS接受到一个请求时,会先看是否请求了一个静态资源(.html,css, ...

  8. elasticsearch httpclient认证机制

    最近公司单位搬迁,所有的服务都停止了,我负责的elasticsearch不知道怎么回事,一直不能运行呢,因此,我一直在负责调试工作.经过两天的调试工作,我发现新的服务器增加了httpclient认证机 ...

  9. angular 2 - 005 路由实现机制

    angular2的路由是不是很神奇, url发生了变化却没有看到有任何请求发出? 1. hash模式 url类似 http://localhost:4200/#/task-list,跳转到路由页面再刷 ...

随机推荐

  1. 7月新的开始 - Axure学习04 - 发布与预览、菜单和表格元件、流程图和连接点、标记元件

    Axure 的发布与预览 1.发布 2.生成html文件 常规:指定浏览器.工具栏的生成 页面.页面说明.元件说明.交互.标志(logo和描述).字体.移动设备等 3.发布到Axshare Axure ...

  2. Mybatis接口中传递多个参数

    1.接口 public interface MemberMapper { public boolean insertMember(Members member); public Members sel ...

  3. 2019牛客多校A All-one Matrices——单调栈

    题目 求非嵌套子矩阵的个数. 分析 单调栈的套路题(类似的有求最大子矩阵). 首先,按列预处理,每个位置化成连续1的个数. 例如,左边的图转成右边.                      然后枚举 ...

  4. Elasticsearch 索引文档的增删改查

    利用Elasticsearch-head可以在界面上(http://127.0.0.1:9100/)对索引进行增删改查 1.RESTful接口使用方法 为了方便直观我们使用Head插件提供的接口进行演 ...

  5. zabbix的日志监控

    前提条件是该日志文件对于启动zabbix agent进程的用户开启了可读权限,而且该日志的路径对该用户开方x权限,让能取到这个日志文件 第一个参数可以不用引号,前提是zabbix用户可以进入文件路径, ...

  6. Manthan, Codefest 19

    目录 Contest Info Solutions A. XORinacci B. Uniqueness C. Magic Grid D. Restore Permutation E. Let The ...

  7. python qq发消息

    # 原理是先将需要发送的文本放到剪贴板中,然后将剪贴板内容发送到qq窗口 # 之后模拟按键发送enter键发送消息 import win32gui import win32con import win ...

  8. F 阎小罗的Minimax (第十届山东理工大学ACM网络编程擂台赛 正式赛 )

    题解:by Mercury_Lc 阎小罗的矩阵给的n和m都不超过300,枚举一下所有情况就可以了,用前缀和来储存.数组a[x][y]代表前x行前y列的和是多少,那么枚举每一种切割的方式就可以.注意一下 ...

  9. JavaEE三层架构与MVC

    JavaEE三层架构与MVC 摘要与总结 等下总结下 MVC模式 Model1 ​ 说到设计模式,不得不提一下之前的的Model1.据说早期对于java企业项目的开发,用的是Jsp+JavaBean的 ...

  10. try except else finally

    try..except..else没有捕获到异常,执行else语句 try..except..finally不管是否捕获到异常,都执行finally语句