Go语言大厂编程 gRPC——服务发现 源码解析

312 阅读2分钟

服务发现的架构设计 gRpc-服务发现流程.png

grpc 服务注册发现流程

rpcx.png

parseTargetAndFindResolver

func (cc *ClientConn) parseTargetAndFindResolver() (resolver.Builder, error) {
   channelz.Infof(logger, cc.channelzID, "original dial target is: %q", cc.target)

   var rb resolver.Builder
   parsedTarget, err := parseTarget(cc.target)
   if err != nil {
      channelz.Infof(logger, cc.channelzID, "dial target %q parse failed: %v", cc.target, err)
   } else {
      channelz.Infof(logger, cc.channelzID, "parsed dial target is: %+v", parsedTarget)
      rb = cc.getResolver(parsedTarget.Scheme)
      if rb != nil {
         cc.parsedTarget = parsedTarget
         return rb, nil
      }
   }

   defScheme := resolver.GetDefaultScheme()
   channelz.Infof(logger, cc.channelzID, "fallback to scheme %q", defScheme)
   canonicalTarget := defScheme + ":///" + cc.target

   parsedTarget, err = parseTarget(canonicalTarget)
   if err != nil {
      channelz.Infof(logger, cc.channelzID, "dial target %q parse failed: %v", canonicalTarget, err)
      return nil, err
   }
   channelz.Infof(logger, cc.channelzID, "parsed dial target is: %+v", parsedTarget)
   rb = cc.getResolver(parsedTarget.Scheme)
   if rb == nil {
      return nil, fmt.Errorf("could not get resolver for default scheme: %q", parsedTarget.Scheme)
   }
   cc.parsedTarget = parsedTarget
   return rb, nil
}

func (cc *ClientConn) getResolver(scheme string) resolver.Builder {
   for _, rb := range cc.dopts.resolvers {
      if scheme == rb.Scheme() {
         return rb
      }
   }
   return resolver.Get(scheme)
}

这段代码主要干了两件事情,parseTargetcc.getResolver 获取了一个 resolverBuilderparseTarget 其实就是将 target 赋值给了 resolver.Target 对象的 Endpoint 属性,如下

func parseTarget(target string) (resolver.Target, error) {
   u, err := url.Parse(target)
   if err != nil {
      return resolver.Target{}, err
   }
   endpoint := u.Path
   if endpoint == "" {
      endpoint = u.Opaque
   }
   endpoint = strings.TrimPrefix(endpoint, "/")
   return resolver.Target{
      Scheme:    u.Scheme,
      Authority: u.Host,
      Endpoint:  endpoint,
      URL:       *u,
   }, nil
}

再来看 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.Register(你实现的Builder)

resolver 官方组件库

resolver 主要提供了一个名字解析的规范,所有的名字解析服务可以实现这个规范。

// 创建一个解析器,用于监视服务端地址更新
type Builder interface {
    Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
    Scheme() string
}

我们在调用 Dial 方法发起 rpc 请求之前需要创建一个 ClientConn 连接,在 DialContext 这个方法中对 ClientConn 各属性进行了赋值,其中newCCResolverWrapper 加载了对服务端地址的解析方式。

rWrapper, err := newCCResolverWrapper(cc)

func newCCResolverWrapper(cc *ClientConn, rb resolver.Builder) (*ccResolverWrapper, error) {
   ccr := &ccResolverWrapper{
      cc:   cc,
      done: grpcsync.NewEvent(),
   }

   var credsClone credentials.TransportCredentials
   if creds := cc.dopts.copts.TransportCredentials; creds != nil {
      credsClone = creds.Clone()
   }
   rbo := resolver.BuildOptions{
      DisableServiceConfig: cc.dopts.disableServiceConfig,
      DialCreds:            credsClone,
      CredsBundle:          cc.dopts.copts.CredsBundle,
      Dialer:               cc.dopts.copts.Dialer,
   }

   var err error
   
   ccr.resolverMu.Lock()
   defer ccr.resolverMu.Unlock()
   ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, rbo)
   if err != nil {
      return nil, err
   }
   return ccr, nil
}

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

总结

grpc 的服务发现,主要通过 resolver 接口去定义,支持业务自己实现服务发现的 resolver


欢迎指教,有疑问或者有其它感兴趣的学习方向可以在下方评论,我们将一一为您解答