Etcd 介绍与使用(入门篇)

etcd 介绍


etcd 简介


etc (基于 Go 语言实现)在 Linux 系统中是配置文件目录名;etcd 就是配置服务;

etcd 诞生于 CoreOS 公司,最初用于解决集群管理系统中 os 升级时的分布式并发控制、配置文件的存储与分发等问题。基于此,etcd 设计为提供高可用、强一致性的小型** kv 数据存储**服务。项目当前隶属于 CNCF 基金会,被包括 AWS、Google、Microsoft、Alibaba 等大型互联网公司广泛使用;

etcd 是一个可靠的分布式 KV 存储,其底层使用 Raft 算法保证一致性,主要用于共享配置、服务发现、集群监控、leader 选举、分布式锁等场景;

1)共享配置:配置文件的存储与分发,将配置文件存储在 etcd 中,其它节点加载配置文件;如需修改配置文件,则修改后,其它节点只需重新加载即可;

2)服务发现:客户端请求经过 etcd 分发给各个服务端,此时如果增加一个服务端并连接到 etcd,由于客户端在一直监听着 etcd,所以客户端能够快速拉去新增服务端地址,并将客户端请求通过 etcd 下发到新增的服务端;

3)集群监控:客户端会通过 etcd 监控所有的 master、slave 节点,一旦有节点发生宕机,客户端能够及时发现;

4)leader 选举:假设一个 master 节点连接多个 slave 节点,如果 master 节点发生宕机,此时 etcd 会从 slave 节点中选取一个节点作为 master 节点;

5)分布式锁:常规情况下锁的持有者和释放者是进程中的线程,而在分布式情况下,锁的持有者和释放者可以是微服务或进程;

etcd 安装


1)安装 golang 环境;

2)下载并编译安装 etcd;

javascript 复制代码
// 下载源代码
git clone https://gitee.com/k8s_s/etcd.git

// 设置源代理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

// 进入 etcd 目录
cd etcd

// 切换最新分支
git checkout release-3.5

go mod vendor
./build

在 etcd/bin 目录生成对应的执行文件 etcd、etcdctl 和 etcdutl

// 查看 etcd 版本
./etcdctl version

说明:可以到 Gitee - 基于 Git 的代码托管和研发协作平台 网站搜 etcd 下载最新的即可!

执行结果如下:

etcd 使用


etcd 的启动与使用

javascript 复制代码
cd etcd/bin

// 启动 etcd
nohup ./etcd > ./start.log 2>&1 &

// 使用 v3 版本 api
export ETCDCTL_API=3

// ./etcdctl + etcd 命令即可
./etcdctl put key val

执行结果如下所示:

etcd v2 和 v3 比较


扩展:一般情况下一个请求需要建立一条连接,比较浪费资源,所以有了 http + json 通信模式(json 是一种协议),但 json 加解密非常慢;

  • 使用 gRPC + protobuf 取代 http + json 通信,提高通信效率;gRPC 只需要一条连接;http 是每个请求建立一条连接;protobuf(是一种二进制协议所以包体小)加解密比 json 加解密速度得到数量级的提升;包体也更小;
  • v3 使用 lease (租约)替换 key ttl 自动过期机制(lease 将过期日期一致的 key 绑定到实体(该实体被称为 lease),通过检测实体的过期时间达到批量检查 key 过期时间的效果,效率更高);
  • v3 支持事务和多版本并发控制(一致性非锁定读)的磁盘数据库;而 v2 是简单的 kv 内存数据库(可靠性低,一旦服务器宕机数据无法得到保存);
  • v3 是扁平的 kv 结构;v2 是类型文件系统的存储结构;

扩展:

1)文件系统的存储结构

  • /node
  • /node/node1
  • /node/node2
  • /node/node1/sub1
  • /node/node1/sub2

2)扁平的 kv 结构

  • node
  • node1
  • node2
  • node3
  • 使用 get node --prefix 命令获取对应文件

etcd 架构(体系结构)


