Etcd 是 CoreOS 基于 Raft 开发的分布式 key-value 存储,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。
角色
Term (任期)
复制状态机:保证数据的一致性
心跳(heartbeat) 和 超时机制(timeout)
在Raft算法中,有两个timeout机制来控制leader选举:
触发条件:
选举过程:
限制条件:
当前 Leader 收到客户端的日志(事务请求)后先把该日志追加到本地的 Log 中,然后通过 heartbeat 把该 Entry 同步给其他 Follower,Follower 接收到日志后记录日志然后向 Leader 发送 ACK,当 Leader 收到大多数(n/2+1)Follower 的 ACK 信息后将该日志设置为已提交并追加到本地磁盘中,通知客户端并在下个 heartbeat 中 Leader 将通知所有的 Follower 将该日志存储在自己的本地磁盘中。
示例:
Client向Leader提交指令(如:SET 5),Leader收到命令后,将命令追加到本地日志中,该目录状态处于"uncommitted",复制状态机不会执行该命令
Leader将命令(SET 5)并发复制给其他节点,并等待其他节点将命令写入到日志中,如果此时有些节点失败或者慢,Leader节点会一直重试,直到半数以上节点将该命令写入到了日志中。之后Leader节点就提交命令,并返回给Client节点
Leader提交命令后,下一次的心跳包中会通知其他follower也来提交这条命令。收到Leader的消息后,就将命令应用到状态机中(State Machine),最终保证每个节点的数据一致。
安全性是用于保证每个节点都执行相同序列的安全机制,如当某个 Follower 在当前 Leader commit Log 时变得不可用了,稍后可能该 Follower 又会被选举为 Leader,这时新 Leader 可能会用新的 Log 覆盖先前已 committed 的 Log,这就是导致节点执行不同序列;Safety 就是用于保证选举出来的 Leader 一定包含先前 committed Log 的机制;
选举安全性(Election Safety):每个任期(Term)只能选举出一个 Leader
Leader 完整性(Leader Completeness):指 Leader 日志的完整性,当 Log 在任期 Term1 被 Commit 后,那么以后任期 Term2、Term3… 等的 Leader 必须包含该 Log;Raft 在选举阶段就使用 Term 的判断用于保证完整性:当请求投票的该 Candidate 的 Term 较大或 Term 相同 Index 更大则投票,否则拒绝该请求。
Leader 失效:其他没有收到 heartbeat 的节点会发起新的选举,而当 Leader 恢复后由于步进数小会自动成为 follower(日志也会被新 leader 的日志覆盖)
follower 节点不可用:follower 节点不可用的情况相对容易解决。因为集群中的日志内容始终是从 leader 节点同步的,只要这一节点再次加入集群时重新从 leader 节点处复制日志即可。
多个 candidate:冲突后 candidate 将随机选择一个等待间隔(150ms ~ 300ms)再次发起投票,得到集群中半数以上 follower 接受的 candidate 将成为 leader
WAL:Write Ahead Log,预写式日志。它的最大作用是记录了整个数据变化的全部历程。在etcd中,所有数据的修改在提交前,都要先写入到WAL。
WAL 数据存储优势:
wal 日志是二进制的,解析出来后是以上数据结构 LogEntry,其构成:LogEntry: type|term|index|data
type: 有两种, 0 表示 Normal,1 表示 ConfChange(ConfChange 表示 Etcd 本身的配置变更同步,比如有新的节点加入等)
term:每个 term 代表一个主节点的任期,每次主节点变更 term 就会变化。
index:这个序号是严格有序递增的,代表变更序号
data:二进制格式,将 raft request 对象的 pb 结构整个保存下。Etcd 源码下有个 tools/etcd-dump-logs,可以将 wal 日志 dump 成文本查看,可以协助分析 raft 协议。
raft 协议本身不关心应用数据,也就是 data 中的部分,一致性都通过同步 wal 日志来实现,每个节点将从主节点收到的 data apply 到本地的存储,raft 只关心日志的同步状态,如果本地存储实现的有 bug,比如没有正确的将 data apply 到本地,也可能会导致数据不一致。
日志名称:
WAL: $seq-$index.wal
Snapshot: $term-$index.snap
cd /var/lib/etcd/default.etcd/member
tree
.
├── snap
│ ├── 0000000000000023-00000000000186a1.snap
│ └── db
└── wal
├── 0000000000000000-0000000000000000.wal
├── 0000000000000001-000000000001677b.wal
├── 0.tmp
└── 1.tmp
etcd-raft中的snapshot,其主要功能是为了回收日志占用的存储空间(包括内存和磁盘)。
etcd-raft中的snapshot代表了应用的状态数据,而执行snapshot的动作也就是将应用状态数据持久化存储,这样,在该snapshot之前的所有日志便成为无效数据,可以删除。。
Etcd v3 store 分为两部分:
多版本记录:
etcdctl txn <<<'
put key1 "v1"
put key2 "v2"
'
etcdctl txn <<<'
put key1 "v12"
put key2 "v22"
'
# boltdb
rev={3 0}, key=key1, value="v1"
rev={3 1}, key=key2, value="v2"
rev={4 0}, key=key1, value="v12"
rev={4 1}, key=key2, value="v22"
# revision 主要由两部分组成,第一部分 main rev,每次事务进行加一,第二部分 sub rev,同一个事务中的每次操作加一。
watcherGroup 状态:
当 Etcd 收到客户端的 watch 请求,如果请求携带了 revision 参数,则比较请求的 revision 和 store 当前的 revision,如果大于当前 revision,则放入 synced 组中,否则放入 unsynced 组。同时 Etcd 会启动一个后台的 goroutine 持续同步 unsynced 的 watcher,然后将其迁移到 synced 组。
接口通过 grpc 提供 rpc 接口,放弃了 v2 的 http 接口。优势是长连接效率提升明显,缺点是使用不如以前方便,尤其对不方便维护长连接的场景。
废弃了原来的目录结构,变成了纯粹的 kv,用户可以通过前缀匹配模式模拟目录。
内存中不再保存 value,同样的内存可以支持存储更多的 key。
watch 机制更稳定,基本上可以通过 watch 机制实现数据的完全同步。
提供了批量操作以及事务机制,用户可以通过批量事务请求来实现 Etcd v2 的 CAS 机制(批量事务支持 if 条件判断)。
$ cat > docker-compose.yml <<EOF
version: "3"
networks:
etcd_net:
driver: bridge
volumes:
etcd1_data:
etcd2_data:
etcd3_data:
services:
etcd1:
image: "bitnami/etcd:3.5.1"
container_name: etcd1
restart: always
ports:
- "12380:2380"
- "12379:2379"
networks:
- etcd_net
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd1
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd1:2379
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd1:2380
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- etcd1_data:/bitnami/etcd
etcd2:
image: "bitnami/etcd:3.5.1"
container_name: etcd2
restart: always
ports:
- "22380:2380"
- "22379:2379"
networks:
- etcd_net
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd2
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd2:2379
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd2:2380
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- etcd2_data:/bitnami/etcd
etcd3:
image: "bitnami/etcd:3.5.1"
container_name: etcd3
restart: always
ports:
- "32380:2380"
- "32379:2379"
networks:
- etcd_net
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd3
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd3:2379
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- etcd3_data:/bitnami/etcd
EOF
$ docker-compose up -d
$ docker exec -it eaf4ca35e233 /bin/sh
# etcdctl member list
ade526d28b1f92f7, started, etcd1, http://etcd1:2380, http://etcd1:2379, false
bd388e7810915853, started, etcd3, http://etcd3:2380, http://etcd3:2379, false
d282ac2ce600c1ce, started, etcd2, http://etcd2:2380, http://etcd2:2379, false
# 1. 安装
wget https://github.com/etcd-io/etcd/releases/download/v3.5.1/etcd-v3.5.1-linux-arm64.tar.gz
tar zxvf etcd-v3.5.1-linux-arm64.tar.gz
mv etcd-v3.5.1-linux-arm64/etcd* /usr/local/bin/
mkdir -p /etc/etcd
# 2. 配置
cat > /etc/etcd/etcd.conf.yml <<EOF
name: 'etcd-1'
data-dir: /var/lib/etcd/default.etcd
wal-dir: ''
snapshot-count: 10000
heartbeat-interval: 100
election-timeout: 1000
quota-backend-bytes: 0
listen-peer-urls: 'http://192.168.3.191:2380'
listen-client-urls: 'http://localhost:2379,http://192.168.3.191:2379'
max-snapshots: 5
max-wals: 5
cors: ''
initial-advertise-peer-urls: 'http://192.168.3.191:2380'
advertise-client-urls: 'http://localhost:2379,http://192.168.3.191:2379'
discovery: ''
discovery-fallback: 'proxy'
discovery-proxy: ''
discovery-srv: ''
initial-cluster: 'etcd-1=http://192.168.3.191:2380,etcd-2=http://192.168.3.192:2380,etcd-3=http://192.168.3.193:2380'
initial-cluster-token: 'etcd-cluster'
initial-cluster-state: 'new'
strict-reconfig-check: false
enable-v2: true
enable-pprof: false
proxy: 'off'
proxy-failure-wait: 5000
proxy-refresh-interval: 30000
proxy-dial-timeout: 1000
proxy-write-timeout: 5000
proxy-read-timeout: 0
client-transport-security:
cert-file: ''
key-file: ''
client-cert-auth: false
trusted-ca-file: ''
auto-tls: false
peer-transport-security:
cert-file: ''
key-file: ''
client-cert-auth: false
trusted-ca-file: ''
auto-tls: false
self-signed-cert-validity: 1
log-level: info
logger: zap
log-outputs: [stderr]
force-new-cluster: false
auto-compaction-mode: periodic
auto-compaction-retention: "1"
EOF
# 3. 启动
cat > /lib/systemd/system/etcd.service << EOF
[Unit]
Description=Etcd Server
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/local/bin/etcd --config-file=/etc/etcd/etcd.conf.yml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start etcd
systemctl status etcd
$ etcdctl member list -w table
+------------------+---------+--------+---------------------------+-------------------------------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+-------------------------------------------------+------------+
| 12971f2c752331ba | started | etcd-3 | http://192.168.3.193:2380 | http://192.168.3.193:2379,http://localhost:2379 | false |
| 7fe4b7b1994a2a9f | started | etcd-1 | http://192.168.3.191:2380 | http://192.168.3.191:2379,http://localhost:2379 | false |
| 96c03e6d8f6451f3 | started | etcd-2 | http://192.168.3.192:2380 | http://192.168.3.192:2379,http://localhost:2379 | false |
+------------------+---------+--------+---------------------------+-------------------------------------------------+------------+
$ etcdctl endpoint status --write-out=table
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 127.0.0.1:2379 | 7fe4b7b1994a2a9f | 3.5.1 | 20 kB | true | false | 2 | 9 | 9 | |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
$ etcdctl endpoint health --write-out=table
+----------------+--------+------------+-------+
| ENDPOINT | HEALTH | TOOK | ERROR |
+----------------+--------+------------+-------+
| 127.0.0.1:2379 | true | 5.796647ms | |
+----------------+--------+------------+-------+
--quota-backend-bytes
: 当后端数据空间大小超过配额时发出告警,集群进入维护模式,只支持读取和删除。在释放足够空间后,告警解除,集群恢复正常
--auto-compaction-mode=revision --auto-compaction-retention=1000
automatically Compact
on "latest revision" - 1000
every 5-minute (when latest revision is 30000, compact on revision 29000)
--auto-compaction-mode=periodic --auto-compaction-retention=10h
automatically Compact
with 10-hour retention windown for every 1-hour. Now, Compact
happens, for every 1-hour but still with 10-hour retention window.
0Hr (rev = 1)
1hr (rev = 10)
...
8hr (rev = 80)
9hr (rev = 90)
10hr (rev = 100, Compact(1))
11hr (rev = 110, Compact(10))
...
Whether compaction succeeds or not, this process repeats for every 1/10 of given compaction period. If compaction succeeds, it just removes compacted revision from historical revision records
Etcd 的数据目录:
snap:存放快照,Etcd 为防止 WAL 文件过多会创建快照。
wal:存放预写式日志,其最大的作用是记录整个数据变化的全部历程。在 Etcd 中,所有数据的修改在提交前,都要先写入 WAL 中。使用 WAL 进行数据的存储使得 Etcd 拥有故障快速回复和数据回滚这两个重要功能。
故障快速恢复:如果你的数据遭到破坏,就可以通过执行所有 WAL 中记录的修改操作,快速从最原始的数据恢复到数据损坏之前的状态。
数据回滚(undo)/ 重做(redo):因为所有的修改操作都被记录在 WAL 中,所以进行回滚或重做时,只需要反向或正向执行日志中的操作即可。
既然有了 WAL 实时存储所有的变更,那么为什么还需要做快照呢?
首次启动时,Etcd 会把启动的配置信息存储到 data-dir 参数指定的数据目录中。配置信息包括本地节点的 ID、集群 ID 和初始时集群信息。用户需要避免从一个过期的数据目录中重新启动 Etcd,因为使用过期的数据目录启动的节点会与集群的其他节点产生不一致(例如,重启之后又向 Leader 节点申请这个信息)的问题。所以,为了最大化保障集群的安全性,一旦有任何数据存在损坏或丢失的可能性,就应该包这个节点从集群中移除,任何加入一个不带数据目录的新节点。
悲观锁和乐观锁:
“悲观锁”:Pessimistic Concurrency Control(PCC)它可以阻止一个事务影响其他用户来修改数据。如果一个事务执行的操作对某行数据应用了锁,那么只有在这个事务将锁释放之后,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
“乐观锁”:它假设多用户并发的事务在处理时彼此之间不会互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务都会先检查在该事物读取数据之后,在没有其他事务又修改了该数据。如果其他事务有更新的话,那么正在提交的事务会进行回滚。
MVCC: 多版本并发控制(Multiversion Concurrency Control)它能够与两者很好地结合以增加事务的并发量。MVCC 的每一个写操作都会创建一个新的版本的数据,读操作会从有限多个版本的数据中挑选一个 “最适合”(要么是最新版本,要么是指定版本)的结果直接返回。通过这种方式,读写操作之间的冲突就不再需要收到关注。因此,如何管理和高效地选取数据的版本就成了 MVCC 需要解决的主要问题。