ETCD学习笔记

etcd是什么

etcd是一个分布式、高可用的键值存储系统,常用于配置中心、服务注册、Leader选举等场景。

分布式系统

采用etcd作为分布式一致性KV存储,它基于Raft算法,可以保证在主从节点之间数据一致性。我们常把服务注册信息、配置中心的数据保存在etcd中,多个服务节点通过watch等机制保持一致状态。

分布式锁

数据一致性

Raft算法

代码实例

go 复制代码
package main

import (
	"context"
	"log"
	"time"

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

// 服务注册
func registerService(client *clientv3.Client, serviceName, serviceAddr string, ttl int64) error {
	// 创建租约
	leaseResp, err := client.Grant(context.TODO(), ttl)
	if err != nil {
		return err
	}

	// 服务路径
	key := "/services/" + serviceName + "/" + serviceAddr

	// 将服务地址注册到etcd,并与租约关联
	_, err = client.Put(context.TODO(), key, serviceAddr, clientv3.WithLease(leaseResp.ID))
	if err != nil {
		return err
	}

	// 自动续约
	ch, err := client.KeepAlive(context.TODO(), leaseResp.ID)
	if err != nil {
		return err
	}

	// 处理续约应答
	go func() {
		for range ch {
			// 续约成功
			log.Printf("Service %s renewed", serviceAddr)
		}
		log.Printf("Service %s lease expired", serviceAddr)
	}()

	return nil
}

// 服务发现
func discoverServices(client *clientv3.Client, serviceName string) ([]string, error) {
	// 前缀查询
	resp, err := client.Get(context.TODO(), "/services/"+serviceName+"/", clientv3.WithPrefix())
	if err != nil {
		return nil, err
	}

	var services []string
	for _, kv := range resp.Kvs {
		services = append(services, string(kv.Value))
	}

	return services, nil
}

// 监听服务变化
func watchServices(client *clientv3.Client, serviceName string) {
	rch := client.Watch(context.TODO(), "/services/"+serviceName+"/", clientv3.WithPrefix())
	
	for wresp := range rch {
		for _, ev := range wresp.Events {
			switch ev.Type {
			case clientv3.EventTypePut:
				log.Printf("Service added: %s", ev.Kv.Value)
			case clientv3.EventTypeDelete:
				log.Printf("Service removed: %s", ev.Kv.Key)
			}
		}
	}
}

func main() {
	// 连接etcd
	client, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"}, // etcd服务器地址
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatalf("Failed to connect to etcd: %v", err)
	}
	defer client.Close()

	// 注册服务
	serviceName := "user-service"
	serviceAddr := "127.0.0.1:8080"
	err = registerService(client, serviceName, serviceAddr, 10) // 10秒租约
	if err != nil {
		log.Fatalf("Failed to register service: %v", err)
	}
	log.Printf("Service %s registered successfully", serviceAddr)

	// 启动服务监听
	go watchServices(client, serviceName)

	// 发现服务
	services, err := discoverServices(client, serviceName)
	if err != nil {
		log.Fatalf("Failed to discover services: %v", err)
	}
	log.Printf("Discovered services: %v", services)

	// 保持程序运行
	select {}
}

代码功能

  • 服务注册
    1. 使用etcd的租约机制注册(为了避免死锁,使用租约)
    2. 设置服务的存活时间(租约有效时间,到期前如果未续费就自动断开)
    3. 通过自动续约机制保持服务在线状态
    4. 服务路径格式为/services/服务名/服务地址
  • 服务发现
    1. 通过前缀查询获取指定服务名的所有实例
    2. 可以获取当前可用的服务列表
  • 服务监听
    1. 实时监控服务的变化(新增或移除)
    2. 当服务上下线时能及时收到通知

上线与注册流程说明

  1. 连接etcd集群
go 复制代码
client, err := clientv3.New(clientv3.Config{
    Endpoints:   []string{"localhost:2379"}, // etcd服务器地址
    DialTimeout: 5 * time.Second,
})

创建了一个 etcd 客户端,通过指定的端点 (Endpoints) 连接到 etcd 集群。

  1. 服务注册实现
go 复制代码
// 创建租约
leaseResp, err := client.Grant(context.TODO(), ttl)

// 写入服务信息并关联租约
_, err = client.Put(context.TODO(), key, serviceAddr, clientv3.WithLease(leaseResp.ID))

// 自动续约
ch, err := client.KeepAlive(context.TODO(), leaseResp.ID)

// 处理续约应答
go func() {
    for range ch {
        // 续约成功
    }
}()
  • Grant() 创建租约,设置服务存活时间
  • Put() 将服务信息写入 etcd,并通过WithLease()关联租约
  • KeepAlive() 启动自动续约机制
  • 用 goroutine 处理续约响应,保持服务活跃
  1. 服务发现实现
go 复制代码
// 前缀查询获取服务实例
resp, err := client.Get(context.TODO(), "/services/"+serviceName+"/", clientv3.WithPrefix())

// 提取服务地址
var services []string
for _, kv := range resp.Kvs {
    services = append(services, string(kv.Value))
}
  • Get() 方法配合WithPrefix()参数实现前缀查询
  • 遍历查询结果,提取所有服务实例地址
  1. 监听服务变化
go 复制代码
rch := client.Watch(context.TODO(), "/services/"+serviceName+"/", clientv3.WithPrefix())

for wresp := range rch {
    for _, ev := range wresp.Events {
        switch ev.Type {
        case clientv3.EventTypePut:    // 服务新增
        case clientv3.EventTypeDelete: // 服务移除
        }
    }
}
  • Watch() 方法建立对指定前缀路径的监听
  • 通过循环读取事件通道,处理服务新增和移除事件
  1. 服务下线
相关推荐
杨运交6 小时前
[060][调度模块]Redisson vs Redis 原生锁:两种分布式锁实现深度对比
数据库·redis·分布式
heimeiyingwang11 小时前
【架构实战】分布式事务:从CAP定理到Seata实战,一文讲透跨服务数据一致性
分布式·架构
lakernote15 小时前
图解 Kafka Consumer 常用 API:poll、seek、pause、wakeup 到底在控制什么?
分布式·kafka·linq
数据库小学妹18 小时前
集群与分布式啥区别?从主从集群到分布式实战
分布式·分布式数据库·数据库架构·集群·分库分表·主从集群·集群与分布式
江畔柳前堤19 小时前
AgentScope 设计与原理全解:从消息原语到分布式智能体工程底座
大数据·人工智能·分布式·目标检测·机器学习·语言模型·架构
萧瑟余晖19 小时前
Java深入解析篇三十四之分布式事务
java·开发语言·分布式
零域码客19 小时前
Redis 双刃剑:缓存与分布式锁的本质区别
redis·分布式·缓存·并发控制·后端架构
ZJU_统一阿萨姆20 小时前
【推理优化进阶】性能实验科学:工作负载、统计显著性与尾延迟归因
开发语言·人工智能·语言模型·系统架构·vllm
段振轩20 小时前
gozero整合mysql,gorm,redis.jwt
前端·redis·mysql·go
镭立智能制造20 小时前
储能PACK产线MES系统架构设计:从电芯到PACK的三级追溯是怎么实现的?
系统架构