etcd 体系结构如下所示:

  • boltdb 是一个单机的支持事务的 kv 存储,etcd 的事务是基于 boltdb 的事务实现的;boltdb 为每一个 key 都创建一个索引(B+树);该 B+ 树存储了 key 所对应的版本数据;
  • wal(write ahead log)预写式日志实现事务日志的标准方法;执行写操作前先写日志,跟 mysql 中 redo 类似,wal 实现的是顺序写,而若按照 B+ 树写,则涉及到多次 io 以及随机写;
  • snapshot 快照数据,用于其他节点同步主节点数据从而达到一致性地状态;类似 redis 中主从复制中 rdb 数据恢复;流程:1. leader 生成 snapshot;2. leader 向 follower 发送 snapshot;3. follower 接收并应用 snapshot;gRPC server ectd 集群间以及 client 与 etcd 节点间都是通过 gRPC 进行通讯;

etcd APIs


数据版本号机制


  • term:leader 任期,leader 切换时 term 加一;全局单调递增,64bits;
  • revision:etcd 键空间版本号,key 发生变更,则 revision 加一;全局单调递增,64bits;
  • kvs:
    • create_revision 创建数据时,对应的版本号;
    • mod_revision 数据修改时,对应的版本号;
    • version 当前的版本号;标识该 val 被修改了多少次;

示例分析:

javascript 复制代码
# ./etcdctl put key2 val2
OK
# ./etcdctl get key2
key2
val2
# ./etcdctl get key2 -w json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":9,"raft_term":7},"kvs":[{"key":"a2V5Mg==","create_revision":9,"mod_revision":9,"version":1,"value":"dmFsMg=="}],"count":1}

参数说明:

  • cluster_id:集群 id;
  • member_id:当前 etcd 节点 id;
  • revision:整个 etcd 的版本 id,且只要 key 发生变更(增、删、改),则 revision 加一;全局单调递增,64bits;
  • raft_term:leader 任期,leader 切换时 term 加一;全局单调递增,64bits;
  • kvs:
    • create_revision 创建数据时,对应的版本号;
    • mod_revision 数据修改时,对应的版本号;
    • version 当前的版本号;标识该 val 被修改了多少次;

注:"key":"a2V5Mg==" 和 "value":"dmFsMg==" 是因为值被加密了,在 get 时会对其进行解密!

执行结果:

设置


设置即存储共享配置信息;

javascript 复制代码
NAME:
    put - Puts the given key into the store
    
USAGE:
    etcdctl put [options] <key> <value> (<value> can also be given fromstdin) [flags]
    
DESCRIPTION:
    Puts the given key into the store.
    When <value> begins with '-', <value> is interpreted as a flag.
    Insert '--' for workaround:
    
    $ put <key> -- <value>
    $ put -- <key> <value>
    
    If <value> isn't given as a command line argument and '--ignorevalue' is not specified,this command tries to read the value from standard input.
    
    If <lease> isn't given as a command line argument and '--ignorelease' is not specified,this command tries to read the value from standard input.
    
    For example,
    $ cat file | put <key>
    will store the content of the file to <key>.
    
OPTIONS:
    -h, --help[=false] help for put
        --ignore-lease[=false] updates the key using its current lease
        --ignore-value[=false] updates the key using its current value
        --lease="0" lease ID (in hexadecimal) to attach to thekey
        --prev-kv[=false] return the previous key-value pair beforemodification

语法命令:

javascript 复制代码
put key val

// 存储 key value 的同时返回上一次存储的 key value
put key val --prev-kv

删除


删除 key vla;

javascript 复制代码
NAME:
    del - Removes the specified key or range of keys [key, range_end)
    
USAGE:
    etcdctl del [options] <key> [range_end] [flags]
    
OPTIONS:
        --from-key[=false] delete keys that are greater than or equal to the given key using byte compare

    -h, --help[=false]        help for del
        --prefix[=false]      delete keys with matching prefix
        --prev-kv[=false]     return deleted key-value pairs

语法命令:

javascript 复制代码
del key

// 删除成功,返回 1
// 若 key 不存在,则返回 0

获取


获取 key vla;

javascript 复制代码
NAME:
    get - Gets the key or a range of keys
    
USAGE:
    etcdctl get [options] <key> [range_end] [flags]
    
