Elasticsearch中version与seqNo+primaryTerm分析

6,755 阅读5分钟

前言

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"}

数据插入后版本号变化:

image.png

version和primary_term,每个文档都为初始值1.

而seq_no值各有变化。这是什么原因导致的呢?

查看数据分片分布

GET /user/_search
{
  "explain": true,
  "query": {
    "match_all": {
    }
  }
}

image.png

数据分片分配关系如下:

  1. 分片0:5
  2. 分片1:2,3,4
  3. 分片2:1,6

image.png

从这里可以看出:

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.

修改前后对比:

image.png

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.

修改前后对比:

image.png

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.

修改前后对比:

image.png

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。

修改前后对比:

image.png

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。

修改前后对比:

image.png

primaryTerm自增测试

查看分片节点分配信息

GET /_cat/shards/user?v

image.png

分片主分片节点副本节点
0node-3node-1
1node-2node-1
2node-3node-1

开始测试

1.下线node-1

node-1节点上均为副本节点,所以不影响primaryTerm。

下线node-1后查看分片分配节点信息:

GET /_cat/shards/user?v

image.png

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 不变

修改前后对比:

image.png

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 不变

修改前后对比:

image.png

4.恢复node-1

查看分片分配节点信息,恢复成之前的分配了:

GET /_cat/shards/user?v

image.png

5.下线node-3

node3上有分片0和分片2的主分片。

查看分片分配节点信息:

GET /_cat/shards/user?v

image.png

分片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。

修改前后对比:

image.png

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主节点切换。

修改前后对比:

image.png

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不需要变化。

修改前后对比:

image.png

总结

为什么因故障产生主分片切换时,需要记录primary_term了?

个人觉得如下场景会导致:

  1. 集群中某节点发生故障宕机,节点上的主分片下线,但是故障发生瞬间,主分片上的数据还未完全同步至其它副本。
  2. 集群中其它副本将被重新选举为新的主分片。
  3. 宕机节点重新上线,此时它需要和新的主节点同步数据。
  4. 由于故障时原主分片数据未完本同步,那么新老主分片的version和seq_no就可能重复存在冲突,那么怎么区分哪些数据是新主分片增加的了?这就是primary_term的作用了,在新主分片上发生的任何修改,primary_term的值都比老主分片产生的primary_term值+1.

seq_no记录分片上发生操作的顺序,配合primary_term一起解决分片数据同步的一致性问题。