gRPC服务发现

基于 etcd 实现的服务发现,按照非规范化的 etcd key 实现,详细见代码注释。

go 复制代码
package discovery

import (
	"context"
	"encoding/json"
	"fmt"
	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
	"google.golang.org/grpc/resolver"
	"strings"
	"time"
)

// gRPC 的服务一般会使用 protobuf 作为数据传输的介质
// gRPC 服务定义在 proto 的文件中,例如:service RoutingService {}
// protoc 将 proto 后缀文件转为 go 文件,文件内自动生成了 gRPC 服务的描述信息、服务注册的函数、客户端声明的函数等内容
// 如下,它们的格式是固定的,注意函数的参数
// 服务描述信息:RoutingService_ServiceDesc,格式:服务名_ServiceDesc
// 服务注册函数:RegisterRoutingServiceServer,格式:Register你服务名Server
// 客户端声明函数:NewRoutingServiceClient,格式:New服务名Client
// 其中客户端声明函数的参数是 gRPC 连接,返回值是 gRPC 服务的客户端接口,这样就可以调用客户端接口定义的 rpc 方法了
// gRPC 连接不会与某个 gRPC 服务绑定,它只是一个连接。
// 获取 gRPC 连接的方式如下两种,第一个参数就是 gRPC 服务的地址,可以写死 ip + port,也可以使用服务发现来获取 gRPC 服务的地址。
// grpc.NewClient(fmt.Sprintf("%s:///%s", scheme, serviceName))
// grpc.Dial(fmt.Sprintf("%s:///%s", scheme, serviceName))(废弃)
// 服务发现是实现 Builder 和 Resolver 接口,Builder 用于创建 Resolver 实例,Resolver 用于解析服务地址。
// Builder 的 Scheme 方法返回值是 与 grpc.NewClient 中的 scheme 对应
// Builder 的 Build 第一个参数 target.Endpoint() 得到的结果是 grpc.NewClient 中的 serviceName,Build 方法的触发分情况:
// grpc.NewClient 声明不会触发 Build 方法,首次调用 rpc 方法时触发 Build
// grpc.Dial 声明会触发 Build 方法,但已经废弃了
// Resolver 的 ResolveNow 方法是 gRPC 主动调用的,我们可以使用它动态去 etcd 中获取服务地址,也可以不实现它,自定义服务发现的逻辑

// 服务发现的实现方式:
// 假如我们有三个应用,user-center、device-center、网关,user-center 和 device-center 暴露了很多 gRPC 服务,网关需要调用它们的服务
// 假如我们使用 etcd 作为注册中心,同时规范化 etcd 的 key ,例如:grpc/services/{serviceName}/实例ID
// grpc/services/user-center/实例1
// grpc/services/user-center/实例2
// grpc/services/device-center/实例1
// grpc/services/device-center/实例2
// 网关中分别实现 Builder 和 Resolver,并将 Builder 的实例注册在 grpc 的地址解析中,resolver.Register(Builder实现的实例)
// 获取 user-center 和 device-center 的 grpc 连接
// grpc.NewClient(fmt.Sprintf("%s:///%s", "grpc", "user-center"))
// grpc.NewClient(fmt.Sprintf("%s:///%s", "grpc", "device-center"))
// 当 gRPC 连接建立时,gRPC 会调用 Builder 的 Build 方法,我们获取 target.Endpoint() 就是 serviceName
// 这样 fmt.Sprintf("grpc/services/%s", serviceName) 获取 serviceName 的 etcd 的 key 前缀
// 如:grpc/services/user-center/
// Build 方法中按前缀匹配查询 etcd 的数据,这样就获取到了 user-center 的所有实例的地址,再同步到 Resolver 中
// 如上就实现了规范化 etcd 的 key 前缀的服务发现,不管有多少个应用,代码中只需要一个服务发现的实例

// 如果没有规范化 etcd 的 key 前缀,那么我们需要为各个服务声明不同的 scheme,每个 scheme 对应一个服务发现的实例
// Builder 的实现必须包含 etcd 的 key 前缀 ,不能利用 target.Endpoint() 去实现服务发现
// 如:ServiceDiscovery 实现了 Builder
// type ServiceDiscovery struct {
//		serverKey string
// }
// grpc/services/user-center/ 固定写死赋值给 serverKey
// 声明 ServiceDiscovery { serverKey },注册 resolver.Register(ServiceDiscovery实例)
// grpc.NewClient(fmt.Sprintf("%s:///%s", "user", "user-center"))

