【grpc】grpc进阶三,服务发现

5 篇文章 0 订阅

一、 grpc服务解析接口

grpc 的 resolver 包中提供了可以自定义服务发现的接口:

// package google.golang.org/grpc/resolver
func Register(b Builder) {
	m[b.Scheme()] = b
}

// ...

type Builder interface {
	// 创建新的Resolver,需要保存 cc
	Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
	// 标识解析器,返回字符串,遵循RFC 3986,比如在服务地址输入 etcd://server.ip,会使用 Scheme 为 etcd 的解析器解析 server.ip
	Scheme() string
}

// ...

type Resolver interface {
	// 解析目标地址,需要在实现中调用上面 Build 中入参 ClientConn 的 UpdateState 方法更新解析结果。 注意可能存在并发情况
	ResolveNow(ResolveNowOptions)
	// Close closes the resolver.
	Close()
}

二、基于 etcd 的服务发现

我们首先考虑下实现服务发现需要哪些步骤:

  • 服务端:需要注册节点,而且注册的节点需要有过期时间,防止服务挂掉节点还能被客户端解析;相同服务集群下的不同地址注册需要用某种方式标识,以便客户端发现。这里我们使用 etcd 的租约实现保活;使用 服务名/地址 的方式注册,客户端可以用前缀的形式发现对应服务名下的所有地址。
  • 客户端:需要解析节点,解析时需要提前知道待解析的服务名,然后根据约定信息获取对应服务的地址列表,同时还需要监听服务注册信息的变化,及时更新服务节点信息。这里我们使用前缀的形式发现对应服务名下的所有地址,使用 etcd 的 watch 机制监听服务节点变化。

服务发现的这部分代码这里单独拿出来放到了git仓库里:https://github.com/sauryniu/discovery.git
下面就一些实现把代码贴出来。

  • 服务节点信息
    这里我们创建个 node 结构保存服务节点信息:
// node.go

package discovery

import (
	"fmt"
	"strings"
)

type Node struct {
	Name string `json:"name"` // 名称
	Addr string `json:"addr"` // 地址
}

// 把服务名中的 . 转换为 /
func (s Node) transName() string {
	return strings.ReplaceAll(s.Name, ".", "/")
}

// 构建节点 key
func (s Node) buildKey() string {
	return fmt.Sprintf("/%s/%s", s.transName(), s.Addr)
}

// 构建节点前缀
func (s Node) buildPrefix() string {
	return fmt.Sprintf("/%s", s.transName())
}


  • 从 etcd 解析服务节点
// etcdResolver.go

package discovery

import (
	"context"
	"encoding/json"
	etcdV3 "go.etcd.io/etcd/client/v3"
	"google.golang.org/grpc/resolver"
	"os"
	"strings"
	"sync"
	"time"
)

const (
	tickerTime = 10 * time.Second
)

type etcdResolver struct {
	// 记录所有创建的解析器,同一个host只创建一个解析器
	mr    map[string]resolver.Resolver
	mrMux sync.RWMutex

	// etcd 客户端
	cli *etcdV3.Client
	// etcd 地址
	etcdAddrs []string
	// 连接 etcd 超时时间
	dialTimeout time.Duration

	// 需要解析的目标节点
	tnsMux        sync.RWMutex
	targetNodeSet map[string]*Node

	// 解析到的服务节点, host:addr:*Node
	snsMux       sync.RWMutex
	serviceNodes map[string]map[string]*Node

	cancel context.CancelFunc

	once sync.Once
}

// 获取当前解析到的服务节点
func (e *etcdResolver) getServiceNodes(name string) []*Node {
	e.snsMux.RLock()
	defer e.snsMux.RUnlock()
	nodes := make([]*Node, 0)

	for _, n := range e.serviceNodes[name] {
		nodes = append(nodes, n)

	}
	return nodes
}

