【存储】etcd的存储是如何实现的(2)

news2024/9/9 8:02:43

在上一篇中,介绍了etcd底层存储的内容,包括wal、raft.MemoryStorage以及backend。在介绍backend时提到了backend只是etcd kv存储的一部分,负责持久化存储,backend加内存化treeIndex才构成etcd完整的支持mvcc的kv存储。所以这篇就来介绍etcd mvcc的实现。

文章目录

  • 整体架构
  • index
    • keyIndex
    • treeIndex
  • kvstore

整体架构

在看mvcc的具体实现之前,我们先从整体上介绍mvcc。

etcd维护了一个全局递增的版本号,每次变更都会产生新的版本号(版本号实际是main_sub的格式,会在kvstore部分详细介绍)。同时,etcd在内存中维护key => 版本号之间的映射关系,而持久化存储Backend中存储的是版本号 => value之间的映射关系。

查询时,首先根据key在查询到版本号,然后再根据版本号去backend中查询value;增加或者修改时,先在内存中增加key和版本号之间的映射,再将版本号和value存储至backend。

整体架构图如下。index模块在内存中维护了key => 版本号的映射。backend模块是kv存储,负责存储版本号 => value,前面已经详细介绍过。KV在index和backend上封装了支持mvcc的kv存储,其支持读写事务。

index

etcd使用了google开源的B树(btree package)在内存中维护了key和相应版本之间的关系。

treeIndex的结构如下,其使用泛型的B树来维护key和版本之间的关系。B树传入的类型为keyIndex,可以认为其是一个key为etcd key,value为key index的B树。

type treeIndex struct {
   sync.RWMutex
   tree *btree.BTreeG[*keyIndex] //泛型实现
   lg   *zap.Logger
}

keyIndex

key index是etcd中记录版本信息的数据结构,其结构如下。key是etcd的key,用来构建B树;modified表示最近一次修改的版本号;generations中记录着该key的修改历史。

type keyIndex struct {
   key         []byte
   modified    revision // the main rev of the last modification
   generations []generation
}

etcd使用revision表示版本,revision有两个字段。main表示事务操作的主版本号,同一事务中发生变更的key共享同一main版本号,sub表示同一事务中发生变更的次数。

// A revision indicates modification of the key-value space.
// The set of changes that share same main revision changes the key-value space atomically.
type revision struct {
   // main is the main revision of a set of changes that happen atomically.
   main int64

   // sub is the sub revision of a change in a set of changes that happen
   // atomically. Each change has different increasing sub revision in that
   // set.
   sub int64
}

etcd中使用generations来记录历史修改数据,其是由generation组成的数组。generation是代的意思,一个key从创建到删除是一个generation,generation记录key从创建到删除中间所有变更的版本信息。当key被删除后再次被操作,会创建新的generation来记录版本信息。

type generation struct {
   ver     int64
   created revision // when the generation is created (put in first revision).
   revs    []revision
}

在此基础上,keyIndex提供了一系列增删查的方法,包括追加版本、基于版本的压缩、基于版本的匹配查询等,都很简单,不一一描述。这里提一下tombstone方法,当etcd的删除key时,会显式调用相应keyIndex的tombstone方法,结束当前generation并开启新的空generation。

func (ki *keyIndex) tombstone(lg *zap.Logger, main int64, sub int64) error {
   if ki.isEmpty() {
      lg.Panic(
         "'tombstone' got an unexpected empty keyIndex",
         zap.String("key", string(ki.key)),
      )
   }
   if ki.generations[len(ki.generations)-1].isEmpty() {
      return ErrRevisionNotFound
   }
   ki.put(lg, main, sub)
   ki.generations = append(ki.generations, generation{})
   keysGauge.Dec()
   return nil
}

treeIndex

理解了keyIndex,再回过头来看index。index的接口定义如下,可以看到index提供了一系列基于key和rev的增删查方法。key的部分由B树来实现,rev的部分由keyIndex来实现。也就是说treeIndex就是在B树上面做了一层封装,封装的内容就是对B树的值keyIndex的操作。

