etcd 笔记(09)— 基于 etcd 实现微服务的注册与发现

1. 服务注册与发现基本概念

在单体应用向微服务架构演进的过程中,原本的巨石型应用会按照业务需求被拆分成多个微服务,每个服务提供特定的功能,也可能依赖于其他的微服务。此时,每个微服务实例都可以动态部署,服务实例之间的调用通过轻量级的远程调用方式(HTTP、消息队列等)实现,它们之间通过预先定义好的接口进行访问。

在微服务架构中,多个微服务间的通信需要依赖服务注册与发现组件获取指定服务实例的地址信息,才能正确地发起 RPC 调用,保证分布式系统的高可用、高并发。

服务注册与发现主要包含两部分:服务注册的功能与服务发现的功能。

  • 服务注册是指服务实例启动时将自身信息注册到服务注册与发现中心,并在运行时通过心跳等方式向其汇报自身服务状态;
  • 服务发现是指服务实例向服务注册与发现中心获取其他服务实例信息,用于远程调用;

2. 服务注册与发现中心功能

服务注册与发现中心主要有以下职责:

  • 管理当前注册到服务注册与发现中心的微服务实例元数据信息,包括服务实例的服务名、IP 地址、端口号、服务描述和服务状态等;

  • 与注册到服务发现与注册中心的微服务实例维持心跳,定期检查注册表中的服务实例是否在线,并剔除无效服务实例信息;

  • 提供服务发现能力,为服务调用方提供服务提供方的服务实例元数据;

通过服务发现与注册中心,我们可以很方便地管理系统中动态变化的服务实例信息。但是与此同时,它也可能成为系统的瓶颈和故障点。因为服务之间的调用信息来自服务注册与发现中心,当它不可用时,服务之间的调用也就无法正常进行。因此服务发现与注册中心一般会集群化部署,提供高可用性和高稳定性。

3. 服务端代码

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

//服务注册对象
type ServiceRegister struct {
	client     *clientv3.Client
	kv         clientv3.KV
	lease      clientv3.Lease
	canclefunc func()
	key        string

	leaseResp     *clientv3.LeaseGrantResponse
	keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
}

// 初始化注册服务
func InitService(host []string, timeSeconds int64) (*ServiceRegister, error) {
	config := clientv3.Config{
		Endpoints:   host,
		DialTimeout: 5 * time.Second,
	}

	client, err := clientv3.New(config)
	if err != nil {
		fmt.Printf("create connection etcd failed %s\n", err)
		return nil, err
	}

	// 得到KV和Lease的API子集
	kv := clientv3.NewKV(client)
	lease := clientv3.NewLease(client)

	service := &ServiceRegister{
		client: client,
		kv:     kv,
		lease:  lease,
	}
	return service, nil
}

// 设置租约
func (s *ServiceRegister) setLease(timeSeconds int64) error {
	leaseResp, err := s.lease.Grant(context.TODO(), timeSeconds)
	if err != nil {
		fmt.Printf("create lease failed %s\n", err)
		return err
	}

	// 设置续租
	ctx, cancelFunc := context.WithCancel(context.TODO())
	leaseRespChan, err := s.lease.KeepAlive(ctx, leaseResp.ID)
	if err != nil {
		fmt.Printf("KeepAlive failed %s\n", err)
		return err
	}

	s.leaseResp = leaseResp
	s.canclefunc = cancelFunc
	s.keepAliveChan = leaseRespChan
	return nil
}

// 监听续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
	for {
		select {
		case leaseKeepResp := <-s.keepAliveChan:
			if leaseKeepResp == nil {
				fmt.Println("续租功能已经关闭")
				return
			} else {
				fmt.Println("续租成功")
			}
		}
	}
}

// 通过租约注册服务
func (s *ServiceRegister) PutService(key, val string) error {
	fmt.Printf("PutService key <%s> val <%s>\n", key, val)
	_, err := s.kv.Put(context.TODO(), key, val, clientv3.WithLease(s.leaseResp.ID))
	return err
}

// 撤销租约
func (s *ServiceRegister) RevokeLease() error {
	s.canclefunc()
	time.Sleep(2 * time.Second)
	_, err := s.lease.Revoke(context.TODO(), s.leaseResp.ID)
	return err

}

func main() {
	service, _ := InitService([]string{"192.168.0.129:2379"}, 5)
	service.setLease(10)
	defer service.RevokeLease()
	go service.ListenLeaseRespChan()

	err := service.PutService("/wohu", "http://localhost:8080")
	if err != nil {
		fmt.Printf("PutService failed %s\n", err)
	}
	// 使得程序阻塞运行,便于观察输出结果
	select {}
}

