1858415e04a958a97d701cf30bf98938
5 - grpc 服务发现

服务发现

在了解 grpc 服务发现之前,我们先来了解一下服务发现的路由方式。一般来说,我们有客户端路由和代理层路由两种方式。

客户端路由

客户端路由模式,也就是调用方负责获取被调用方的地址信息,并使用相应的负载均衡算法发起请求。调用方访问服务注册服务,获取对应的服务 IP 地址和端口,可能还包括对应的服务负载信息(负载均衡算法、服务实例权重等)。调用方通过负载均衡算法选取其中一个发起请求。如下:

server 启动的时候向 config server 注册自身的服务地址,server 正常退出的时候调用接口移除自身地址,通过定时心跳保证服务是否正常以及地址的有效性。

代理层路由

代理层路由,不是由调用方去获取被调方的地址,而是通过代理的方式,由代理去获取被调方的地址、发起调用请求。如下:

代理层路由这种模式,对 server 的寻址不再是由 client 去实现,而是由代理去实现。client 只是会对代理层发起简单请求,代理层去进行 server 寻址、负载均衡等。

grpc 官方介绍的服务发现流程如下:

由这张图可以看出,grpc 是使用客户端路由的方式。具体的过程我们在介绍负载均衡时再继续介绍

grpc 服务发现

之前在介绍 grpc client 时谈到了 resolver 这个类,对下面这段代码并未做详细介绍,我们来详细看看

if cc.dopts.resolverBuilder == nil {
        // Only try to parse target when resolver builder is not already set.
        cc.parsedTarget = parseTarget(cc.target)
        grpclog.Infof("parsed scheme: %q", cc.parsedTarget.Scheme)
        cc.dopts.resolverBuilder = resolver.Get(cc.parsedTarget.Scheme)
        if cc.dopts.resolverBuilder == nil {
            // If resolver builder is still nil, the parsed target's scheme is
            // not registered. Fallback to default resolver and set Endpoint to
            // the original target.
            grpclog.Infof("scheme %q not registered, fallback to default scheme", cc.parsedTarget.Scheme)
            cc.parsedTarget = resolver.Target{
                Scheme:   resolver.GetDefaultScheme(),
                Endpoint: target,
            }
            cc.dopts.resolverBuilder = resolver.Get(cc.parsedTarget.Scheme)
        }
    } else {
        cc.parsedTarget = resolver.Target{Endpoint: target}
    }

这段代码主要干了两件事情,parseTarget 和 resolver.Get 获取了一个 resolverBuilder

parseTarget 其实就是将 target 赋值给了 resolver target 对象的 endpoint 属性,如下

func parseTarget(target string) (ret resolver.Target) {
    var ok bool
    ret.Scheme, ret.Endpoint, ok = split2(target, "://")
    if !ok {
        return resolver.Target{Endpoint: target}
    }
    ret.Authority, ret.Endpoint, ok = split2(ret.Endpoint, "/")
    if !ok {
        return resolver.Target{Endpoint: target}
    }
    return ret
}

这里来看 resolver.Get 方法 ,这里从一个 map 中取出了一个 Builder

var (
    // m is a map from scheme to resolver builder.
    m = make(map[string]Builder)
    // defaultScheme is the default scheme to use.
    defaultScheme = "passthrough"
)

func Get(scheme string) Builder {
    if b, ok := m[scheme]; ok {
        return b
    }
    return nil
}

Builder 是在 resolver 中定义的,在了解 Builder 是啥之前,我们先来看看 resolver 这个结构体

resolver

// Package resolver defines APIs for name resolution in gRPC.
// All APIs in this package are experimental.

resolver 主要提供了一个名字解析的规范,所有的名字解析服务可以实现这个规范,包括 dns 解析类 dns_resolver 就是实现了这个规范的一个解析器。

resolver 中定义了 Builder ,通过调用 Build 去获取一个 resolver 实例

// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
    // Build creates a new resolver for the given target.
    //
    // gRPC dial calls Build synchronously, and fails if the returned error is
    // not nil.
    Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
    // Scheme returns the scheme supported by this resolver.
    // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
    Scheme() string
}

我们在调用 Dial 方法发起 rpc 请求之前需要创建一个 ClientConn 连接,在 DialContext 这个方法中对 ClientConn 各属性进行了赋值,其中有一行代码就完成了 build resolver 的工作。

// Build the resolver.
rWrapper, err := newCCResolverWrapper(cc)

func newCCResolverWrapper(cc *ClientConn) (*ccResolverWrapper, error) {
    rb := cc.dopts.resolverBuilder
    if rb == nil {
        return nil, fmt.Errorf("could not get resolver for scheme: %q", cc.parsedTarget.Scheme)
    }

    ccr := &ccResolverWrapper{
        cc:     cc,
        addrCh: make(chan []resolver.Address, 1),
        scCh:   make(chan string, 1),
    }

    var err error
    ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, resolver.BuildOption{DisableServiceConfig: cc.dopts.disableServiceConfig})
    if err != nil {
        return nil, err
    }
    return ccr, nil
}

不出意料,我们之前通过 get 去获取了一个 Builder, 这里调用了 Builder 的 Build 方法产生一个 resolver。

register()

上面我们说到了,resolver 通过 get 方法,根据一个 string key 去一个 builder map 中获取一个 builder,这个 map 在 resolver 中初始化如下,那么是怎么进行赋值的呢?

var (
    // m is a map from scheme to resolver builder.
    m = make(map[string]Builder)
    // defaultScheme is the default scheme to use.
    defaultScheme = "passthrough"
)

我们猜测肯定会有一个服务注册的过程,果然看到了一个 Register 方法

func Register(b Builder) {
    m[b.Scheme()] = b
}

所有的 resolver 实现类通过 Register 方法去实现 Builder 的注册,比如 grpc 提供的 dnsResolver 这个类中调用了 init 方法,在服务初始化时实现了 Builder 的注册

func init() {
    resolver.Register(NewBuilder())
}
top Created with Sketch.