一:什么是etcd
一句话定义 :etcd 是一个分布式、强一致性 的键值(Key-Value)存储系统,主要用于共享配置和服务发现,是云原生体系中的"基石型"基础设施。
深入理解:
-
分布式:数据在多台机器(节点)上复制和存储,提供高可用性。即使部分节点故障,集群整体仍能正常工作。
-
强一致性 :基于 Raft 共识算法,确保集群中所有节点看到的数据顺序和内容完全一致。这是 etcd 最核心的特性,也是 Kubernetes 等系统依赖它的根本原因。
-
键值存储 :数据模型非常简单,就是
key->value。key以目录结构组织(如/app/database/config),支持前缀查询。 -
核心用途:
-
服务发现:微服务或容器可以将自己的地址注册到 etcd,其他服务通过查询 etcd 来发现它们。
-
配置中心:将分布式系统的配置信息集中存储在 etcd 中,配置变更可实时、一致地通知到所有节点。
-
分布式锁:利用 etcd 的强一致性,可以实现安全的分布式锁(通过 Lease 和事务)。
-
领导者选举:在分布式主从系统中,多个节点可以通过竞争同一个 Key 来选举主节点。
-
-
关键地位 :etcd 是 Kubernetes 的"大脑",存储了集群的所有元数据(如 Pod、Node、ConfigMap 等)和状态。Kubernetes API Server 是唯一直接与 etcd 交互的组件。
一个比喻 :etcd 就像一个高度可靠、实时同步的分布式"电话簿"。任何服务都可以去查询(获取配置)、登记自己(服务注册),并且任何变更都会立刻、准确地通知到所有关心这个变更的服务
二 etcd架构

