gRPC-11 etcd实现服务注册和服务发现

19 篇文章 11 订阅

本篇文章基于这篇: https://blog.csdn.net/wanmei002/article/details/117659330

代码地址

https://github.com/wanmei002/grpc-learn/tree/master/ch10

原理

  1. 首先为服务起一个名字,启动的时候,把信息存储到etcd
    1.1 比如: key是服务名 值是ip。 多个相同的服务让key的前缀一样,这样 读取etcd的时候就可以通过前缀来读取
    1.2 生成租约,并设置比较短的过期时间,把 etcd 中存储的值跟租约绑定,这样租约如果没有在有效的时间内续期(很可能已经不能提供服务了),则 etcd 保存的值就会过期
    1.3 定期更新租约的过期时间
  2. 客户端注册相关服务的解析器,在解析器中拼接 etcd的key的前缀,获取提供服务的ip列表,前端通过特定的算法实现负载均衡
    2.1 客户端拼接 etcd 中key的前缀,读取提供服务的ip列表,更新拨号列表中的ip列表
    2.2 同时,启动一个协程,监听 etcd 中服务信息是否有变化(增加了服务ip或减少了服务ip)。如果有变化则更新拨号列表中的ip

服务端代码

// 需要注册服务到 etcd 并设置租约,定期续租,如果服务停止了则租约过期,
// 就会从 etcd 服务中消失

type RegisterEtcdServer struct {
	etcdCli *clientv3.Client
	leaseId clientv3.LeaseID
	ctx     context.Context
}
// 主要逻辑
// 1. 创建 etcd 客户端
// 2. 创建租约
// 3. k v 跟租约绑定
// 4. 定期续期

// 服务端调用这个方法  实现服务注册
func RegisterServer(k, v string, expire int64) (*RegisterEtcdServer, error) {
	etcdClient, err := clientv3.New(
		clientv3.Config{
			Endpoints:   []string{":2379"},
			DialTimeout: 5 * time.Second,
		},
	)

	if err != nil {
		log.Println("new etcd client failed; err:", err)
		return nil, err
	}

	svr := &RegisterEtcdServer{
		etcdCli: etcdClient,
		ctx:     context.Background(),
	}

	// 创建租约
	err = svr.createLease(expire)
	if err != nil {
		log.Println("创建租约失败 err:", err)
		return nil, err
	}
	// 值绑定租约
	err = svr.BindLease(k, v)
	if err != nil {
		log.Println("etcd put failed; err:", err)
		return nil, err
	}
	// 定时给租约续期,如果服务停止,则租约过期
	err = svr.keepAlive()
	if err != nil {
		log.Println("定时续期失败l err:", err)
		return nil, err
	}

	return svr, nil
}

func (s *RegisterEtcdServer) createLease(expire int64) error {
	res, err := s.etcdCli.Grant(s.ctx, expire)
	if err != nil {
		log.Println("create grant failed; err : ", err)
		return err
	}
	s.leaseId = res.ID
	return nil
}

func (s *RegisterEtcdServer) BindLease(k, v string) error {
	res, err := s.etcdCli.Put(s.ctx, k, v, clientv3.WithLease(s.leaseId))
	if err != nil {
		log.Println("etcd put failed; err:", err)
		return err
	}

	log.Printf("etcd put succ : %+v\n", res)
	return nil
}

func (s *RegisterEtcdServer) keepAlive() error {
	leaseResCh, err := s.etcdCli.KeepAlive(s.ctx, s.leaseId)
	if err != nil {
		log.Println("etcd keep live failed; err :", err)
		return err
	}

	go s.watch(leaseResCh)
	return nil
}

func (s *RegisterEtcdServer) watch(leaseCh <-chan *clientv3.LeaseKeepAliveResponse) {
	for k := range leaseCh {
		log.Printf("续约成功; val:%+v\n", k)
	}

	log.Println("租约续期关闭")
}

func (s *RegisterEtcdServer) Close() error {
	// 撤销租约
	res, err := s.etcdCli.Revoke(s.ctx, s.leaseId)
	if err != nil {
		log.Println("撤销租约失败")
		return err
	}

	log.Printf("撤销租约返回的结果: %+v\n", res)

	return s.etcdCli.Close()
}

客户端主要代码实现

type serverDiscovery struct {
	cli           *clientv3.Client    // 用来连接etcd
	conn          resolver.ClientConn // 用来 调用 UpdateState 这个方法,更新本地服务ip列表
	serviceIpList sync.Map            // 用来存储获得的ip列表,因为监听服务是新的协程在运行,可能会存在对map的同时读写,引起资源冲突
}