4. 客户端代码

package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/mvcc/mvccpb"
)

// 客户端对象
type Client struct {
	client     *clientv3.Client
	kv         clientv3.KV
	lease      clientv3.Lease
	watch      clientv3.Watcher
	serverList map[string]string
	lock       sync.Mutex
}

// 初始化客户端对象
func InitClient(addr []string) (*Client, error) {
	conf := clientv3.Config{
		Endpoints:   addr,
		DialTimeout: 5 * time.Second,
	}
	client, err := clientv3.New(conf)
	if err != nil {
		fmt.Printf("create connection etcd failed %s\n", err)
		return nil, err
	}

	// 得到 KV 、Lease、 Watcher 的API子集
	kv := clientv3.NewKV(client)
	lease := clientv3.NewLease(client)
	watch := clientv3.NewWatcher(client)

	// 给客户端对象赋值
	c := &Client{
		client:     client,
		kv:         kv,
		lease:      lease,
		watch:      watch,
		serverList: make(map[string]string),
	}
	return c, nil
}

// 根据注册的服务名,获取服务实例的信息
func (c *Client) getServiceByName(prefix string) ([]string, error) {
	// 读取的时候带有 WithPrefix 选项,所以会读取该前缀所有的字段值
	resp, err := c.kv.Get(context.Background(), prefix, clientv3.WithPrefix())
	if err != nil {
		fmt.Printf("getServiceByName failed %s\n", err)
		return nil, err
	}
	// 返回的 resp 是多个字段值。需要遍历提取对应的 key value
	addrs := c.extractAddrs(resp)
	return addrs, nil

}

// 根据 etcd 的响应,提取服务实例的数组
func (c *Client) extractAddrs(resp *clientv3.GetResponse) []string {
	addrs := make([]string, 0)
	if resp == nil || resp.Kvs == nil {
		return addrs
	}

	for i := range resp.Kvs {
		if v := resp.Kvs[i].Value; v != nil {
			// 将 key  value 值保存在  ServiceList 表中
			c.SetServiceList(string(resp.Kvs[i].Key), string(resp.Kvs[i].Value))
			addrs = append(addrs, string(v))
		}
	}
	return addrs
}

// 设置 serverList
func (c *Client) SetServiceList(key, val string) {
	c.lock.Lock()
	defer c.lock.Unlock()
	// serverList 为初始化设置的本地 map 对象,由于考虑到多个 client 运行,所以需要加锁控制
	c.serverList[key] = string(val)
	fmt.Println("set data key :", key, "val:", val)
}

// 删除本地缓存的服务实例信息
func (c *Client) DelServiceList(key string) {
	c.lock.Lock()
	defer c.lock.Unlock()
	delete(c.serverList, key)
	fmt.Println("del data key:", key)

	newRes, err := c.getServiceByName(key)
	if err != nil {
		fmt.Printf("getServiceByName failed %s\n", err)
	} else {
		fmt.Printf("get  key %s", key, " current val is: %v\n", newRes)
	}

}

// 获取服务实例信息
func (c *Client) GetService(prefix string) ([]string, error) {
	if addrs, err := c.getServiceByName(prefix); err != nil {
		panic(err)
	} else {
		fmt.Println("get service ", prefix, " for instance list: ", addrs)
		go c.watcher(prefix)
		return addrs, nil
	}
}

// 监控指定键值对的变更
func (c *Client) watcher(prefix string) {
	watchRespChan := c.watch.Watch(context.Background(), prefix, clientv3.WithPrefix())
	for watchResp := range watchRespChan {
		for _, event := range watchResp.Events {
			switch event.Type {
			case mvccpb.PUT: // 写入的事件
				c.SetServiceList(string(event.Kv.Key), string(event.Kv.Value))
			case mvccpb.DELETE: // 删除的事件
				c.DelServiceList(string(event.Kv.Key))
			}
		}
	}
}

func main() {
	/*
		先创建 etcd 连接,构建 Client 对象,随后获取指定的服务 /wohu 实例信息;
		最后监测 wohu 服务实例的变更事件,根据不同的事件产生不同的行为。
	*/

	c, _ := InitClient([]string{"192.168.0.129:2379"})
	c.GetService("/wohu")

	// 使得程序阻塞运行,模拟服务的持续运行
	select {}
}
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wohu007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值