// 设置解析到的服务节点
func (e *etcdResolver) setServiceNodes(name string, nodes ...*Node) {
	e.snsMux.Lock()
	defer e.snsMux.Unlock()
	ns := e.serviceNodes[name]
	if ns == nil {
		ns = make(map[string]*Node)
	}
	for i := range nodes {
		logger.Infof("resolver node [%s:%s]", name, nodes[i].Addr)
		ns[nodes[i].Addr] = nodes[i]
	}
	e.serviceNodes[name] = ns
}

// 溢出服务节点
func (e *etcdResolver) removeServiceNode(name, addr string) {
	e.snsMux.Lock()
	defer e.snsMux.Unlock()
	nodes := e.serviceNodes[name]
	if nodes == nil {
		return
	}
	delete(nodes, addr)
}

// 设置解析器
func (e *etcdResolver) setManuResolver(host string, m resolver.Resolver) {
	e.mrMux.Lock()
	defer e.mrMux.Unlock()
	e.mr[host] = m
}

// 根据host获取解析器
func (e *etcdResolver) getManuResolver(host string) (resolver.Resolver, bool) {
	e.mrMux.RLock()
	defer e.mrMux.RUnlock()
	if m, ok := e.mr[host]; ok {
		return m, ok
	}
	return nil, false
}

// 设置解析目标节点
func (e *etcdResolver) setTargetNode(host string) {
	e.tnsMux.Lock()
	e.targetNodeSet[host] = &Node{Name: host}
	e.tnsMux.Unlock()

	// 开始解析时进行相关操作,只执行一次
	e.once.Do(func() {
		var ctx context.Context
		ctx, e.cancel = context.WithCancel(context.Background())
		e.start(ctx)
	})
}

// 获取解析目标节点
func (e *etcdResolver) getTargetNodes() []*Node {
	e.tnsMux.RLock()
	defer e.tnsMux.RUnlock()

	nodes := make([]*Node, 0)
	for _, n := range e.targetNodeSet {
		nodes = append(nodes, n)
	}
	return nodes
}

// 解析所有需要解析的节点
func (e *etcdResolver) resolverAll(ctx context.Context) {
	nodes := e.getTargetNodes()
	for _, node := range nodes {
		// 根据前缀获取节点信息
		cctx, cancel := context.WithTimeout(context.Background(), e.dialTimeout)
		rsp, err := e.cli.Get(cctx, node.buildPrefix(), etcdV3.WithPrefix())
		cancel()
		if err != nil {
			logger.Errorf("get service node [%s] error:%s", node.Name, err.Error())
			continue
		}
		for j := range rsp.Kvs {
			n := &Node{}
			err = json.Unmarshal(rsp.Kvs[j].Value, n)
			if err != nil {
				logger.Errorf("get service node [%s] error:%s", node.Name, err.Error())
				continue
			}
			e.setServiceNodes(node.Name, n)
		}
	}

	// 解析完服务节点后,更新到连接上
	e.mrMux.RLock()
	defer e.mrMux.RUnlock()
	for _, v := range e.mr {
		v.ResolveNow(resolver.ResolveNowOptions{})
	}
}