type index interface {
   Get(key []byte, atRev int64) (rev, created revision, ver int64, err error)
   Range(key, end []byte, atRev int64) ([][]byte, []revision)
   Revisions(key, end []byte, atRev int64, limit int) ([]revision, int)
   CountRevisions(key, end []byte, atRev int64) int
   Put(key []byte, rev revision)
   Tombstone(key []byte, rev revision) error
   Compact(rev int64) map[revision]struct{}
   Keep(rev int64) map[revision]struct{}
   Equal(b index) bool

   Insert(ki *keyIndex)
   KeyIndex(ki *keyIndex) *keyIndex
}

kvstore

kvstore是真正意义的支持mvcc的kv存储。store的结构体如下。

ReadView及WriteView是抽象出的读写方法。mu读写锁,但该读写锁并非用来做读写事务的并发保护,而且是将事务操作和非事务操作隔离。

backend以及index构建kv存储的两大部分。currentRev是全局递增的版本号,已经保护该版本号的revMu。

另外还有一些压缩相关的内容,我们先略过,暂时只关心读写相关的内容。

type store struct {
   ReadView
   WriteView

   cfg StoreConfig

   // mu read locks for txns and write locks for non-txn store changes.
   mu sync.RWMutex

   b       backend.Backend
   kvindex index

   le lease.Lessor

   // revMuLock protects currentRev and compactMainRev.
   // Locked at end of write txn and released after write txn unlock lock.
   // Locked before locking read txn and released after locking.
   revMu sync.RWMutex
   // currentRev is the revision of the last completed transaction.
   currentRev int64
   // compactMainRev is the main revision of the last compaction.
   compactMainRev int64

   fifoSched schedule.Scheduler

   stopc chan struct{}

   lg     *zap.Logger
   hashes HashStorage
}

调用Read方法得到一个读事务storeTxnRead,其是在backend.ReadTx上的封装。创建时需要对store.mu和tx.rwmu分别加锁,解锁需要显式调用End方法。backend.ReadTx的提交是由backend在batchInterval是统一提交。

storeTxnRead同时还提供了读方法(range方法),比较简单,先根据key查版本号,再根据版本号查value。其通过版本号保证不会读到更新的值。

type storeTxnRead struct {
   s  *store
   tx backend.ReadTx

   firstRev int64
   rev      int64

   trace *traceutil.Trace
}

func (s *store) Read(mode ReadTxMode, trace *traceutil.Trace) TxnRead {
   s.mu.RLock()
   s.revMu.RLock()
   // For read-only workloads, we use shared buffer by copying transaction read buffer
   // for higher concurrency with ongoing blocking writes.
   // For write/write-read transactions, we use the shared buffer
   // rather than duplicating transaction read buffer to avoid transaction overhead.
   var tx backend.ReadTx
   if mode == ConcurrentReadTxMode {
      tx = s.b.ConcurrentReadTx()
   } else {
      tx = s.b.ReadTx()
   }

   tx.RLock() // RLock is no-op. concurrentReadTx does not need to be locked after it is created.
   firstRev, rev := s.compactMainRev, s.currentRev
   s.revMu.RUnlock()
   return newMetricsTxnRead(&storeTxnRead{s, tx, firstRev, rev, trace})
}

func (tr *storeTxnRead) End() {
   tr.tx.RUnlock() // RUnlock signals the end of concurrentReadTx.
   tr.s.mu.RUnlock()
}

调用Write方法得到一个写事务storeTxnWrite,写事务是在读事务及backend.BatchTx的封装。在写事务的range方法中,会使用最新的版本号进行读,所以可以读到最新的修改。写事务结束时,会将全局的版本号递增。

type storeTxnWrite struct {
   storeTxnRead
   tx backend.BatchTx
   // beginRev is the revision where the txn begins; it will write to the next revision.
   beginRev int64
   changes  []mvccpb.KeyValue
}

func (s *store) Write(trace *traceutil.Trace) TxnWrite {
   s.mu.RLock()
   tx := s.b.BatchTx()
   tx.LockInsideApply()
   tw := &storeTxnWrite{
      storeTxnRead: storeTxnRead{s, tx, 0, 0, trace},
      tx:           tx,
      beginRev:     s.currentRev,
      changes:      make([]mvccpb.KeyValue, 0, 4),
   }
   return newMetricsTxnWrite(tw)
}

