天天看点

SpringBoot:ElasticSearch-路由(_routing)机制

前言

当我们创建索引时,需要设置索引名、分片数、副本数三个参数。

SpringBoot:ElasticSearch-路由(_routing)机制

索引名就是类似数据库名,分片数指存储数据的几个空间,副本数指数据备份。

当我们向索引库内存储一条数据时,数据会存到某个分片中,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?

SpringBoot:ElasticSearch-路由(_routing)机制

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

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. 结果在通道节点上合并、排序并返回给用户

SpringBoot:ElasticSearch-路由(_routing)机制

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

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

路由的作用

在ElaticSearch里面,路由功能算是高级用法,大多数时候我们用的都是系统默认的路由功能,路由的作用就是将同类型数据存储到相同的分片中,通过检索查询时,可以快速定位某个分片获取数据。

通过上面那个例子说明:

上面场景的问题很明显,由于数据分散到多个分片,导致数据查询的效率加大,优化思路也比较明确,那就是按照相同类型的数据存储到一个分区中,然后查询时,直接查询对应类型的数据即可。

实操演示

创建索引库

# 先创建一个名为route_test的索引,该索引有2个shard,0个副本
PUT route_test/
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0
  }
}      

查看分片数据 

# 查看shard 可以看到docs下都是0 表示两个分片都没有数据
GET _cat/shards/route_test?v
index      shard prirep state   docs store ip         node
route_test 1     p      STARTED    0  230b 172.19.0.2 es7_02
route_test 0     p      STARTED    0  230b 172.19.0.5 es7_01      

 不指定路由添加数据

 插入一条数据A

// 插入第1条数据
PUT route_test/_doc/a?refresh
{
  "data": "A"
}      

 查看分片数据 

# 查看shard 可以看到docs下的第0个分片的数据为1
GET _cat/shards/route_test?v
index      shard prirep state   docs store ip         node
route_test 1     p      STARTED    0  230b 172.19.0.2 es7_02
route_test 0     p      STARTED    1 3.3kb 172.19.0.5 es7_01      

 插入第二条数据B

# 插入第2条数据
PUT route_test/_doc/b?refresh
{
  "data": "B"
}      

查看分片数据 

# 查看数据 可以看到分片1也添加1条数据
GET _cat/shards/route_test?v
index      shard prirep state   docs store ip         node
route_test 1     p      STARTED    1 3.3kb 172.19.0.2 es7_02
route_test 0     p      STARTED    1 3.3kb 172.19.0.5 es7_01      

指定路由添加数据

插入第三条数据C

# 插入第3条数据 并且设置路由参数为key1(自定义)
PUT route_test/_doc/c?routing=key1&refresh
{
  "data": "C"
}      

查看分片数据

# 查看shard 通过docs的值可以看到ES将这条数据存入到分片0中
GET _cat/shards/route_test?v
index      shard prirep state   docs store ip         node
route_test 1     p      STARTED    1 3.4kb 172.19.0.2 es7_02
route_test 0     p      STARTED    2 6.9kb 172.19.0.5 es7_01      

查询索引库的全部数据

# 查看索引数据 可以看到_id=c的数据多了路由参数"_routing" : "key1"
GET route_test/_search
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "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"
        }
      }
    ]
  }
}      

数据重复问题

我们知道路由的概念是将同类型的数据存储到相同分片中,如果我们修改A、B的数据,给它们添加路由会怎么样?

修改数据A

# 插入 _id=a 的数据(插入id相同的数据,ES会默认覆盖旧数据,也就是修改),并指定 routing=key1
PUT route_test/_doc/a?routing=key1&refresh
{
  "data": "A with routing key1"
}

## ES的返回信息为:
{
  "_index" : "route_test",
  "_type" : "_doc",
  "_id" : "a",
  "_version" : 2,
  "result" : "updated",        # 注意此处为updated,表示执行的修改操作
  "forced_refresh" : true,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}      

查看分片数据

# 查看shard 可以看到数据A还是在原来的分片0,与数据C同属分片0,没有变化
GET _cat/shards/route_test?v
index      shard prirep state   docs  store ip         node
route_test 1     p      STARTED    1  3.4kb 172.19.0.2 es7_02
route_test 0     p      STARTED    2 10.5kb 172.19.0.5 es7_01      

查询索引库的全部数据 

# 查询索引 可以看到数据A已经有"_routing" : "key1"
GET route_test/_search
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "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"
        }
      }
    ]
  }
}      

