一、路由
上一篇文章,我们练习文档删除的时候提到,如果新增文档的时候指定了路由,那么查询、更新、删除文档的时候也要使用指定的路由才能查得到。这篇文章就来讲一讲es的路由(routing)。
1.1、什么是路由
首先,es是一个分布式系统,当我们存储一个文档到es上,实际是保存到某一个主分片中,这个主分片可能是master节点也可能是slave节点,然后再保存到对应的副本分片中。而具体保存在哪个主分片,是通过下面这个公式来决定的:
shard = hash(routing) % number_of_primary_shards
这里routing就是路由,默认是文档的_id,通过hash函数生成一个数字,再除以number_of_primary_shards(主分片的数量)取余数,就能得到具体某一个分片。
这也解释了为什么我们要在创建索引的时候就确定好主分片的数量,并且不允许修改。因为一旦主分片的数量改变了,之前的所有路由都会无效,之前的数据也都找不回来了。
1.2、默认路由
前面说到es使用_id作为默认路由,我们来演示看看,首先新建一个索引,执行主分片数量为3,副本数量为0:
然后往routing中新增文档,文档_id定义为a:
PUT routing/_doc/a
{
"content":"routing-a"
}
新增文档之后,我们可以使用下面查询语句,查看文档具体是存放在哪个主分片上:
GET _cat/shards/routing?v
可以看到,slave01节点的分片0上文档数量变成1了,表示文档是被存到这里了。我们继续新增几个文档,也都会路由到不同的主分片上。
使用默认路由最大的优势是负载均衡,所有数据都会平均得分配到不同的主分片上,尽量避免产生热点数据。与此同时带来的问题是:查询时无法确认文档的具体位置,每次都要将请求广播到所有的分片上执行查询,将所有查询结果合并之后返回。如果分片数量过多(例如100个),且系统数据量过大时,就会带来性能问题。这时我们可以考虑使用自定义路由的方式进行优化。
1.3、自定义路由
自定义路由需要在新增文档的时候就进行指定,最简单的方式就是直接在URL上使用?routing=指定参数:
PUT routing/_doc/f?routing=amby
例如上面指定了routing参数为amby,那么查询、更新、删除文档的时候,也要指定routing,否则可能会查询不到结果报404。为什么说是可能呢,因为如果不指定routing,es默认是使用_id来查询,有可能出现默认_id计算出来的分片恰好等于指定routing计算出来的分片,就能查询到对应的数据。这种情况一般只有主分片数比较少时才有概率出现。
自定义路由后,后续的CRUD操作都应该加上路由,否则可能导致一份文档会被保存到多个分片上。我们可以通过设置来强制所有CRUD操作都带上路由参数,设置之后,不带路由参数的操作将会报错。注意,只能在新建索引的时候设置强制路由参数。
// 6.5.4版本
PUT routing
{
"mappings": {
"_doc": {
"_routing": {
"required": true
}
}
}
}
// 7.0版本
PUT routing
{
"mappings": {
"_routing": {
"required": true
}
}
}
1.4、优化路由的单点查询问题
自定义路由可能会导致索引分配不均,大量的索引路由到一个分片上,导致这个分片的索引和查询性能降低。为了解决这个问题,可以设置 routing_partition_size 参数。注意,这个参数同样只能在创建索引的时候设置。
PUT routing
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0,
"routing_partition_size": 1
},
"mappings": {
"_routing": {
"required": true
}
}
}
这样routing将路由到一组分片,然后_id字段在决定文档保存到那一个分片上。由于这个原因,routing_partition_size的值必须是一个大于1但是小于number_of_shards设置的分片数量的一个整数。设置参数后具体公式如下:
shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
索引一旦设置了routing_partition_size 后,join field将不能被创建了,同时索引中的所有mapping必须设置_routing为required的。
1.5、总结
当主分片数量不多,系统数据量不大时,使用默认路由就可以满足绝大多数业务场景。但如果主分片数量很大(例如100个)时,或数据量在PB级别以上时,可以考虑自定义路由的方式,加快查询检索的效率。具体的自定义规则应该由业务场景来决定,慎重考虑路由的字段,且进行必要的测试。
参考文档:
二、并发与版本控制
ElasticSearch是分布式系统,允许异步和并发,所以必须要保证并发执行时旧数据不会覆盖新数据。这里用到了乐观锁机制,es主要通过版本控制来实现。
2.1、什么是版本
还记得上一篇文章中,操作文档时返回的响应json中,都有如下三个版本属性:
-
_version 可以理解成文档的版本号,更新文档时,版本会自动加 1,属于单个文档。
-
_seq_no 可以理解成索引的版本号,也可以用来做版本控制,当索引中的任何文档新增、更新、删除时,都会自动加1,属于整个索引。
-
_primary_term 表示文档所在主分片的编号。
2.2、如何进行版本控制
2.2.1、内部版本
es自己维护的就是内部版本,维护操作不需要用户指定,底层自动进行操作:每份文档创建时版本号_version都赋值为1,之后每次更新操作,都会讲版本号自增1。在并发操作中,es要求version参数值必须等于当前文档的版本号_version,否则将返回409报错。
在es6.7之前,我们可以手动指定内部版本号:
// 使用version指定内部版本号。注意:在es6.7之前的版本有效,6.7之后不支持此方法
PUT version/_doc/1?version=1
{
"content": "version-1"
}
注意:es6.7之后,已经不支持指定内部版本号的方式,执行上面语句会报错返回400,并提示使用if_seq_no和if_primary_term进行版本控制(后面有讲)。
2.2.2、外部版本
es还支持外部版本,通过version_type=external来指定使用外部版本号。版本号必须是大于0的整数, 且小于9.2E+18(Java中long类型的正值)。外部版本的处理方式和内部版本有些不同:
-
version_type=external 检查当前_version是否小于<外部版本号,如果是使用外部版本号更新文档,否则返回409报错。
-
version_type=external_gte 检查当前_version是否小于或等于≤外部版本号,如果是则使用外部版本号更新文档,否则返回409报错。
// 使用version和version_type指定外部版本号
PUT version/_doc/1?version=2&version_type=external
{
"content": "version-2"
}
2.2.3、最新版本
在es6.7之后的版本,推荐使用if_seq_no和if_primary_term进行版本控制。要注意参数值必须等于所要更新文档的seq_no和primary_term。
// 使用if_seq_no和if_primary_term进行版本控制
PUT version/_doc/1?if_seq_no=5&if_primary_term=1
{
"content": "version-3"
}
参考官方文档(旧版本): https://www.elastic.co/guide/cn/elasticsearch/guide/current/optimistic-concurrency-control.html
版权声明:
本文仅记录ElasticSearch学习心得,如有侵权请联系删除。
更多内容请访问原创作者:
微信公众号:江南一点雨
B站:https://space.bilibili.com/49484631?from=search&seid=6136072956000981995