OPTIONS:
        --consistency="l"             Linearizable(l) or Serializable(s)
        --count-only[=false]          Get only the count
        --from-key[=false]            Get keys that are greater than or equal to the given key using byte compare
    -h, --help[=false]                help for get
        --keys-only[=false]           Get only the keys
        --limit=0                     Maximum number of results
        --order=""                    Order of results; ASCEND or DESCEND(ASCEND by default)
        --prefix[=false]              Get keys with matching prefix
        --print-value-only[=false]    Only write values when using the "simple" output format
        --rev=0                       Specify the kv revision
        --sort-by=""                  Sort target; CREATE, KEY, MODIFY, VALUE, or VERSION

语法命令:

javascript 复制代码
get key

// 获取前缀匹配 key 的所有 key val
get key --prefix   

// 获取字符串小于 key2 的所有 key val
get key key2 

// 获取字符串大于等于 key2 的所有 key val
get key2 --from-key

// 只获取字符串等于 key2 的 key
get key2 --keys-only

// 获取前缀匹配 key 的所有 key
get key --prefix --keys-only

// 获取前缀匹配 key 的前两个 key
get key --prefix --keys-only --limit=2

// 先排序,再获取前缀匹配 key 的前两个 key
get key --prefix --keys-only --limit=2 --sort-by=value

get "小于" 案例

javascript 复制代码
// 获取所有前缀和 key 匹配的 key val
# ./etcdctl get key --prefix
key
val2023
key1
val1
key2
val2
key20
val20
key2024
val2024

// 范围查询,获取 key2 之前(范围区间为左闭右开)的 key val    
# ./etcdctl get key key2
key
val2023
key1
val1

注:比较范围区间时是按字符串进行比较的,如:key、key1、key2、key20、key2024 中只有 key、key1 小于 key2;

执行结果:

get "大于等于" 案例

javascript 复制代码
# ./etcdctl get key --prefix
key
val2023
key1
val1
key2
val2
key20
val20
key2024
val2024
# ./etcdctl get key2 --from-key
key2
val2
key20
val20
key2024
val2024

执行结果:

监听


用来实现监听和推送服务;

javascript 复制代码
NAME:
    watch - Watches events stream on keys or prefixes
    
USAGE:
    etcdctl watch [options] [key or prefix] [range_end] [--] [execcommand arg1 arg2 ...] [flags]
    
OPTIONS:
    -h, --help[=false]                    help for watch     
    -i, --interactive[=false]             Interactive mode
        --prefix[=false]                  Watch on a prefix if prefix is set
        --prev-kv[=false]                 get the previous key-value pair before the event happens 
        --progress-notify[=false]         get periodic watch progress notification from server
        --rev=0                           Revision to start watching

语法命令:

javascript 复制代码
// 监听 key 的变动
watch key    

1) 启两个 session
2) 在 session A 中执行:WATCH key
3) 在 session B 中执行操作 key 的命令,如:PUT key val,DEL key 等,则同时会在 session A 中显示具体操作

// 当前事件发生前先获取前一个 key val
watch key --prev-kv

// 监听多个 key 的变动
watch key --prefix

说明:监听时也可以指定监听范围和版本等信息;

事务


用于分布式锁以及 leader 选举;保证多个操作的原子性;确保多个节点数据读写的一致性;

有关数据版本号信息请参考上述:数据版本号机制 部分;

javascript 复制代码
NAME:
    txn - Txn processes all the requests in one transaction
    
USAGE:
    etcdctl txn [options] [flags]
    
OPTIONS:
    -h, --help[=false]                     help for txn
    -i, --interactive[=false]              Input transaction in interactive mode

事务
1. 比较
    1. 比较运算符 > = < !=
    2. create 获取key的create_revision
    3. mod 获取key的mod_revision
    4. value 获取key的value
    5. version 获取key的修改次数
2. 比较成功,执行下述代码
    1. 成功后可以操作多个 del put get
    2. 这些操作保证原子性
3. 比较失败,执行下述代码
    1. 成功后可以操作多个 del put get
    2. 这些操作保证原子性

语法命令:

javascript 复制代码
TXN if/ then/ else ops

mod 比较案例

javascript 复制代码
# ./etcdctl put key val1995
OK
# ./etcdctl get key -w json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":12,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":12,"version":5,"value":"dmFsMTk5NQ=="}],"count":1}

