基于go-grpc和etcd的负载均衡、服务注册和服务发现

使用etcd做服务注册与发现时,etcd已经集成了grpc客户端的服务发现 功能,自己做服务注册功能。

服务注册:

package balancer
import (
	"context"
	clientv3 "go.etcd.io/etcd/client/v3"
	"strconv"
	"time"
)
var cli *clientv3.Client
var interval = 5

//注册服务
//etcdAddrs:etcd地址
//serviceName:注册到etcd的服务名称
//serviceAddr:自己grpc业务监听的地址端口
func Register(etcdAddrs []string, serviceName string, serviceAddr string) error {
    
    
	//获取链接
	var err error
	if cli == nil {
    
    
		cli, err = clientv3.New(clientv3.Config{
    
    
			Endpoints:   etcdAddrs,
			DialTimeout: 5 * time.Second,
		})
		if err != nil {
    
    
			return err
		}
	}
	//注册续租
	register(serviceName, serviceAddr)
	return nil
}
//etcd服务发现时,底层解析的是一个json串,且包含Addr字段
func getValue(addr string) string  {
    
    
	return "{\"Addr\":\"" + addr + "\"}"
}

func register(serviceName, serviceAddr string) error  {
    
    
	//注册服务
	leaseResp, err := cli.Grant(context.Background(), int64(interval + 1))
	if err != nil {
    
    
		return err
	}
	fullKey := serviceName + "/" + strconv.Itoa(int(leaseResp.ID))
	_, err = cli.Put(context.Background(), fullKey, getValue(serviceAddr), clientv3.WithLease(leaseResp.ID))
	if err != nil {
    
    
		return err
	}
	keepAlive(serviceName, serviceAddr, leaseResp)
	return nil
}

//异步续约
func keepAlive(name string, addr string, leaseResp *clientv3.LeaseGrantResponse)  {
    
    
	//永久续约,续约成功后,etcd客户端和服务器会保持通讯,通讯成功会写数据到返回的通道中
	//停止进程后,服务器链接不上客户端,相应key租约到期会被服务器自动删除
	c, err := cli.KeepAlive(cli.Ctx(), leaseResp.ID)
	go func() {
    
    
		if err == nil {
    
    
			for  {
    
    
				select {
    
    
				case _, ok := <-c:
					if !ok {
    
    //续约失败
						cli.Revoke(cli.Ctx(), leaseResp.ID)
						register(name, addr)
						return
					}
				}
			}
			defer cli.Revoke(cli.Ctx(), leaseResp.ID)
		}
	}()
}

由以下proto生成grpc代码:

syntax = "proto3";
option go_package = "./;distest";
message ReqUser {
    
    
  string Account = 1;
}
message ReplyUser {
    
    
  string Account = 1;
  string Name = 2;
  int32 Age = 3;
}
service UserService {
    
    
  rpc GetUserService(ReqUser) returns (ReplyUser);
}

服务器注册服务:

//实现服务功能
type UserService struct {
    
    
}
func (s *UserService) GetUserService(c context.Context,r *distest.ReqUser) (*distest.ReplyUser, error) {
    
    
	log.Println("server收到消息", *r)
	return &distest.ReplyUser{
    
    
		Account: "989876",
		Name: "lx01",
		Age: 30,
	}, nil
}

func main() {
    
    
	port := ""
	flag.StringVar(&port, "p", "10010", "端口")
	flag.Parse()
	addr := "127.0.0.1:" + port
	lis, err := net.Listen("tcp", addr)
	if err != nil {
    
    
		log.Fatalf("failed to listen: %s", err)
	}
	defer lis.Close()
	s := grpc.NewServer()
	defer s.GracefulStop()
	//grpc注册服务
	distest.RegisterUserServiceServer(s, &UserService{
    
    })
	//向etcd注册grpc服务,服务名称为project_test。前端做服务发现时,要和此服务名相同,最上面自己写的注册方式
	balancer.Register([]string{
    
    "127.0.0.1:2379"}, "project_test/", addr)
	log.Println("启动Grpc服务器:", addr)
	if err := s.Serve(lis); err != nil {
    
    
		log.Fatalf("failed to serve: %s", err)
	}
}

以上代码不懂的,自行补充grpc和etcd基础知识。
下面是客户端通过etcd发现grpc服务,并调用服务:

func TestEtctDisClient(t *testing.T)  {
    
    
	//初始化etcd客户端
	var addr = "127.0.0.1:2379"
	cli, _ := clientv3.New(clientv3.Config{
    
    
		Endpoints:   []string{
    
    addr},
		DialTimeout: 5 * time.Second,
	})
	//新建builder,etcd官方实现的Builder对象
	r, _ := resolver.NewBuilder(cli)
	//向grpc注册builder,这样Dial时,就可以按照Scheme查找到此Builder,
	grpcResolver.Register(r)
	//注意:project_test为服务名称,要和服务器注册服务名称相匹配。round_robin表示以轮询方式访问grpc服务
	conn, err := grpc.Dial(r.Scheme() + "://"+addr + "/project_test", grpc.WithInsecure(), grpc.WithBalancerName("round_robin"))

	if err != nil {
    
    
		panic(err)
	}
	//下面每隔1s对服务器调用一次
	client := distest.NewUserServiceClient(conn)
	for {
    
    
		resp, err := client.GetUserService(context.Background(), &distest.ReqUser{
    
    Account: "xxxx"})
		if err != nil {
    
    
			log.Println(err)
		} else {
    
    
			log.Println(resp)
		}
		<-time.After(time.Second)
	}
}

运行:
启动三个服务器端:
go run main.go -p 9001
go run main.go -p 9002
go run main.go -p 9003
然后再golang中启动TestEtctDisClient测试,结果如下图:

服务器均收到客户发的数据并返回。此时,任意打开、或者关闭服务器,均不影响客户端正常执行。

核心问题,当我们注册多个服务时,前端做服务发现时,为什么要服务名一直?
注册的服务,在etcd中像下面这样:

在etcd中,key为服务名//租约ID拼合而成。

客服端做服务发现时,核心时etcd的watch,底层watch的是以project_test开头的key值,所以能监听到project_testxxxxxxx为key的value变化。
etcd底层监听源码:

func (m *endpointManager) watch(ctx context.Context, rev int64, upch chan []*Update) {
    
    
	defer close(upch)
	lg := m.client.GetLogger()
	opts := []clientv3.OpOption{
    
    clientv3.WithRev(rev), clientv3.WithPrefix()}
	wch := m.client.Watch(ctx, m.target, opts...)

重点在clientv3.WithPrefix()上。

我们在使用etcd命令行工具时,也可以监听前缀:
.\etcdctl.exe watch lx --prefix
以上监听以lx开头的key值变化。

grpc服务发现原理,百度很多。

猜你喜欢

转载自blog.csdn.net/lcl20093466/article/details/122482419
今日推荐