什么使用路由

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

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

因为默认情况下,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即可。虽然使用简单,但有细节需要注意。我们来看看:

  1. 先创建一个名为study_route的索引,该索引有2个shard,0个副本
PUT study_route/
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0
  }
}
  1. 查看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. 插入第1条数据
PUT study_route/_doc/a?refresh
{
  "data": "A"
}
  1. 查看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
  1. 插入第2条数据
PUT study_route/_doc/b?refresh
{
  "data": "B"
}
  1. 查看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,看看有什么变化。

  1. 插入第3条数据
PUT study_route/_doc/c?routing=key1&refresh
{
  "data": "C"
}
  1. 查看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”,最终结果会是什么样?

  1. 插入_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。

  1. 插入_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。

  1. 查看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
  1. 查询索引内容
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"
        }
      }
    ]
  }
}