# ./etcdctl put key val2024
OK
# ./etcdctl get key -w json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":13,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":13,"version":6,"value":"dmFsMjAyNA=="}],"count":1}
# ./etcdctl txn -i
compares:
mod("key")="9"
Error: malformed comparison: mod("key")="9"; got mod("key")  ""
# ./etcdctl txn -i
compares:
mod("key") = "12"

success requests (get, put, del):
get key

failure requests (get, put, del):
get key --rev=12

FAILURE

key
val1995

从上述执行结果来看,代码走的是 比较失败 的逻辑;

注:mod("key") = "12" 等号前后要有空格,不然会报错!

执行结果:

create 比较案例

javascript 复制代码
# ./etcdctl txn -i
compares:
create("key") = "2"

success requests (get, put, del):
get key

failure requests (get, put, del):
del key

SUCCESS

key
val2024

执行结果:

version 比较案例

javascript 复制代码
# ./etcdctl put key val2020
OK
# ./etcdctl get key -w json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":14,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":14,"version":7,"value":"dmFsMjAyMA=="}],"count":1}
# ./etcdctl put key val2023
OK
# ./etcdctl get key -w json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":15,"version":8,"value":"dmFsMjAyMw=="}],"count":1}
# ./etcdctl txn -i
compares:
version("key") = "7"

success requests (get, put, del):
get key

failure requests (get, put, del):
get key --rev=14

FAILURE

key
val2020

执行结果:

租约


用于集群监控以及服务注册发现;

javascript 复制代码
etcdctl lease grant <ttl> [flags]                                 创建一个租约
etcdctl lease keep-alive [options] <leaseID> [flags]              续约
etcdctl lease list [flags]                                        枚举所有的租约
etcdctl lease revoke <leaseID> [flags]                            销毁租约
etcdctl lease timetolive <leaseID> [options] [flags]              获取租约信息

OPTIONS:
    --keys[=false]         Get keys attached to this lease

语法命令:

javascript 复制代码
// 创建一个 100 秒的租约
lease grant 100

// 如果租约创建成功会显示如下输出
lease 694d7b82c54a9309 granted with TTL(100s)

// 将多个 key 绑定到租约
put key1 vla1 --lease=694d7b82c54a9309
put key2 vla2 --lease=694d7b82c54a9309
put key3 vla3 --lease=694d7b82c54a9309

// 获取具有匹配前缀的 key(包括:绑定租约的 key 和未绑定租约的 key)
get key --prefix    

// 输出结果
key1
vla1 
key2 
vla2 
key3 
vla3 

// 销毁租约
lease revoke 694d7b82c54a9309

// 获取具有匹配前缀的 key(因为租约已被销毁,所以此时返回的只有未绑定租约的 key)
get key --prefix 

// 获取租约信息(如果租约未过期,则输出结果会显示租约的剩余日期;如果租约已过期,则显示已过期)
lease timetolive 694d7b82c54a9309

// 输出结果(租约已过期)
lease 694d7b82c54a9309 already expired

// 续约(可以让租约剩余日期一直保持在设定时间;续约前提是当前租约未过期)
lease keep-alive 694d7b82c54a9309


javascript 复制代码
USAGE:
    etcdctl lock <lockname> [exec-command arg1 arg2 ...] [flags]
    
OPTIONS:
    -h, --help[=false]             help for lock
        --ttl=10                   timeout for session

Go 操作 etcd


驱动包安装


不能直接 go get go.etcd.io/etcd/clientv3(官方提供驱动包)不然会报错的;因为 gRPC 版本过新的缘故;

这里我们需要指定 gRPC 的版本信息;

javascript 复制代码
# 指定 gRPC 版本为 v1.26.0
go mod edit --require=google.golang.org/grpc@v1.26.0

# 下载安装 gRPC 驱动包
go get -u -x google.golang.org/grpc@v1.26.0

# 下载安装 etcd 驱动包
go get go.etcd.io/etcd/clientv3

Go 操作 etcd 实例


启动 etcd

1)方式一

javascript 复制代码
nohup ./etcd > ./start.log 2>&1 &

// 查看端口对外开放情况(etcd 默认端口为 2379)
lsof -i:2379

执行结果:

从上述执行结果可知,使用方式一启动时,etcd 的端口号只能在本地连接。

2)方式二