func (tw *storeTxnWrite) Range(ctx context.Context, key, end []byte, ro RangeOptions) (r *RangeResult, err error) {
   rev := tw.beginRev
   if len(tw.changes) > 0 {
      rev++
   }
   return tw.rangeKeys(ctx, key, end, rev, ro)
}

func (tw *storeTxnWrite) End() {
   // only update index if the txn modifies the mvcc state.
   if len(tw.changes) != 0 {
      // hold revMu lock to prevent new read txns from opening until writeback.
      tw.s.revMu.Lock()
      tw.s.currentRev++
   }
   tw.tx.Unlock()
   if len(tw.changes) != 0 {
      tw.s.revMu.Unlock()
   }
   tw.s.mu.RUnlock()
}

以上就是etcd mvcc模块的介绍,后续会介绍etcd如何在mvcc的基础上实现事务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/183109.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

红队大量资产指纹探测工具和摄像头漏-洞渗-透和利用工具

红队大量资产指纹探测工具和摄像头漏-洞渗-透和利用工具。 Finger定位于一款红队在大量的资产中存活探测与重点攻-击系统指纹探测工具。在面临大量资产时候Finger可以快速从中查找出重点攻-击系统协助我们快速展开渗-透。 实际效果 URL批量扫描效果如下: 调用api进行资产收集效…

第九层(6):STL之queue

文章目录前情回顾queue概念queue容器需要注意的地方queue类内的构造函数queue类内的赋值操作queue类内的插入操作queue类内的删除操作queue类内的访问queue类内的大小操作下一座石碑🎉welcome🎉 ✒️博主介绍:一名大一的智能制造专业学生&…

设计模式第4式:观察者模式Spring事件监听机制

前言 观察者模式是一种非常重要的设计模式,在JDK和Spring源码中使用非常广泛,而且消息队列软件如kafka、rocketmq等也应用了观察者模式。那么我们就很有必要学习一下观察者模式了。 随后我们来看看大名鼎鼎的事件监听机制,它是基于观察者模…

直波导与微环的耦合——Lumerical仿真1

微环与直波导的耦合的Lumerical仿真的一个记录,包括仿真步骤和一些问题的探究,参考自https://www.bilibili.com/video/BV1tF411z714。 🎁附Lumerical仿真文件 Lumerical第一个仿真一、建立结构1、放置微环结构2、修改结构二、光源1、放置光源…

React是不是MVVM架构?

首先说结论:不是 一、MVVM Model-View-ViewModel:一句话概括MVVM,操作数据,就是操作视图,就是操作DOM。开发者只需要完成包含申明绑定的视图模板,编写ViewModel中业务数据变更逻辑,View层则完…

MySQL(六):redo日志——保证事务的持久性

目录一、redo日志的基本介绍1.1 什么是redo日志1.2 redo日志的格式1.3 redo日志的类型1.4 Mini-Transaction二、redo日志的写入过程2.1 redo log block2.2 redo日志缓冲区2.3 将redo日志写入log buffer2.3 redo日志刷新到磁盘2.5 redo日志文件组2.6 redo日志文件格式2.7 log se…

【MySQL进阶】MySQL触发器详解

序号系列文章7【MySQL基础】运算符及相关函数详解8【MySQL基础】MySQL多表操作详解9【MySQL进阶】MySQL事务详解10【MySQL进阶】MySQL视图详解文章目录前言1,触发器1.1,触发器概述1.2,触发器使用环境2,触发器基本操作2.1&#xff…

CentOS7 安装单机 Kafka

一、单机安装 1、上传压缩文件到服务器、解压 tar -zxvf kafka_2.13-3.3.2.tgz -C /usr/local #解压到 usr/local目录下 进入解压目录下 更名kafka mv kafka_2.13-3.3.2/ kafka-3.3.2 2、配置环境变量 vim /etc/profile export KAFKA_HOME/usr/local/kafka-3.3.2export PATH$PA…

MySQL学习记录(9)InnoDB存储引擎

文章目录6、InnoDB存储引擎6.1、逻辑存储结构6.2、架构6.2.1、概述6.2.2、内存结构6.2.3、磁盘结构6.2.4、后台线程6.3、事务原理6.3.1、事务基础6.3.2、redo log日志6.3.3、undo log日志6.4、MVCC6.4.1、基本概念6.4.2、记录中隐藏字段6.4.3、undo log日志6.4.4、readview6.4.…

