gRPC+etcd实现服务注册和服务发现

  • 服务注册:创建etcd服务注册对象 --> 创建租约 --> 为kv绑定租约 --> 获取续租通道 --> 开启协程通过续租通道来持续续租
  • 服务发现:创建etcd服务发现对象 --> 根据prefix读取etcd服务端的kv到本地map --> 开启协程持续监听kv的变化并更新本地kv

项目结构:

gRPC+etcd实现服务注册和服务发现_第1张图片

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

① Config(存放配置信息,后续可以引入viper)

config.go:

package config

const (
	ServiceName    = "/hello"
	ServiceAddr    = "127.0.0.1:9090"
	EtcdServerAddr = "127.0.0.1:2379"
)

② proto(存放.proto文件,生成gRPC代码)

hello.proto:

syntax = "proto3";

package service;

option go_package = "./proto/service";

message Hello{
  uint64 HelloID = 1;
  string Msg = 2;
  string SendTime = 3;
}

message HelloRequest{
  Hello hello = 1;
}

message HelloResponse{
  Hello Resp = 1;
  int64 Code = 2;
}

service HelloService{
  rpc SayHello(HelloRequest)returns(HelloResponse){}
}

在这里插入图片描述
在./proto/service目录下生成gRPC相关代码。

③ serviceImpl(实现gRPC接口中的方法)

type HelloService struct {
}

var _ service.HelloServiceServer = (*HelloService)(nil)

func (h *HelloService) SayHello(ctx context.Context, req *service.HelloRequest) (*service.HelloResponse, error) {
	log.Printf("[Server]:接收到消息...消息ID为%d,消息类型为%s,发送时间为%s", req.Hello.HelloID, req.Hello.Msg, req.Hello.SendTime)
	return &service.HelloResponse{
		Resp: &service.Hello{
			HelloID:  req.Hello.HelloID + 1,
			Msg:      "Hello from Server",
			SendTime: time.Now().Format("2006-01-02 15:04:05"),
		},
		Code: 0,
	}, nil
}

④ etcd(服务注册和服务发现)

服务发现 discovery.go:

// EtcdDiscovery 服务发现
type EtcdDiscovery struct {
	cli        *clientv3.Client  // etcd连接
	serviceMap map[string]string // 服务列表(k-v列表)
	lock       sync.RWMutex      // 读写互斥锁
}

func NewServiceDiscovery(endpoints []string) (*EtcdDiscovery, error) {
	// 创建etcdClient对象
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   endpoints,
		DialTimeout: 5 * time.Second,
	})

	if err != nil {
		return nil, err
	}

	return &EtcdDiscovery{
		cli:        cli,
		serviceMap: make(map[string]string), // 初始化kvMap
	}, nil
}

// ServiceDiscovery 读取etcd的服务并开启协程监听kv变化
func (e *EtcdDiscovery) ServiceDiscovery(prefix string) error {
	// 根据服务名称的前缀,获取所有的注册服务
	resp, err := e.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
	if err != nil {
		return err
	}

	// 遍历key-value存储到本地map
	for _, kv := range resp.Kvs {
		e.putService(string(kv.Key), string(kv.Value))
	}

	// 开启监听协程,监听prefix的变化
	go func() {
		watchRespChan := e.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
		log.Printf("watching prefix:%s now...", prefix)
		for watchResp := range watchRespChan {
			for _, event := range watchResp.Events {
				switch event.Type {
				case mvccpb.PUT: // 发生了修改或者新增
					e.putService(string(event.Kv.Key), string(event.Kv.Value)) // ServiceMap中进行相应的修改或新增
				case mvccpb.DELETE: //发生了删除
					e.delService(string(event.Kv.Key)) // ServiceMap中进行相应的删除
				}
			}
		}
	}()

	return nil
}

// SetService 新增或修改本地服务
func (s *EtcdDiscovery) putService(key, val string) {
	s.lock.Lock()
	s.serviceMap[key] = val
	s.lock.Unlock()
	log.Println("put key :", key, "val:", val)
}

// DelService 删除本地服务
func (s *EtcdDiscovery) delService(key string) {
	s.lock.Lock()
	delete(s.serviceMap, key)
	s.lock.Unlock()
	log.Println("del key:", key)
}

// GetService 获取本地服务
func (s *EtcdDiscovery) GetService(serviceName string) (string, error) {
	s.lock.RLock()
	serviceAddr, ok := s.serviceMap[serviceName]
	s.lock.RUnlock()
	if !ok {
		return "", fmt.Errorf("can not get serviceAddr")
	}
	return serviceAddr, nil
}

// Close 关闭服务
func (e *EtcdDiscovery) Close() error {
	return e.cli.Close()
}

服务注册 register.go:

// EtcdRegister 服务注册
type EtcdRegister struct {
	etcdCli *clientv3.Client // etcdClient对象
	leaseId clientv3.LeaseID // 租约id
}