javascript 复制代码
nohup ./etcd --listen-client-urls 'http://0.0.0.0:2379' --advertise-client-urls 'http://0.0.0.0:2379' > ./start.log 2>&1 &

// 查看端口对外开放情况(etcd 默认端口为 2379)
lsof -i:2379

执行结果:

从上述执行结果可知,使用方式一启动时,etcd 的端口号可以被外部连接。

注:使用方式二启动 etcd!


注:如果 etcd 所在机器是公司内部机器,需要把安全组对应端口号放开,即需要放开 2379!


put、get 使用

Go 复制代码
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/coreos/etcd/clientv3"
)

func main() {
    // 创建连接
    cli, err := clientv3.New(clientv3.Config{
       // Endpoints 是一个切片,可同时连接多个服务器
       Endpoints:   []string{"120.92.144.250:2379"},
       DialTimeout: 5 * time.Second, // 连接超时时间
    })
    if err != nil {
       panic(err)
    }

    // 程序执行结束前释放连接资源
    defer cli.Close()

    // v3 通讯服务使用的是 gRPC,需设置超时控制(即如果 put 命令执行后,在超时时间内没有返回结果,则取消 put 命令的执行)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    _, err = cli.Put(ctx, "key", "mark")
    cancel()
    if err != nil {
       panic(err)
    }

    // 获取 key
    ctx, cancel = context.WithTimeout(context.Background(), time.Second)
    /*
      此处的 get 等同于在终端执行 ./etcdctl get key -w json
      输出结果:
      {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":17,"raft_term":7},
       "kvs":[{"key":"a2V5","create_revision":2,"mod_revision":15,"version":8,"value":"dmFsMjAyMw=="}],"count":1}
    */
    resp, err := cli.Get(ctx, "key")
    cancel()
    if err != nil {
       panic(err)
    }

    for _, ev := range resp.Kvs {
       fmt.Printf("%s:%s\n", ev.Key, ev.Value)
    }
}

watch 使用

Go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/coreos/etcd/clientv3"
)

func main() {
    // 创建连接
    cli, err := clientv3.NewFromURL("120.92.144.250:2379")
    if err != nil {
       panic(err)
    }
    defer cli.Close()

    // watch key 的操作
    //watch := cli.Watch(context.Background(), "key")

    // watch 大于等于 key3 的操作,监听对象由第三个参数控制
    watch := cli.Watch(context.Background(), "key", clientv3.WithFromKey())

    for resp := range watch {
       for _, ev := range resp.Events {
          fmt.Printf("Type: %s Key: %s Value: %s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
       }
    }
}

执行上述代码后会阻塞等待其它用户操作 etcd,如下所示:

1)在终端执行 etcd 操作

2)在 go 客户端查看监听情况

lease 使用

Go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/coreos/etcd/clientv3"
)

func main() {
    // 创建连接
    cli, err := clientv3.NewFromURL("120.92.144.250:2379")
    if err != nil {
       panic(err)
    }
    defer cli.Close()

    // 创建租约
    lease, err := cli.Grant(context.Background(), 5)
    if err != nil {
       panic(err)
    }
    fmt.Println("lease id", lease.ID)

    // 把 key-val 绑定到租约
    _, err = cli.Put(context.Background(), "key", "mark", clientv3.WithLease(lease.ID))
    if err != nil {
       panic(err)
    }

    // 续租:长期续租、短期续租
    // 长期续租:不停的续租
    if false {
       ch, err := cli.KeepAlive(context.Background(), lease.ID)
       if err != nil {
          panic(err)
       }
       for {
          recv := <-ch
          fmt.Println("time to live", recv.TTL)
       }
    }
    // 短期续租:只续租一次
    if true {
       res, err := cli.KeepAliveOnce(context.Background(), lease.ID)
       if err != nil {
          panic(err)
       }
       fmt.Println("time to live", res.TTL)
    }
}

lock 使用

Go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/coreos/etcd/clientv3"
    "github.com/coreos/etcd/clientv3/concurrency"
)