// 普通 rpc 调用时,服务端挂掉:
// 服务发现找不到数据时:rpc error: code = Unavailable desc = no children to pick from
// 服务挂掉但etcd/服务发现还有数据:rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp 10.202.160.190:6888: connect: connection refused"
// 服务重启后客户端连接可以恢复

// 流式 rpc 调用,服务端挂掉:
// 客户端发送方:EOF
// 客户端接收方:rpc error: code = Unavailable desc = error reading from server: EOF
// 服务重启后客户端连接不可恢复

// ServiceDiscovery is a gRPC resolver that uses etcd for service discovery.
// 配合 grpc.NewClient(fmt.Sprintf("%s:///%s", scheme, serviceName) 来使用
// Build 方法的 target.Endpoint() 就是 serviceName

type Mode int

const (
	ModeFormat Mode = iota // 格式化模式,etcd 的前缀格式是统一的,服务发现的逻辑是根据 scheme 找到 resolver,通过 serviceName 与固定前缀拼接作为 etcd 的前缀(,例如:grpc/services/{serviceName}
	ModeTarget             // 目标模式,适用于 etcd 前缀格式不统一,需要开发者指定 etcd 的前缀
)

// ServiceDiscovery 服务发现,声明的实例需要注册到 grpc 的 resolver 中,如:resolver.Register(ServiceDiscovery实例)
// mode: 模式,ModeFormat 或 ModeTarget,服务发现逻辑按照 mode 决定 etcd 中的 key,例如:ModeFormat是 serviceKey + target.Endpoint(),ModeTarget 是 serviceKey
// scheme: 当多个 grpc 服务暴露的 etcd 前缀格式不统一时,schema 要唯一,一个 schema 声明一个 ServiceDiscovery 实例;当格式统一时,schema 固定一个即可,仅需要一个 ServiceDiscovery 实例
// serviceKey: etcd 中服务的 key 前缀,以/开头和不以/开头是不一样的,另外建议以/结尾。与 mode 的表现有关系
type ServiceDiscovery struct {
	mode       Mode
	scheme     string
	serviceKey string
	etcdClient *clientv3.Client
}

// serviceResolver is a gRPC resolver that resolves service addresses from etcd.
// 一个 scheme 对应一个 serviceResolver,当 grpc 建立连接时触发 ServiceDiscovery 的 Build 方法
// 注意:
// grpc.NewClient 不会触发 Build 方法
// grpc.Dial 会触发 Build 方法,但已经废弃了
type serviceResolver struct {
	mode       Mode
	scheme     string
	serviceKey string

	target  resolver.Target
	client  *clientv3.Client
	cc      resolver.ClientConn
	addrMap map[string]resolver.Address
	closed  chan struct{}
}

// ServiceInfo 服务信息,etcd 中存储的服务信息
type ServiceInfo struct {
	AppName      string `json:"appName"`
	Ip           string `json:"ip"`
	LeaseId      uint64 `json:"leaseId"`
	Port         uint16 `json:"port"`
	ServiceName  string `json:"serviceName"`
	RegisterTime int64  `json:"registerTime"`
}

func NewServiceDiscovery(mode Mode, scheme string, serviceKey string, etcdClient *clientv3.Client) *ServiceDiscovery {
	return &ServiceDiscovery{
		mode:       mode,
		scheme:     scheme,
		serviceKey: serviceKey,
		etcdClient: etcdClient,
	}
}

// Build creates a new ServiceDiscovery resolver.
// grpc.NewClient 不会触发 Build 方法
// grpc.Dial 会触发 Build 方法,但已经废弃了
// target: grpc.NewClient(fmt.Sprintf("%s:///%s", scheme, serviceName) 中的 serviceName 就是 target
func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	// 创建服务解析器
	sr := &serviceResolver{
		mode:       s.mode,
		target:     target,
		cc:         cc,
		scheme:     s.scheme,
		serviceKey: s.serviceKey,
		client:     s.etcdClient,
		closed:     make(chan struct{}),
		addrMap:    make(map[string]resolver.Address),
	}

	// 首次拉取所有数据
	if err := sr.rePull(); err != nil {
		return nil, err
	}
	// 开启 watcher 监听 etcd 中的服务地址变化
	go sr.watcher()

	return sr, nil
}