// CreateLease 创建租约。expire表示有效期(s)
func (e *EtcdRegister) CreateLease(expire int64) error {

	lease, err := e.etcdCli.Grant(context.Background(), expire)
	if err != nil {
		log.Println(err.Error())
		return err
	}

	e.leaseId = lease.ID // 记录生成的租约Id
	return nil
}

// BindLease 绑定租约。将租约与对应的key-value绑定
func (e *EtcdRegister) BindLease(key string, value string) error {

	res, err := e.etcdCli.Put(context.Background(), key, value, clientv3.WithLease(e.leaseId))
	if err != nil {
		log.Println(err.Error())
		return err
	}

	log.Printf("bind lease success %v \n", res)
	return nil
}

// KeepAlive 获取续约通道 并 持续续租
func (e *EtcdRegister) KeepAlive() error {
	keepAliveChan, err := e.etcdCli.KeepAlive(context.Background(), e.leaseId)

	if err != nil {
		log.Println(err.Error())
		return err
	}

	go func(keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse) {
		for {
			select {
			case resp := <-keepAliveChan:
				log.Printf("续约成功...leaseID=%d", resp.ID)
			}
		}
	}(keepAliveChan)

	return nil
}

// Close 关闭服务
func (e *EtcdRegister) Close() error {
	log.Printf("close...\n")
	// 撤销租约
	e.etcdCli.Revoke(context.Background(), e.leaseId)
	return e.etcdCli.Close()
}

// NewEtcdRegister 初始化etcd服务注册对象
func NewEtcdRegister(etcdServerAddr string) (*EtcdRegister, error) {

	client, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{etcdServerAddr},
		DialTimeout: 3 * time.Second,
	})

	if err != nil {
		log.Println(err.Error())
		return nil, err
	}

	e := &EtcdRegister{
		etcdCli: client,
	}
	return e, nil
}

// ServiceRegister 服务注册。expire表示过期时间,serviceName和serviceAddr分别是服务名与服务地址
func (e *EtcdRegister) ServiceRegister(serviceName, serviceAddr string, expire int64) (err error) {

	// 创建租约
	err = e.CreateLease(expire)
	if err != nil {
		return err
	}

	// 将租约与k-v绑定
	err = e.BindLease(serviceName, serviceAddr)
	if err != nil {
		return err
	}

	// 持续续租
	err = e.KeepAlive()
	return err
}

⑤ server(开启HelloService并在etcd注册)

server/main.go:

func main() {

	etcdRegister, err := etcd.NewEtcdRegister(config.EtcdServerAddr)
	if err != nil {
		log.Fatal(err)
	}

	err = etcdRegister.ServiceRegister(config.ServiceName, config.ServiceAddr, 30)
	if err != nil {
		log.Fatal(err)
	}
	defer etcdRegister.Close()

	listener, err := net.Listen("tcp", config.ServiceAddr)
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	server := grpc.NewServer()
	service.RegisterHelloServiceServer(server, &serviceImpl.HelloService{})

	err = server.Serve(listener)
	if err != nil {
		log.Fatal(err)
	}
}

⑥ client(进行etcd服务发现,开启HelloClient)

client/main.go:

func main() {
	c := make(chan os.Signal, 1)
	go func() {
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	}()

	etcdDiscovery, err := etcd.NewServiceDiscovery([]string{config.EtcdServerAddr})
	if err != nil {
		log.Fatal(err)
	}
	defer etcdDiscovery.Close()

	err = etcdDiscovery.ServiceDiscovery("/hello")
	if err != nil {
		log.Fatal(err)
	}

	var helloID uint64

	for {
		helloID++

		serviceAddr, err := etcdDiscovery.GetService(config.ServiceName)
		conn, err := grpc.Dial(serviceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
		if err != nil {
			log.Fatal(err)
		}
		cli := service.NewHelloServiceClient(conn)

		req := &service.HelloRequest{
			Hello: &service.Hello{
				HelloID:  helloID,
				Msg:      "Hello from Client",
				SendTime: time.Now().Format("2006-01-02 15:04:05"),
			},
		}

		resp, err := cli.SayHello(context.Background(), req)
		if err != nil {
			log.Fatal(err)
		}

		log.Printf("%+v", resp)
		conn.Close()
		time.Sleep(time.Second * 2)
	}
}

以下是实现过程中,易错以及易忘的地方:

  1. 关闭etcd服务注册对象时,不要忘记revoke lease(撤销租约)
    gRPC+etcd实现服务注册和服务发现_第2张图片
  2. 开启etcd服务发现的监听协程(每次写这部分都有点卡壳…)
    gRPC+etcd实现服务注册和服务发现_第3张图片
  3. gRPC的client端,conn是通过gRPC来建立的,不是net
    在这里插入图片描述

你可能感兴趣的:(Go,etcd,微服务,go)