func main() {
    // 创建连接
    cli, err := clientv3.New(clientv3.Config{
       Endpoints: []string{"127.0.0.1:2379"},
    })
    if err != nil {
       panic(err)
    }
    defer cli.Close()

    // 创建 session1
    s1, err := concurrency.NewSession(cli, concurrency.WithContext(context.Background()), concurrency.WithTTL(10))
    if err != nil {
       panic(err)
    }
    defer s1.Close()

    // 为 session1 创建锁
    m1 := concurrency.NewMutex(s1, "lock")

    // 创建 session2
    s2, err := concurrency.NewSession(cli, concurrency.WithContext(context.Background()))
    if err != nil {
       panic(err)
    }
    defer s2.Close()

    // 为 session2 创建锁
    m2 := concurrency.NewMutex(s2, "lock")

    // 对 session1 加锁
    if err := m1.Lock(context.Background()); err != nil {
       panic(err)
    }
    fmt.Println("s1 acquired lock")

    // 创建管道
    m2ch := make(chan struct{})

    // 开启协程,对 session2 加锁,但由于已经被 session1 锁住,所以 session2 的加锁操作,阻塞等待
    go func() {
       defer close(m2ch)
       if err := m2.Lock(context.Background()); err != nil {
          panic(err)
       }
    }()

    // session1 释放锁
    if err := m1.Unlock(context.Background()); err != nil {
       panic(err)
    }
    fmt.Println("s1 released lock")

    // 通知 session2 session1 已经释放锁,此时 session2 可执行加锁操作
    <-m2ch
    fmt.Println("s2 acquired lock")
}

注:Go 项目在创建好之后,需要在终端执行:go mod init 项目名称,生成 go.mod 文件。

etcd 存储原理及读写机制


存储原理


etcd 为每个 key 创建一个索引;一个索引对应着一个 B+ 树;B+ 树 key 为 revision,B+ 树节点存储的值为 value;B+ 树存储着 key 的版本信息从而实现了 etcd 的 mvcc;etcd 不会任由版本信息膨胀,通过定期的 compaction 来清理历史数据;

etcd 为了加速索引数据,在内存中维持着一个 B 树;B 树 key 为 key-val 中的 key,value 为该 key 的 revision;示意图如下:

etcd 不同命令执行流程:

  • etcd get 命令执行流程:etcd 在执行 get 获取数据时,先从内存中的 B 树中寻找,如果找不到,再从 B+ 树中寻找,从 B+ 树中找到数据后,将其缓存到 B 树并输出到客户端;
  • etcd put 命令执行流程:etcd 在执行 put 插入或修改数据时,先从内存中的 B 树中寻找,如果找到了,则对其进行修改并将其写入到 B+ 树;

问题:mysql 的 mvcc 是通过什么实现的?

答:undolog;

问题:mysql B+ 树存储什么内容?

答:具体分为聚簇索引和二级索引;

问题:mysql 为了加快索引数据,采用什么数据结构?

答:MySQL 采用自适应 hash 来加速索引;

扩展:B-树和 B+ 树区别?

  • B-树和 B+ 树都是多路平衡搜索树;采用中序遍历的方式会得到一个有序的结构;都是通过 key 的方式来维持树的有序性;
  • B-树一个节点中 n 个元素对应着 n+1 个指针;而 B+ 树一个节点中 n 个元素对应着 n 个指针;
  • B-树每个节点都存储节点信息,B+ 树只有叶子节点存储节点信息,非叶子节点只存储索引信息;
  • B+ 树叶子节点之间通过双向链表连接,对于范围查询速度更快,这样减少了磁盘 io;

读写机制


etcd 是串行写(避免不必要的加锁),并发读;

并发读写时(读写同时进行),读操作是通过 B+ 树 mmap 访问磁盘数据;写操作走日志复制流程;可以得知如果此时读操作走 B 树出现脏读幻读问题;通过 B+ 树访问磁盘数据其实访问的事务开始前的数据,由 mysql 可重复读隔离级别下 MVCC 读取规则可智能避免脏读和幻读问题;

并发读时,可走内存 B 树;

注:由于 etcd 写的时候是先写到内存中的 B 树,然后再写到磁盘上的 B+ 树,因此并发读写时需要读 B+ 树数据,否则容易出现脏读幻读问题;

相关推荐
打鱼又晒网22 分钟前
【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
数据库·mysql
大白要努力!27 分钟前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
tatasix1 小时前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。1 小时前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度1 小时前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮2 小时前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9992 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️2 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98763 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发