func (e *etcdResolver) start(ctx context.Context) {
	if len(e.etcdAddrs) == 0 {
		panic("discovery should call SetDiscoveryAddress or set env DISCOVERY_HOST")
	}

	var err error
	e.cli, err = etcdV3.New(etcdV3.Config{
		Endpoints:   e.etcdAddrs,
		DialTimeout: e.dialTimeout,
	})
	if err != nil {
		panic(err)
	}

	// 开始先全部解析
	e.resolverAll(ctx)

	ticker := time.NewTicker(tickerTime)

	// 定时解析
	go func() {
		for {
			select {
			case <-ticker.C:
				e.resolverAll(ctx)

			case <-ctx.Done():
				logger.Infoln("resolver ticker exit")
				return
			}
		}
	}()

	// 每个节点watch变化
	nodes := e.getTargetNodes()
	for i := range nodes {
		go func(node *Node) {
			wc := e.cli.Watch(ctx, node.buildPrefix(), etcdV3.WithPrefix())
			for {
				select {
				case rsp := <-wc:
					for _, event := range rsp.Events {
						switch event.Type {
						case etcdV3.EventTypePut:
							n := &Node{}
							err = json.Unmarshal(event.Kv.Value, n)
							if err != nil {
								logger.Errorf("unmarshal to node error:%s", err.Error())
								continue
							}
							e.setServiceNodes(node.Name, n)
						case etcdV3.EventTypeDelete:
							n := &Node{}
							err = json.Unmarshal(event.Kv.Value, n)
							if err != nil {
								logger.Errorf("unmarshal to node error:%s", err.Error())
								continue
							}
							e.removeServiceNode(node.Name, n.Addr)
						}
					}
				case <-ctx.Done():
					logger.Infoln("resolver watcher exit")
					return
				}
			}
		}(nodes[i])
	}
}

func (e *etcdResolver) stop() {
	logger.Infoln("resolver stop")
	e.cancel()
}

func etcdResolverInit() {
	envEtcdAddr := os.Getenv("DISCOVERY_HOST")
	eRegister = &etcdRegister{
		nodeSet:     make(map[string]*Node),
		cli:         nil,
		dialTimeout: time.Second * 3,
		ttl:         3,
	}
	if len(envEtcdAddr) > 0 {
		eRegister.etcdAddrs = strings.Split(envEtcdAddr, ";")
	}
}
  • 注册节点信息到 etcd
// etcdRegister.go

package discovery

import (
	"context"
	"encoding/json"
	etcdV3 "go.etcd.io/etcd/client/v3"
	"google.golang.org/grpc/resolver"
	"os"
	"strings"
	"sync"
	"time"
)

type etcdRegister struct {
	// 注册节点set
	nodeSet map[string]*Node
	// etcd句柄
	cli *etcdV3.Client
	// etcd服务地址
	etcdAddrs []string
	// 连接超时时间
	dialTimeout time.Duration
	// etcd租约id
	etcdLeaseId etcdV3.LeaseID
	// 注册节点过期时间
	ttl int64
	// 取消函数,用去结束注册任务
	cancel context.CancelFunc
	once   sync.Once
}

// 新增注册的服务节点
func (e *etcdRegister) addServiceNode(node *Node) {
	e.nodeSet[node.buildKey()] = node

	// 新增注册节点的时候,开始执行注册任务
	e.once.Do(
		func() {
			var ctx context.Context
			ctx, e.cancel = context.WithCancel(context.Background())
			e.start(ctx)
		})
}

// 开始注册任务
func (e *etcdRegister) start(ctx context.Context) {
	if len(e.etcdAddrs) == 0 {
		panic("discovery should call SetDiscoveryAddress or set env DISCOVERY_HOST")
	}

	// 连接etcd
	var err error
	e.cli, err = etcdV3.New(etcdV3.Config{
		Endpoints:   e.etcdAddrs,
		DialTimeout: e.dialTimeout,
	})

	if err != nil {
		panic(err)
	}

	// 创建租约
	cctx, cancel := context.WithTimeout(ctx, e.dialTimeout)
	rsp, err := e.cli.Grant(cctx, e.ttl)
	if err != nil {
		panic(err)
	}
	cancel()

	e.etcdLeaseId = rsp.ID

	// 保活
	kc, err := e.cli.KeepAlive(ctx, rsp.ID)
	if err != nil {
		logger.Errorf("etcd keepalive error:%s", err.Error())
	}

	go func() {
		for {
			select {
			case kaRsp := <-kc:
				if kaRsp != nil {
					e.register(ctx)
				}
			case <-ctx.Done():
				logger.Infoln("register exit")
				return
			}
		}
	}()
}

// 注册节点
func (e *etcdRegister) register(ctx context.Context) {
	// 遍历所有的服务节点进行注册
	for _, n := range e.nodeSet {
		value, err := json.Marshal(n)
		if err != nil {
			logger.Errorf("json marshal node:%s error:%s", n.Name, err.Error())
			continue
		}
		// 使用租约id注册
		cctx, cancel := context.WithTimeout(ctx, e.dialTimeout)
		_, err = e.cli.Put(cctx, n.buildKey(), string(value), etcdV3.WithLease(e.etcdLeaseId))
		cancel()

		if err != nil {
			logger.Errorf("put %s:%s to etcd with lease id %d error:%s", n.buildKey(), string(value), e.etcdLeaseId, err.Error())
			continue
		}
		logger.WithField("component", "discovery").Infof("put %s:%s to etcd with lease id %d", n.buildKey(), string(value), e.etcdLeaseId)
	}
}

// 停止注册任务
func (e *etcdRegister) stop() {
	logger.Infoln("register stop")
	// 退出注册任务
	e.cancel()

	// 清理注册信息
	for _, n := range e.nodeSet {
		value, err := json.Marshal(n)
		if err != nil {
			logger.Errorf("json marshal node:%s error:%s", n.Name, err.Error())
			continue
		}
		cctx, cancel := context.WithTimeout(context.Background(), e.dialTimeout)
		_, _ = e.cli.Delete(cctx, n.buildKey())
		cancel()
		logger.Infof("delete %s:%s from etcd", n.buildKey(), string(value))
	}
}

// 注册器初始化
func etcdRegisterInit() {
	envEtcdAddr := os.Getenv("DISCOVERY_HOST")
	eResolver = &etcdResolver{
		mr:            make(map[string]resolver.Resolver),
		dialTimeout:   time.Second * 3,
		targetNodeSet: make(map[string]*Node),
		serviceNodes:  make(map[string]map[string]*Node),
	}
	if len(envEtcdAddr) > 0 {
		eResolver.etcdAddrs = strings.Split(envEtcdAddr, ";")
	}
}

  • 实现 Resolver 接口:
// resolver.go

package discovery

import "google.golang.org/grpc/resolver"

type IResolver interface {
	getServiceNodes(host string) []*Node
	setTargetNode(host string)
	setManuResolver(host string, m resolver.Resolver)
}

type manuResolver struct {
	cc     resolver.ClientConn
	target resolver.Target
	r      IResolver
}

func (m manuResolver) ResolveNow(options resolver.ResolveNowOptions) {
	nodes := m.r.getServiceNodes(m.target.URL.Host)
	addresses := make([]resolver.Address, 0)
	for i := range nodes {
		addresses = append(addresses, resolver.Address{
			Addr: nodes[i].Addr,
		})
	}
	if err := m.cc.UpdateState(resolver.State{
		Addresses: addresses,
	}); err != nil {
		logger.Errorf("resolver update cc state error:%s", err.Error())
	}
}

func (manuResolver) Close() {

}

  • 实现 Builder 接口:
// builder.go

package discovery

import "google.golang.org/grpc/resolver"

const (
	etcdScheme = "etcd"
)

type builder struct{}

func (builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	mr := manuResolver{
		cc:     cc,
		target: target,
		r:      eResolver,
	}
	// 记录解析器
	mr.r.setManuResolver(target.URL.Host, mr)
	// 记录需要解析的节点
	mr.r.setTargetNode(target.URL.Host)
	return mr, nil
}

func (builder) Scheme() string {
	return etcdScheme
}

func init() {
	resolver.Register(builder{})
}

最后是使用方法:

// client.go
import _ "github.com/sauryniu/discovery"
// server.go
import "github.com/sauryniu/discovery"
func init() {
	discovery.AddServiceNode(&discovery.Node{
		Name: "api.grpcdemo.com",
		Addr: "127.0.0.1:8080",
	})
}
// main.go

func main() {
	go server.Start(":8080")
	// 确保服务开起来
	time.Sleep(time.Second)
	client.Test("etcd://api.grpcdemo.com")
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值