什么使用路由
假设有一个100个分片的索引,当一个请求在集群上执行时会发生什么呢?
- 这个搜索的请求会被发送到集群中的一个节点上
- 接收到这个请求的节点,将这个查询转到这个索引的每个分片上(可能是主分片,也可能是副本分片)
- 每个分片执行这个搜索查询并返回结果
- 结果在通道节点上合并、排序并返回给用户
因为默认情况下,Elasticsearch使用文档的ID(类似于关系数据库中的自增ID),如果插入数据量比较大,文档会平均的分布于所有的分片上,这导致了Elasticsearch不能确定文档的位置,所以它必须将这个请求转到所有的N个分片上去执行,这种操作会给集群带来负担,增大了网络的开销。
_search_shards API
那么如何确定请求在哪个分片上执行呢?Elasticsearch提供了一个API接口,告诉我们一个搜索请求在哪些节点和分片上执行。
比如我们创建一个两分片的索引:
put route_test
{
"settings": {
"index.number_of_shards": 2
}
}
并放入数据:
put route_test/_doc/1
{
"name": "doc1"
}
put route_test/_doc/2
{
"name": "doc2"
}
put route_test/_doc/3
{
"name": "doc3"
}
put route_test/_doc/4
{
"name": "doc4"
}
使用搜索分片(search shards)的API接口来查看请求将在哪些分片上执行。
get route_test/_search_shards?filter_path=shards
{
"shards" : [
[
{
"state" : "STARTED",
"primary" : true,
"node" : "0e-WRjOATYmkLfGuKdcVsg",
"relocating_node" : null,
"shard" : 0,
"index" : "route_test",
"allocation_id" : {
"id" : "BxiXM7V4Sx-S3HOD8sBUAg"
}
}
],
[
{
"state" : "STARTED",
"primary" : true,
"node" : "0e-WRjOATYmkLfGuKdcVsg",
"relocating_node" : null,
"shard" : 1,
"index" : "route_test",
"allocation_id" : {
"id" : "J7Gmrp0SS9-OStreLOw3Fw"
}
}
]
]
}
可以看到,会搜索全部的两个分片。
带上路由进行搜索:
get route_test/_search_shards?routing=1&filter_path=shards
{
"shards" : [
[
{
"state" : "STARTED",
"primary" : true,
"node" : "0e-WRjOATYmkLfGuKdcVsg",
"relocating_node" : null,
"shard" : 0,
"index" : "route_test",
"allocation_id" : {
"id" : "BxiXM7V4Sx-S3HOD8sBUAg"
}
}
]
]
}
即使在索引中只有两个分片,当指定路由值1的时候,只有分片shard 0会被搜索。对于搜索需要查找的数据,有效地切除了一半的数据量!所以当处理拥有大量分片的索引时,路由会很有价值,当然对于Elasticsearch的常规使用它并不是必需的。
配置路由
路由也可以不使用文档的ID,而是定制的数值进行散列。通过指定URL中的routing查询参数,系统将使用这个值进行散列,而不是ID。
PUT route_test/_doc/5?routing=rountkey
{
"name": "doc5"
}
在这个例子中,rountkey这个由我们自己输入的值决定文档属于哪个分片的散列值,而不是文档的ID值2。
由上可知,自定义路由的方式非常简单,只需要在插入数据的时候指定路由的key即可。虽然使用简单,但有细节需要注意。我们来看看:
- 先创建一个名为study_route的索引,该索引有2个shard,0个副本
PUT study_route/
{
"settings": {
"number_of_shards": 2,
"number_of_replicas": 0
}
}
- 查看shard
get _cat/shards/study_route?v
index shard prirep state docs store ip node
study_route 1 p STARTED 0 230b 172.18.0.2 3f068cc83647
study_route 0 p STARTED 0 230b 172.18.0.2 3f068cc83647
- 插入第1条数据
PUT study_route/_doc/a?refresh
{
"data": "A"
}
- 查看shard
get _cat/shards/study_route?v
index shard prirep state docs store ip node
study_route 1 p STARTED 0 283b 172.18.0.2 3f068cc83647
study_route 0 p STARTED 1 3.3kb 172.18.0.2 3f068cc83647
- 插入第2条数据
PUT study_route/_doc/b?refresh
{
"data": "B"
}
- 查看shard
get _cat/shards/study_route?v
index shard prirep state docs store ip node
study_route 1 p STARTED 1 3.3kb 172.18.0.2 3f068cc83647
study_route 0 p STARTED 1 3.3kb 172.18.0.2 3f068cc83647
这个例子比较简单,先创建了一个拥有2个shard,0个副本的索引study_route。创建完之后查看两个shard的信息,此时 shard为空,里面没有任何文档(docs列为0)。接着我们插入了两条数据,每次插完之后,都检查shard的变化。通过对比可以发现_id=a的第一条数据写入了shard 0,_id=b的第二条数据写入了shard 1。
接着,我们指定routing,看看有什么变化。
- 插入第3条数据
PUT study_route/_doc/c?routing=key1&refresh
{
"data": "C"
}
- 查看shard
index shard prirep state docs store ip node
study_route 1 p STARTED 1 3.4kb 172.18.0.2 3f068cc83647
study_route 0 p STARTED 2 6.8kb 172.18.0.2 3f068cc83647
我们又插入了1条_id=c的新数据,但这次我们指定了路由,路由的值是一个字符串"key1"。通过查看shard信息,能看出这条数据路由到了shard 0。也就是说用“key1”做路由时,文档会写入到shard 0。
接着我们使用该路由再插入两条数据,但这两条数据的_id分别为之前使用过的“a”和“b”,最终结果会是什么样?
- 插入_id=a的数据,并指定routing=key1
PUT study_route/_doc/a?routing=key1&refresh
{
"data": "A with routing key1"
}
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "a",
"_version" : 2,
"result" : "updated",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
es的返回信息表明文档a是updated。
- 插入_id=b的数据,使用key1作为路由字段的值
PUT study_route/_doc/b?routing=key1&refresh
{
"data": "B with routing key1"
}
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "b",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
es返回的信息变成了created。
- 查看shard信息
get _cat/shards/study_route?v
index shard prirep state docs store ip node
study_route 1 p STARTED 1 3.4kb 172.18.0.2 3f068cc83647
study_route 0 p STARTED 3 13.9kb 172.18.0.2 3f068cc83647
- 查询索引内容
GET study_route/_search?filter_path=hits
{
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "c",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "C"
}
},
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "a",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "A with routing key1"
}
},
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_routing" : "key1",
"_source" : {
"data" : "B with routing key1"
}
},
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "b",
"_score" : 1.0,
"_source" : {
"data" : "B"
}
}
]
}
}
存在两个_id为b的文档,其中一个比另一个多了一个字段“_routing”。
这个就是我们自定义routing后会导致的一个问题:_id不再全局唯一。ES shard的实质是Lucene的索引,所以其实每个shard都是一个功能完善的倒排索引。ES能保证_id全局唯一是采用_id作为了路由,所以同样的_id肯定会路由到同一个 shard上面,如果出现_id重复,就会update或者抛异常,从而保证了集群内_id唯一标识一个doc。但如果我们换用其它值做routing,那这个就保证不了了,如果用户还需要_id的全局唯一性,那只能自己保证了。
因为_id不再全局唯一,所以doc的增删改查API就可能产生问题,比如下面的查询:
get study_route/_doc/b?filter_path=_source
{
"_source" : {
"data" : "B"
}
}
get study_route/_doc/b?routing=key1&filter_path=_source
{
"_source" : {
"data" : "B with routing key1"
}
}
上面两个查询,虽然指定的_id都是b,但返回的结果是不一样的。所以,如果自定义了routing字段的话,一般doc的增删改查接口都要加上routing参数以保证一致性。
为此,ES在mapping中提供了一个选项,可以强制检查doc的增删改查接口是否加了routing参数,如果没有加,就会报错。
设置方式如下:
PUT study_route1/
{
"settings": {
"number_of_shards": 2,
"number_of_replicas": 0
},
"mappings": {
"_routing": {
"required": true
}
}
}
很多时候自定义路由是为了减少查询时扫描shard的个数,从而提高查询效率。默认查询接口会搜索所有的shard,但也可以指定routing字段,这样就只会查询routing计算出来的shard,提高查询速度。
使用方式也非常简单,只需在查询语句上面指定routing即可,允许指定多个:
GET study_route/_search?routing=key1&filter_path=hits
{
"query": {
"match": {
"data": "b"
}
}
}
{
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.86312973,
"hits" : [
{
"_index" : "study_route",
"_type" : "_doc",
"_id" : "b",
"_score" : 0.86312973,
"_routing" : "key1",
"_source" : {
"data" : "B with routing key1"
}
}
]
}
}