【Pytorch项目实战】之图像分类与识别:手写数字识别(MNIST)、普适物体识别(CIFAR-10)

文章目录图像分类与识别(一)实战:基于CNN的手写数字识别(数据集:MNIST)(二)实战:基于CNN的图像分类(数据集:CIFAR-10)图像分类与识别 …

Lua 函数 - 可变参数

Lua 函数 - 可变参数 参考至菜鸟教程。 Lua函数可以接收可变数目的参数,和C语言类似,在函数参数列表中使用三点...表示函数有可变的参数。 function add(...) local s 0 for i, v in ipairs{...} do --> {...} 表示一个由所有变长参数构成的数…

模拟实现C库函数(2)

"烦恼无影踪,丢宇宙~"上一篇的模拟实现了好几个库函数,strlen\strcpy\memcpy\memmove,那么这一篇又会增加几个常用C库函数的模拟实现 memset\itoa\atoi。一、memsetmemset - fill memory with a constant byte#include <string.h>void *memset(void *s, int c,…

机器自动翻译古文拼音 - 十大宋词 - 江城子·乙卯正月二十日夜记梦 苏轼

江城子乙卯正月二十日夜记梦 宋苏轼 十年生死两茫茫&#xff0c;不思量&#xff0c;自难忘。 千里孤坟&#xff0c;无处话凄凉。 纵使相逢应不识&#xff0c;尘满面&#xff0c;鬓如霜。 夜来幽梦忽还乡&#xff0c;小轩窗&#xff0c;正梳妆。 相顾无言&#xff0c;惟有泪千…

uniapp使用及踩坑项目记录

环境准备 下载 HBuilderX 使用命令行创建项目&#xff1a; 一些常识准备 响应式单位rpx 当设计稿宽度为750px的时&#xff0c;1rpx1px。 uniapp中vue文件style不用添加scoped 打包成h5端的时候自动添加上去&#xff0c;打包成 微信小程序端 不需要添加 scoped。 图片的…

SpringDataJpa set()方法自动保存失效

问题描述&#xff1a;springdatajpa支持直接操作对象设置属性进行更新数据库记录的方式&#xff0c;正常情况下&#xff0c;get()得到的对象直接进行set后&#xff0c;即使不进行save操作&#xff0c;也将自动更新数据记录&#xff0c;将改动持久化到数据库中&#xff0c;但这里…

20230126使AIO-3568J开发板在原厂Android11下跑起来

20230126使AIO-3568J开发板在原厂Android11下跑起来 2023/1/26 18:22 1、前提 2、修改dts设备树 3、适配板子的dts 4、&#xff08;修改uboot&#xff09;编译系统烧入固件验证 前提 因源码是直接使用原厂的SDK&#xff0c;没有使用firefly配套的SDK源码&#xff0c;所以手上这…

Linux安装mongodb企业版集群(分片集群)

目录 一、mongodb分片集群三种角色 二、安装 1、准备工作 2、安装 configsvr配置 router配置 shard配置 三、测试 四、整合Springboot 一、mongodb分片集群三种角色 router角色&#xff1a; mongodb的路由&#xff0c;提供入口&#xff0c;使得分片集群对外透明&…

【目标检测论文解读复现NO.27】基于改进YOLOv5的螺纹钢表面缺陷检测

前言此前出了目标改进算法专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读最新目标检测算法论文&#xff0c;…

【工程化之路】Node require 正解

require 实现原理 流程概述 步骤1&#xff1a;尝试执行代码require("./1"). 开始调用方法require.步骤2&#xff1a;此时会得到filename&#xff0c;根据filename 会判断缓存中是否已经加载模块&#xff0c;如果加载完毕直接返回&#xff0c;反之继续执行步骤3&…

python图像处理(laplacian算子)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 和之前的prewitt算子、sobel算子不同,laplacian算子更适合检测一些孤立点、短线段的边缘。因此,它对噪声比较敏感,输入的图像一定要做好噪声的处理工作。同时,laplacian算子设计…