修改数据B

# 插入 _id=b 的数据,并指定 routing=key1
PUT route_test/_doc/b?routing=key1&refresh
{
  "data": "B with routing key1"
}

## ES返回的信息
{
  "_index" : "route_test",
  "_type" : "_doc",
  "_id" : "b",
  "_version" : 1,
  "result" : "created",        # 注意这里不是updated 而是created 表示当前这条数据是新增的!!!!!!!
  "forced_refresh" : true,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 3,
  "_primary_term" : 1
}      

查看分片数据

# 查看shard信息 这里发现不同 分片0添加了1条数据
GET _cat/shards/route_test?v
index      shard prirep state   docs store ip         node
route_test 1     p      STARTED    1 3.4kb 172.19.0.2 es7_02
route_test 0     p      STARTED    3  11kb 172.19.0.5 es7_01      

查询索引库的全部数据

# 查询索引内容 竟然有两条数据B的数据 一条有路由参数 一条没有
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "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"
        }
      }
    ]
  }
}      

我们来分析一下。插入 数据A 时,ES返回的是updated,也就是更新了旧数据。而插入 数据B 的数据时,ES返回的是created,也就是新增了一条数据,它并没有更新旧数据。而且从之后查询的结果来看,有两条 数据B 的数据,但一个有routing(路由参数),一个没有。由此分析出有routing的在分片0上面,没有的路由参数的那个在分片1内。

这会导致的一个问题:_id不再全局唯一

ES 分片( shard )的实质是Lucene的索引,所以其实每个shard都是一个功能完善的倒排索引。ES能保证docid(_id)全局唯一是默认采用docid作为路由,所以同样的docid肯定会路由到同一个shard上面,如果出现docid重复,就会update或者抛异常,从而保证了集群内docid标识唯一条数据。但如果我们自定义设置routing,那就不能保证了,如果用户还需要docid的全局唯一性,那只能自己设计严格的约束。因为docid不再全局唯一,所以数据的增删改查操作就可能产生问题,比如下面的查询:

查询 docid=b 的数据

# 查询 _id=b 的数据
GET route_test/_doc/b

## es返回
{
  "_index" : "route_test",
  "_type" : "_doc",
  "_id" : "b",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "data" : "B"
  }
}



# 再次查询 _id=b 的数据
GET route_test/_doc/b?routing=key1

## es返回
{
  "_index" : "route_test",
  "_type" : "_doc",
  "_id" : "b",
  "_version" : 1,
  "_seq_no" : 3,
  "_primary_term" : 1,
  "_routing" : "key1",
  "found" : true,
  "_source" : {
    "data" : "B with routing key1"  # 可以看到两次查询的数据完全不一致
  }
}      

发现两次查询的数据并不一致,所以如果自定义routing字段的话,一般的增删改查接口都要加上routing参数以保证一致性。

注意这里的【一般】指的是查询,并不是所有查询接口都要加上routing。

ES在创建索引库时,mapping中提供一个选项,可以强制检查doc的增删改查接口是否加了routing参数,如果没有加,就会报错。(具体加不加, 根据自身业务决定)

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

指定路由查询

查询路由为key1的数据

# 如果查询多个路由的数据 直接指定多个路由就可以 ?routing=key1,key2,key3
GET route_test/_search?routing=key1

# 查询索引的内容
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "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",        
        "_source" : {
          "data" : "B with routing key1"
        }
      }
    ]
  }
}      

优化路由造成的负载不均衡

指定routing还有个弊端就是容易造成负载不均衡。

所以ES提供了一种机制可以将数据路由到一组shard上面,而不是某一个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中​

​_routing​

​的​

​required​

​必须设置为true。

对于第2点做测试时,发现如果不写mapping,也是可以的,此时​

​_routing​

​的​

​required​

​默认值其实是false的。但如果显式的写了​

​_routing​

​,就必须设置为true,否则创建索引会报错。

# 不显式的设置mapping,可以成功创建索引
PUT route_test_3/
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0,
    "routing_partition_size": 2
  }
}

# 查询也可以不用带routing,也可以正确执行,增删改也一样
GET route_test_3/_doc/a

# 如果显式的设置了mappings域,且required设置为false,创建索引就会失败,必须改为true
PUT route_test_4/
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0,
    "routing_partition_size": 2
  },
  "mappings": {
    "_routing": {
      "required": false
    }
  }
}