2.1客户端层
官方提供了 clientv3 库(Go语言),以及其他语言的客户端 SDK。
客户端负责与 etcd 集群建立连接、处理请求重试、负载均衡等
2.2API 层
定义了 etcd 的核心操作接口,如键值空间操作、Watch 监听、租约管理
对外暴露 gRPC API (核心)和 HTTP/JSON API(兼容)
节点内部之间使用Raft API
2.3Raft 共识层
这是 etcd 实现强一致性的心脏 。
* Raft 算法 :将集群中的节点分为三种角色:
* Leader :唯一处理所有客户端写请求的节点,并将写操作复制到其他节点。
* Follower :被动接收来自 Leader 的日志复制请求,并投票选举新的 Leader。
* Candidate :在选举过程中产生的临时角色。
* 所有写请求都必须经过 Leader,由 Leader 将其作为日志条目复制给大多数 Follower 节点,在确认"日志提交"后才应用到状态机,并返回给客户端。这个过程保证了数据的强一致性。
2.4存储层
WAL :预写式日志。所有对数据的修改在应用到内存状态机之前,都会顺序、持久化 地写入 WAL 文件。这是保证数据不丢失和崩溃恢复的关键。
快照 :随着 WAL 日志增长,为了快速恢复和压缩日志,etcd 会定期将内存中的完整数据状态持久化为一个快照文件。
BoltDB :实际的键值存储引擎。etcd 将持久化的键值数据存储在一个单机的、嵌入式的 BoltDB 数据库中。注意:从 etcd v3 开始,所有数据都存储在 BoltDB 中,内存中只维护了键的索引
2.5 数据模型(v3):
多版本并发控制:每个键(Key)都保留历史版本。当更新一个 Key 时,旧值不会被覆盖,而是生成一个新版本。这为 Watch、事务等提供了基础。
租约:可以为 Key 绑定一个具有 TTL(生存时间)的租约。租约到期后,所有绑定它的 Key 会被自动删除。这是实现服务健康检查和分布式锁的核心机制。
2.6 etcd和zookeeper对比
| 对比维度 | etcd | ZooKeeper |
|---|---|---|
| 核心定位 | 分布式、强一致的键值存储,专注于配置共享与服务发现。 | 分布式协调服务,提供构建分布式应用的基础原语。 |
| 数据模型 | 扁平化的键值对(key-value)模型,以目录形式组织键,支持范围查询。 | 树形的分层命名空间(类似文件系统),每个节点(znode)可存储少量数据并拥有子节点。 |
| 一致性协议 | Raft协议。设计更易理解和实现,在开源社区应用广泛。 | Zab协议(Zookeeper Atomic Broadcast)。经过大量生产环境长期验证。 |
| 性能特点 | 读写性能均衡,写入性能通常被认为更优。 | 在以读为主(读写比约10:1)的场景下表现出色。 |
| API与客户端 | 提供简洁的gRPC API 和HTTP/JSON接口,官方维护clientv3库。 |
提供原生Java和C客户端,API围绕其树形模型设计(如创建、删除节点)。 |
| 生态与社区 | 云原生(Cloud Native)生态的基石 ,是Kubernetes唯一默认的元数据存储,社区活力强。 | 大数据生态的标准配置 ,被Hadoop、Kafka、HBase等广泛集成,成熟稳定。 |
| 典型场景 | K8s等容器编排平台的配置存储、服务发现、集群状态管理。 | 分布式锁、领导人选举、配置管理、服务命名。 |
| 容错与选举 | 基于Raft选举,选举过程较快(毫秒级),集群重新收敛迅速。 | 同样基于多数派选举,但选举过程可能较长(秒级),期间可能影响服务可用性 |
三部署
3.1 单节点部署
3.1.1 下载包
ETCD_VERSION=$(curl -s https://api.github.com/repos/etcd-io/etcd/releases/latest | grep tag_name | cut -d '"' -f 4)
wget https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz
# 解压并移动至系统路径
tar -xzf etcd-${ETCD_VERSION}-linux-amd64.tar.gz
cd etcd-${ETCD_VERSION}-linux-amd64
mv etcd etcdctl /mnt/d/ubuntu_dir/etcd/bin/
3.1.2运行
nohup /mnt/d/ubuntu_dir/etcd/bin/etcd &
/mnt/d/ubuntu_dir/etcd/bin/etcdctl put name xiaodong
/mnt/d/ubuntu_dir/etcd/bin/etcdctl get name
执行结果

3.2 集群部署
准备三台机器

3.2.1所有节点均需要安装etcd。
1 #创建etcd日志保存目录
2 mkdir ‐p /var/log/etcd/
3 #创建单独的etcd数据目录
4 mkdir ‐p /data/etcd
3.2.2创建集群发现
1 #使用公共etcd发现服务
2 [root@192‐168‐65‐206 ~]# curl https://discovery.etcd.io/new?size=3
3 #生成的url
4 https://discovery.etcd.io/ef89922e093e9d5ff5ae5c8ec481f4e3
size为集群节点数量,若未指定数量,则默认位3
3.2.3启动集群
每个成员必须指定不同的名称标志,否则发现将因重复的名称而失败
1 # etcd1
2 etcd ‐‐name etcd1 ‐‐data‐dir /data/etcd \
3 ‐‐initial‐advertise‐peer‐urls http://192.168.65.206:2380 \
4 ‐‐listen‐peer‐urls http://192.168.65.206:2380 \
5 ‐‐listen‐client‐urls http://192.168.65.206:2379,http://127.0.0.1:2379 \
6 ‐‐advertise‐client‐urls http://192.168.65.206:2379 \
7 ‐‐discovery https://discovery.etcd.io/ef89922e093e9d5ff5ae5c8ec481f4e3
8
9 # etcd2
10 etcd ‐‐name etcd2 ‐‐data‐dir /data/etcd \
11 ‐‐initial‐advertise‐peer‐urls http://192.168.65.209:2380 \
12 ‐‐listen‐peer‐urls http://192.168.65.209:2380 \
13 ‐‐listen‐client‐urls http://192.168.65.209:2379,http://127.0.0.1:2379 \
14 ‐‐advertise‐client‐urls http://192.168.65.209:2379 \
15 ‐‐discovery https://discovery.etcd.io/ef89922e093e9d5ff5ae5c8ec481f4e3
16
17 # etcd3
18 etcd ‐‐name etcd3 ‐‐data‐dir /data/etcd \19 ‐‐initial‐advertise‐peer‐urls http://192.168.65.210:2380 \
20 ‐‐listen‐peer‐urls http://192.168.65.210:2380 \
21 ‐‐listen‐client‐urls http://192.168.65.210:2379,http://127.0.0.1:2379 \
22 ‐‐advertise‐client‐urls http://192.168.65.210:2379 \
23 ‐‐discovery https://discovery.etcd.io/ef89922e093e9d5ff5ae5c8ec481f4e3
上面是通过命令行方式, 也可以通过配置文件的形式,准备一个配置文件etcd.conf
name: 'etcd-01'
data-dir: '/mnt/d/ubuntu_dir/etcd/data'
listen-client-urls: 'http://192.168.65.206:2379'
advertise-client-urls: 'http://192.168.65.206:2379'
listen-peer-urls: 'http://192.168.65.206:2380'
initial-advertise-peer-urls: 'http://192.168.65.206:2380'
# 注意:所有三个节点的 initial-cluster 字符串必须完全一致
initial-cluster: 'etcd-01=http://192.168.65.206:2380,etcd-02=http://192.168.65.209:2380,etcd-03=http://192.168.65.210:2380'
initial-cluster-state: 'new'
启动:
nohup /mnt/d/ubuntu_dir/etcd/bin/etcd --config-file /mnt/d/ubuntu_dir/etcd/conf/etcd.conf &
其他节点也按如上操作
3.2.4集群检测
etcdctl member list
执行结果

四 基本命令
4.1 增删改查
# 默认走 v2 API,显式切到 v3
export ETCDCTL_API=3
# 增/改(覆盖)
etcdctl put /app/config/cache_ttl 60
# 查(单 key)
etcdctl get /app/config/cache_ttl
# 查(前缀)
etcdctl get /app/config --prefix
# 删(单 key)
etcdctl del /app/config/cache_ttl
# 删(前缀)
etcdctl del /app/config --prefix
# 监听
etcdctl watch /app/config --prefix
# 带租约(TTL = 10s)
lease=`etcdctl lease grant 10 | awk '{print $2}'`
etcdctl put /app/lock/leader "node1" --lease=$lease
# 续租
etcdctl lease keep-alive $lease
# 释放
etcdctl lease revoke $lease
4.2 事务(CAS)
# 仅当 key 版本号 = 2 时才更新
etcdctl txn <<EOF
version("/app/config/cache_ttl") = "2"
put /app/config/cache_ttl 120
EOF
4.3 Go 代码(官方 clientv3)
package main
import (
"context"
"fmt"
"go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil { panic(err) }
defer cli.Close()
ctx := context.Background()
// 增/改
_, err = cli.Put(ctx, "/demo/key", "hello etcd")
if err != nil { panic(err) }
// 查
resp, err := cli.Get(ctx, "/demo/key")
if err != nil { panic(err) }
fmt.Printf("Value=%s\n", resp.Kvs[0].Value)
// 监听
watchCh := cli.Watch(ctx, "/demo/key", clientv3.WithPrefix())
for wresp := range watchCh {
for _, ev := range wresp.Events {
fmt.Printf("Type=%s Key=%s Value=%s\n",
ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}