var etcdPort = ":2379"

func NewServerDiscovery() resolver.Builder {
	etcdCli, err := clientv3.New(
		clientv3.Config{
			Endpoints:   []string{etcdPort},
			DialTimeout: 5 * time.Second,
		},
	)
	if err != nil {
		log.Println("client get etcd cli failed; err :", err)
		panic(err)
	}

	return &serverDiscovery{cli: etcdCli}
}
// 服务解析器的入口,在这里初次
func (s *serverDiscovery) Build(target resolver.Target, cc resolver.ClientConn,
	opts resolver.BuildOptions) (resolver.Resolver, error) {
	defer func() {
		if err := recover(); err != nil {
			log.Println("build err:", err)
		}
	}()
	s.conn = cc
	// 获取在 etcd 保存的前缀
	prefix := fmt.Sprintf("/%s.%s/", target.Scheme, target.Endpoint)
	// 获取 etcd 中服务保存的ip列表
	res, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
	if err != nil {
		log.Println("Bulid etcd get addr failed; err:", err)
		return nil, err
	}
	log.Printf("etcd res:%+v\n", res)

	for _, kv := range res.Kvs {
		s.store(kv.Key, kv.Value)
	}
	// 更新 拨号里的ip列表
	s.updateState()

	// 启动 etcd 观察者模式,监听etcd 中保存的服务信息是否有变化
	go s.watch(prefix)

	return s, nil

}

func (s *serverDiscovery) watch(prefix string) {
	res := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
	// res 是一个只读的管道
	for val := range res {
		for _, event := range val.Events {
			switch event.Type {
			case 0:// 0 是有数据增加
				s.store(event.Kv.Key, event.Kv.Value)
				log.Println("put:", string(event.Kv.Key))
				s.updateState()
			case 1:// 1是有数据减少
				log.Println("del:", string(event.Kv.Key))
				s.del(event.Kv.Key)
				s.updateState()
			}
		}
	}

}

func (s *serverDiscovery) Scheme() string {
	return scheme
}

func (s *serverDiscovery) ResolveNow(resolver.ResolveNowOption) {

}

func (s *serverDiscovery) Close() {
	log.Print("i am close\n")
}

func (s *serverDiscovery) store(k, v []byte) {

	s.serviceIpList.Store(string(k), string(v))

}

func (s *serverDiscovery) del(k []byte) {
	s.serviceIpList.Delete(string(k))
}
// 更新拨号中的ip列表
func (s *serverDiscovery) updateState() {
	var addrList resolver.State
	s.serviceIpList.Range(func(k, v interface{}) bool {
		tA, ok := v.(string)
		if !ok {
			return false
		}
		log.Printf("conn.UpdateState key[%v];val[%v]\n", k, v)
		addrList.Addresses = append(addrList.Addresses, resolver.Address{Addr: tA})
		return true
	})

	s.conn.UpdateState(addrList)
}
// 注册服务解析器到解析列表中
func init() {
	resolver.Register(NewServerDiscovery())
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go微服务开发是利用Go语言进行微服务架构的开发方式。在这个问题中,使用了gin、grpcetcd进行重构grpc-todolist项目。 Gin是一个轻量级的Web框架,使用它可以快速构建高性能的Web应用程序。它具有简单易用、性能出色和灵活的特点。在微服务开发中,Gin可以作为HTTP服务器框架,处理和响应客户端的HTTP请求。 gRPC是一种高性能、开源的远程过程调用(RPC)框架。它支持多种编程语言,并使用带有协议缓冲区的Google Protocol Buffers进行数据交换。在微服务架构中,gRPC可以用于服务之间的通信,通过定义接口和消息格式,实现服务间的数据传输和调用。 Etcd是一个高可靠、分布式的键值存储系统。它使用Raft一致性算法来保证数据的可靠性和一致性。在微服务开发中,Etcd可以作为服务发现和配置管理的工具,用于注册发现各个微服务的信息。 对于重构grpc-todolist项目来说,使用gin可以将原有的HTTP接口改写为更加高性能的接口,提高整个系统的性能。通过使用gRPC,可以将原有的接口定义为gRPC接口,实现服务间的高效通信,并且易于扩展和维护。同时,借助Etcd实现服务注册发现,提高系统的可用性和灵活性。 总而言之,通过使用gin、grpcetcdgrpc-todolist项目进行重构,可以提高系统性能、扩展性和可维护性。这种微服务开发方式能够更好地适应大规模分布式系统的需求,使得系统更加稳定和可靠。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值