前言
elasticsearch使用version、seqNo、primaryTerm三个字段做乐观锁并发控制。
早期版本6.7以前,使用version来控制文档版本,如修改指定版本:
PUT user/_doc/1?version=11
6.7及以后版本使用seqNo和primaryTerm,修改指定版本使用下面的方式:
PUT user/_doc/1?if_seq_no=22&if_primary_term=2
version字段的机制仍然是保留的,在8.6的源码中,对于版本冲突的校验,仍然包括了对version的验证。
这三个字段会在不同的场景逻辑下实现自增长。
- version字段针对每个文档的修改(包括删除)操作。
- seq_no字段针对每个分片的文档修改(包括删除)操作。
- primary_term字段针对故障导致的主分片重启或主分片切换,每发生一次自增1。
下面来测试验证一下这个结论。
环境:
- elasticsearch集群:node-1,node-2,node-3
- 索引:user,3个主分片,每个分片一个副本。
创建索引
PUT user
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
}
version和seqNo自增测试
批量插入数据
POST user/_bulk
{"index":{"_id":"1"}}
{"name":"001"}
{"index":{"_id":"2"}}
{"name":"002"}
{"index":{"_id":"3"}}
{"name":"003"}
{"index":{"_id":"4"}}
{"name":"004"}
{"index":{"_id":"5"}}
{"name":"005"}
{"index":{"_id":"6"}}
{"name":"006"}
数据插入后版本号变化:
version和primary_term,每个文档都为初始值1.
而seq_no值各有变化。这是什么原因导致的呢?
查看数据分片分布
GET /user/_search
{
"explain": true,
"query": {
"match_all": {
}
}
}
数据分片分配关系如下:
- 分片0:5
- 分片1:2,3,4
- 分片2:1,6
从这里可以看出:
id=2,id=3和id=4的文档同属分片1,所以seq_no从0 -> 1 -> 2。
id=1和id=6的文档同属分片2,所以seq_no从0 -> 1。
seq_no的初始值从0开始。
开始测试
1.修改id=2的文档
POST user/_doc/2
{
"name":"002"
}
{
"_index": "user",
"_id": "2",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
- version 1 -> 2
- seq_no 0 -> 3
- primary_term 不变
id=2的文档和id=3,id=4两个文档在同属分片1,分片1的seq_no当前值为2。故seq_no增1,值为3.
修改前后对比:
2.修改id=1的文档
POST user/_doc/1
{
"name":"001"
}
{
"_index": "user",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
- version 1 -> 2
- seq_no 0 -> 2
- primary_term 不变
id=1的文档和id=6的文档在同属分片2,分片2的seq_no当前最大值为1,所以seq_no变成2.
修改前后对比:
3.删除id=3的文档
DELETE /user/_doc/3
{
"_index": "user",
"_id": "3",
"_version": 2,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}
- version 1 -> 2
- seq_no 1 -> 4
- primary_term 不变
id=3的文档归属分片1,分片1的seq_no当前值为3。故seq_no最新值为4.
修改前后对比:
4.修改id=2的文档
POST user/_doc/2
{
"name":"002"
}
{
"_index": "user",
"_id": "2",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 5,
"_primary_term": 1
}
- version 2 -> 3
- seq_no 3 -> 5
- primary_term 不变
id=3的文档归属分片1,分片1的seq_no当前值为4。故seq_no为5。
修改前后对比:
5.修改id=1的文档
POST user/_doc/1
{
"name":"001"
}
{
"_index": "user",
"_id": "1",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
- version 2 -> 3
- seq_no 2 -> 3
- primary_term 不变
id=1的文档归属分片2,分片2的seq_no当前值为2。
修改前后对比:
primaryTerm自增测试
查看分片节点分配信息
GET /_cat/shards/user?v
| 分片 | 主分片节点 | 副本节点 |
|---|---|---|
| 0 | node-3 | node-1 |
| 1 | node-2 | node-1 |
| 2 | node-3 | node-1 |
开始测试
1.下线node-1
node-1节点上均为副本节点,所以不影响primaryTerm。
下线node-1后查看分片分配节点信息:
GET /_cat/shards/user?v
2.修改id=1的文档
POST user/_doc/1
{
"name":"001"
}
{
"_index": "user",
"_id": "1",
"_version": 4,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}
- version 3 -> 4
- seq_no 3 -> 4
- primary_term 不变
修改前后对比:
3.修改id=5的文档
POST user/_doc/5
{
"name":"005"
}
{
"_index": "user",
"_id": "5",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
- version 1 -> 2
- seq_no 0 -> 1
- primary_term 不变
修改前后对比:
4.恢复node-1
查看分片分配节点信息,恢复成之前的分配了:
GET /_cat/shards/user?v
5.下线node-3
node3上有分片0和分片2的主分片。
查看分片分配节点信息:
GET /_cat/shards/user?v
分片0和分片2的主分片转移至node-1节点了。
6.修改id=5的文档
POST user/_doc/5
{
"name":"005"
}
{
"_index": "user",
"_id": "5",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 2
}
- version 2 -> 3
- seq_no 1 -> 2
- primary_term 1 -> 2
id=5归属分片0.node3下线分片0的主节点切换至node-1。
修改前后对比:
7.修改id=2的文档
POST user/_doc/2
{
"name":"002"
}
{
"_index": "user",
"_id": "2",
"_version": 4,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 6,
"_primary_term": 1
}
- version 3 -> 4
- seq_no 4 -> 6
- primary_term 1 -> 1 不变
id=2归属分片1.node3下线没有导致分片1主节点切换。
修改前后对比:
8.恢复node-3
查看分片分配节点信息:
GET /_cat/shards/user?v
index shard prirep state docs store ip node
user 0 p STARTED 1 4.6kb 127.0.0.1 node-1
user 0 r STARTED 1 4.6kb 127.0.0.1 node-2
user 1 p STARTED 2 8.9kb 127.0.0.1 node-3
user 1 r STARTED 2 8.9kb 127.0.0.1 node-1
user 2 r STARTED 2 11.1kb 127.0.0.1 node-3
user 2 p STARTED 2 11.1kb 127.0.0.1 node-1
分片1的主分片从node-2切换至node-3了,这是由eslasticsearch内部分配机制调配的。
9.修改id=2的文档
POST user/_doc/2
{
"name":"002"
}
{
"_index": "user",
"_id": "2",
"_version": 5,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 7,
"_primary_term": 1
}
- version 3 -> 4
- seq_no 4 -> 6
- primary_term 1 -> 1 不变
此处primary_term并没有发生变化,因为分片1的主分片虽然切换了,但是这个切换不是故障直接导致的,而是由eslasticsearch内部机制分配的,es内部机制分配可以保障切换过程中的一致性,故此时primary_term不需要变化。
修改前后对比:
总结
为什么因故障产生主分片切换时,需要记录primary_term了?
个人觉得如下场景会导致:
- 集群中某节点发生故障宕机,节点上的主分片下线,但是故障发生瞬间,主分片上的数据还未完全同步至其它副本。
- 集群中其它副本将被重新选举为新的主分片。
- 宕机节点重新上线,此时它需要和新的主节点同步数据。
- 由于故障时原主分片数据未完本同步,那么新老主分片的version和seq_no就可能重复存在冲突,那么怎么区分哪些数据是新主分片增加的了?这就是primary_term的作用了,在新主分片上发生的任何修改,primary_term的值都比老主分片产生的primary_term值+1.
seq_no记录分片上发生操作的顺序,配合primary_term一起解决分片数据同步的一致性问题。