Etcd是什么

  • 分布式、可靠的键值存储 (Distributed Reliable Key-Value Store):核心功能是存储键值对数据。
  • 强一致性 (Strong Consistency):使用 Raft 共识算法确保集群中所有节点看到的数据视图在任何时刻都是一致的。写入操作需要大多数节点确认才算成功,读取默认是线性一致性读。
  • 高可用 (Highly Available):数据在集群节点间复制。即使部分节点故障,只要大多数节点存活,整个集群依然能正常提供读写服务。
  • 持久化 (Persistent):数据会持久化到磁盘,即使重启也不会丢失。
  • Watch 机制:客户端可以监听键或范围键的变化(创建、更新、删除),用于实现配置动态更新、服务发现通知等。
  • 租约 (Lease):为键关联一个有时间限制的租约。租约到期(且不续期),关联的键会被自动删除。用于实现分布式锁、临时节点(如服务注册与健康检查)。
  • 事务 (Transaction):支持条件写入(Compare-and-Swap, Compare-and-Delete)的原子操作,保证多个操作的原子性。

为什么需要Etcd

  • 服务发现 (Service Discovery):服务注册自身信息(如 IP:Port)到 etcd,其他服务通过查询 etcd 发现目标服务。
  • 配置中心 (Configuration Management):将应用配置存储在 etcd 中,应用启动时拉取或监听配置变更实现动态配置更新。
  • 分布式锁 (Distributed Lock):协调多个进程/服务对共享资源的互斥访问。
  • Leader 选举 (Leader Election):在分布式系统中选出一个主节点来协调工作。
  • 元数据存储 (Metadata Store):存储集群本身的元数据(如 Kubernetes 的状态存储)。
  • 协调分布式任务 (Coordinating Distributed Tasks):利用 Watch 和 Lease 协调任务执行。

Etcd核心概念

  • Key-Value Pair: 存储的基本单位,键 (key) 和值 (value) 都是字节数组 (在 Go 中是 []byte,客户端库通常也支持字符串)。
  • Revision: 每次修改(增删改)操作都会使全局 Revision 单调递增。它是实现 Watch 和事务的关键。
  • Watch: 监听指定键或键前缀的变化。可以指定从某个 Revision 开始监听。
  • Lease: 一个有时间限制的租约。创建时需要指定 TTL (Time-To-Live)。客户端需要定期 KeepAlive 来续期租约。关联了租约的键在租约过期后会被自动删除。
  • Transaction (Txn): 包含一组条件 (Compare) 和一组操作 (Then/Else)。只有所有条件满足时,Then 中的操作才会原子性执行;否则 Else 中的操作原子性执行。常用条件:检查键的 Revision、Value、版本等。
  • Range: 获取键或键前缀范围内的数据。

搭建Etcd环境(单节点)