// Scheme returns the scheme of the resolver.
// scheme 是 grpc.NewClient(fmt.Sprintf("%s:///%s", scheme, serviceName)
func (s *ServiceDiscovery) Scheme() string {
	return s.scheme
}

// ResolveNow is called by gRPC to resolve the service address immediately.
// grpc 主动调用去解析服务地址,这里可以实现从 etcd 获取服务地址的逻辑
// 但是不在这里实现,因为这里实现有同步和异步从 etcd 中查询数据
// 同步会阻塞
// 异步会开启很多 goroutine,可能会导致 goroutine 泄漏
func (s *serviceResolver) ResolveNow(options resolver.ResolveNowOptions) {

}

func (s *serviceResolver) Close() {
	close(s.closed)
}

func (s *serviceResolver) makeKey() string {
	if s.mode == ModeTarget {
		return s.serviceKey
	} else {
		// ModeFormat 模式,拼接 serviceKey 和 target.Endpoint()
		if strings.HasSuffix(s.serviceKey, "/") {
			return fmt.Sprintf("%s%s", s.serviceKey, s.target.Endpoint())
		} else {
			return fmt.Sprintf("%s/%s", s.serviceKey, s.target.Endpoint())
		}
	}
}

func (s *serviceResolver) rePull() error {
	ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancelFunc()
	resp, err := s.client.Get(ctx, s.makeKey(), clientv3.WithPrefix())
	if err != nil {
		return err
	}
	s.addrMap = make(map[string]resolver.Address)
	for _, ev := range resp.Kvs {
		key := strings.TrimPrefix(string(ev.Key), s.serviceKey)
		s.addServer(key, ev.Value)
	}
	s.syncToGrpc()
	return nil
}

func (s *serviceResolver) addServer(key string, value []byte) {
	var si ServiceInfo
	if err := json.Unmarshal(value, &si); err != nil {
		return
	}
	s.addrMap[key] = resolver.Address{
		Addr: fmt.Sprintf("%s:%d", si.Ip, si.Port),
	}
}

func (s *serviceResolver) delServer(key string) {
	if _, ok := s.addrMap[key]; ok {
		delete(s.addrMap, key)
	}
}

func (s *serviceResolver) syncToGrpc() {
	addrSlice := make([]resolver.Address, 0, 10)
	for _, v := range s.addrMap {
		addrSlice = append(addrSlice, v)
	}
	err := s.cc.UpdateState(resolver.State{Addresses: addrSlice})
	if err != nil {
		return
	}
}

func (s *serviceResolver) watcher() {
	rePull := false
	for {
		select {
		case <-s.closed:
			return
		default:
		}

		if rePull {
			if err := s.rePull(); err != nil {
				time.Sleep(5 * time.Second)
				continue
			}
		}
		rch := s.client.Watch(context.Background(), s.serviceKey, clientv3.WithPrefix())
	loop:
		for {
			select {
			case <-s.closed:
				return
			case resp, ok := <-rch:
				if !ok {
					rePull = true
					break loop
				}
				for _, ev := range resp.Events {
					key := strings.TrimPrefix(string(ev.Kv.Key), s.serviceKey)
					switch ev.Type {
					case mvccpb.PUT:
						s.addServer(key, ev.Kv.Value)
					case mvccpb.DELETE:
						s.delServer(key)
					}
				}
				s.syncToGrpc()
			}
		}
	}
}
相关推荐
Code季风3 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)
数据库·微服务·go·json·服务发现·consul
Code季风7 小时前
微服务分布式配置中心:Gin Web 服务层与 gRPC 服务层集成 Nacos 实战
分布式·微服务·rpc·架构·go·gin·consul
柒七爱吃麻辣烫9 小时前
八股文系列-----SpringBoot自动配置的流程
java·spring boot·rpc
考虑考虑9 小时前
go中的Map
后端·程序员·go
DemonAvenger12 小时前
Go中UDP编程:实战指南与使用场景
网络协议·架构·go
活椰拿铜12 小时前
Go实现超时控制
go
程序员爱钓鱼14 小时前
Go项目上线部署最佳实践:Docker容器化从入门到进阶
后端·google·go
Joker-01111 天前
牛客周赛Round 99(Go语言)
go·牛客周赛
Code季风1 天前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin