grpc

grpc服务发现分析

Posted by Liangjf on July 20, 2020

grpc服务发现分析

etcdv3作为grpc服务发现组件 github

naming

naming/naming.go

grpc自带只支持dns域名解析名字服务注册发布, 但是通过开放接口的形式, 满足用户的自定义名字发现组件

创建一个监视器,返回是Watcher, 用于监视目标域名的名称解析, 从而得到ip地址

```go
type Resolver interface {
	// 返回一个watcher监听器, 可以自行通过for Next()来等待获取可用目标ip地址
	Resolve(target string) (Watcher, error)
}

```

```go
type Watcher interface {
	// Next blocks until an update or error happens. It may return one or more
	// updates. The first call should get the full set of the results. It should
	// return an error if and only if Watcher cannot recover.
	Next() ([]*Update, error)
	// Close closes the Watcher.
	Close()
}

```

只要外部实现了此两个接口, 就能通过插件的形式使用自定义名字发现服务.

比如框架自带的dns域名解析名字服务naming/dns_resolver.go:58

通过dnsResolver和dnsWatcher两个结构,实现上面的接口, 从而实现域名解析服务

但是看源码, 不建议使用这个包了, 而是使用resolver/resolver.go

resolver

```go
//ClientConn 客户端连接包装, 维护连接的状态,可用连接列表
type ClientConn interface {
	UpdateState(State)
	ReportError(error)
	NewAddress(addresses []Address)
	NewServiceConfig(serviceConfig string)
	ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult
}

//Builder 创建命名服务,返回Resolver, 可用于获取一个可用地址
type Builder interface {
	Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
	Scheme() string
}

//Resolver 监听目标服务的可用地址, 更新本地可用地址列表
type Resolver interface {
	ResolveNow(ResolveNowOptions)
	Close()
}

```

关键的是这三个接口, 其实实质是Builder接口, 这里使用了建造者设计模式, 通过把命名服务的创建和表示分离, 不像上面实现接口的方式, 有点粗糙. 直接把命名服务的创建放到外部创建, 使内部框架保持统一.

看例子, dns命名服务的实现, resolver/dns/dns_resolver.go:34–>internal/resolver/dns/dns_resolver.go:100的实现

```go
type dnsBuilder struct{}

//创建并启动一个DNS解析器,该解析器监视目标的名称解析
func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	//省略了ip解析等代码
    ...

	// DNS address (non-IP).
	ctx, cancel := context.WithCancel(context.Background())
	d := &dnsResolver{
		host:                 host,
		port:                 port,
		ctx:                  ctx,
		cancel:               cancel,
		cc:                   cc,
		rn:                   make(chan struct{}, 1),
		disableServiceConfig: opts.DisableServiceConfig,
	}

	...

	d.wg.Add(1)
	//创建新的goroutine实时监听域名解析, 监听变更
	go d.watcher()

	//实现ResolveNow, 并且返回ResolveNow接口, 用于通知域名解析有变更
	d.ResolveNow(resolver.ResolveNowOptions{})
	return d, nil
}


// ResolveNow 更新一波监听域名的可用列表
func (d *dnsResolver) ResolveNow(resolver.ResolveNowOptions) {
	select {
	//通知有变更
	case d.rn <- struct{}{}:
	default:
	}
}


//watcher 后台实时监听更新通知goroutine
func (d *dnsResolver) watcher() {
	defer d.wg.Done()
	for {
		//收到通知,来一波域名解析变更更新
		select {
		case <-d.ctx.Done():
			return
		case <-d.rn:
		}

		//域名解析
		state, err := d.lookup()
		if err != nil {
			d.cc.ReportError(err)
		} else {
			//更新客户端连接状态
			d.cc.UpdateState(*state)
		}

		// Sleep to prevent excessive re-resolutions. Incoming resolution requests
		// will be queued in d.rn.
		t := time.NewTimer(minDNSResRate)
		select {
		case <-t.C:
		case <-d.ctx.Done():
			t.Stop()
			return
		}
	}
}
```

通过看dns resolver的实现, 可用得出在实现Build接口时, 需要有实时的watch服务可用地址这个过程, 看dns是使用创建一个goroutine来等待ResolveNow的通知, 再发起一次此连接的命名服务可用地址列表的更新.

因此, 得出Build是用于创建一个命名服务, ResolveNow用于发起一次连接可用服务地址列表的通知, Build内部的watch goroutine通知会拉取新的可用地址列表, 刷新本地服务可用地址列表缓存.

ClientConn接口用于维护客户端连接conn的状态, 服务可用地址列表等. ClientConn被ccResolverWrapper包装了, ccResolverWrapper是ClientConn的包装器

ClientConn结构, google.golang.org/grpc@v1.27.0/clientconn.go:473是grpc客户端的连接对象,

```go
//google.golang.org/grpc@v1.27.0/clientconn.go:473
type ClientConn struct {
	ctx    context.Context
	cancel context.CancelFunc
    ...
	csMgr        *connectivityStateManager
	blockingpicker    *pickerWrapper
	resolverWrapper *ccResolverWrapper
	balancerWrapper *ccBalancerWrapper
	...
}
```

包含了ccResolverWrapper包装器, 因此ClientConn结构具有命名服务的能力.

```go
//ClientConn grpc客户端抽象结构
func (cc *ClientConn) resolveNow(o resolver.ResolveNowOptions) {
	cc.mu.RLock()
	r := cc.resolverWrapper
	cc.mu.RUnlock()
	if r == nil {
		return
	}
	go r.resolveNow(o)
}
```

===>

```go
//ccBalancerWrapper 负载均衡器
func (ccb *ccBalancerWrapper) ResolveNow(o resolver.ResolveNowOptions) {
	ccb.cc.resolveNow(o)
}
```

以上就是grpc的命名服务, 从第一版到最新版, 和被谁使用的分析…