docker run -d \
  --name etcd-server \
  -p 2379:2379 \
  -p 2380:2380 \
  -v ~/docker_data/etcd:/etcd-data \
  -e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 \
  -e ETCD_ADVERTISE_CLIENT_URLS=http://localhost:2379 \
  -e ETCD_DATA_DIR=/etcd-data \
  quay.io/coreos/etcd:v3.5.0 \
  etcd
  • 2379是etcd的客户端通信端口(用于接收客户端请求)
  • 2380是etcd的节点间通信端口(用于集群节点间数据同步)
  • ETCD_LISTEN_CLIENT_URLS: 指定 etcd 监听客户端请求的地址和端口

    • http://0.0.0.0:2379 表示监听所有网络接口(允许任意客户端访问)。
    • 若需限制访问,可改为 http://localhost:2379(仅限本机)。
  • ETCD_ADVERTISE_CLIENT_URLS: 向客户端 公告的访问地址(客户端实际使用的连接地址)。

    • 此处设置为 http://localhost:2379,表示客户端需通过 localhost:2379 访问。
    • 注意:若客户端不在本机(如远程访问),需改为宿主机的 IP(如 http://<宿主机IP>:2379),否则无法连接。
  • ETCD_DATA_DIR: 指定 etcd 数据存储目录
  • quay.io/coreos/etcd:v3.5.0: 指定使用的 Docker 镜像。
  • etcd: 容器启动后执行的命令。

使用etcdctl

进入容器

docker exec -it etcd-server sh

基础操作

  • Put (设置键值对)

    etcdctl put mykey "Hello etcd!"
    # 输出: OK
  • Get (获取键值)

    etcdctl get mykey
    # 输出:
    # mykey
    # Hello etcd!
  • Get with Prefix (前缀查询)

    etcdctl put /service/service1/server1 "10.0.0.1:8080"
    etcdctl put /service/service1/server2 "10.0.0.2:8080"
    etcdctl get /service/service1/ --prefix
    # 输出:
    # /service/service1/server1
    # 10.0.0.1:8080
    # /service/service1/server2
    # 10.0.0.2:8080
  • Delete (删除键)

    etcdctl del mykey
    # 输出: 1 (删除的 key 数量)
  • Delete with Prefix (前缀删除)

    etcdctl del /service/service1/ --prefix
    # 输出: 2 (删除的 key 数量)

Watch (监听变化)

  • 在一个终端启动监听 /mywatchkey

    etcdctl watch /mywatchkey
  • 在另一个终端操作:

    etcdctl put /mywatchkey "value changed"
  • 观察第一个终端的输出。

使用Golang

etcd客户端库

安装

go get go.etcd.io/etcd/client/v3@latest

核心类型和概念

  • clientv3.Client: 代表与 etcd 集群的连接。是几乎所有操作的入口点。使用 clientv3.New 创建。
  • context.Context: 用于传递请求的截止时间、取消信号等。所有客户端方法都需要 context.Context 参数。
  • clientv3.Put, clientv3.Get, clientv3.Delete: 对应基础操作的请求结构体。
  • clientv3.Op: 表示一个原子操作(Put, Get, Delete, Txn)。用于构建事务。
  • clientv3.Txn: 表示一个事务。包含条件 (If) 和操作 (Then, Else)。
  • clientv3.Lease: 租约操作的接口 (Grant, Revoke, KeepAlive)。
  • clientv3.Watcher: 用于监听键变化的接口 (Watch)。
  • clientv3.WatchResponse: Watch 返回的通道中接收到的响应,包含事件列表 (Events)。
  • mvccpb.KeyValue: 表示从 etcd 获取到的一个键值对。
  • mvccpb.Event: 表示一个键值变化事件 (PUTDELETE)。

使用Golang实现基础操作

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    clientv3 "go.etcd.io/etcd/client/v3"
)

func main() {
    // 1.创建etcd客户端
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"}, // etcd服务地址
        DialTimeout: 5 * time.Second,            // 连接超时时间
    })
    if err != nil {
        log.Fatal("Failed to connect to etcd:", err)
    }
    defer cli.Close()
    fmt.Println("Connected to etcd successfully!")

    ctx := context.Background()

    // 2.Put(写入键值对)
    putResp, err := cli.Put(ctx, "learn_etcd_go_key", "Hello from Go!")
    if err != nil {
        log.Fatal("Put failed:", err)
    }
    fmt.Printf("Put response Header.Revision: %d\n", putResp.Header.Revision)

    // 3.Get(读取键值对)
    getResp, err := cli.Get(ctx, "learn_etcd_go_key")
    if err != nil {
        log.Fatal("Get failed:", err)
    }
    if len(getResp.Kvs) > 0 {
        kv := getResp.Kvs[0]
        fmt.Printf("Got key: %s, value: %s, revision: %d\n", string(kv.Key), string(kv.Value), kv.ModRevision)
    } else {
        fmt.Println("Key 'learn_etcd_go_key' not found")
    }

    // 4.Watch(监听键的变化 - 在后台goroutine)
    watchKey := "learn_etcd_go_watch_key"
    watchChan := cli.Watch(ctx, watchKey)
    go func() {
        fmt.Println("Starting to watch key:", watchKey)
        for watchResp := range watchChan {
            for _, event := range watchResp.Events {
                fmt.Printf("Watch Event - Type: %s, Key: %s, Value: %s, Revision: %d\n",
                    event.Type, string(event.Kv.Key), string(event.Kv.Value), event.Kv.ModRevision)
            }
        }
    }()

    // 5.模拟被Watch的键的变化(在主goroutine)
    time.Sleep(time.Second) // 给watch goroutine一点启动时间
    _, err = cli.Put(ctx, watchKey, "First Value")
    if err != nil {
        log.Fatal("Put (watch target) failed:", err)
    }
    time.Sleep(1 * time.Second)
    _, err = cli.Put(ctx, watchKey, "Updated value")
    if err != nil {
        log.Fatal("Put (watch target update) failed:", err)
    }
    time.Sleep(1 * time.Second)
    _, err = cli.Delete(ctx, watchKey)
    if err != nil {
        log.Fatal("Delete (watch target) failed:", err)
    }

    // 6.等待一段时间观察 watch 输出 (在实际应用中,watch 通常是长期运行的)
    time.Sleep(2 * time.Second)
}
最后修改:2025 年 06 月 22 日
如果觉得我的文章对你有用,请随意赞赏