服务发现原理与grpc源码解析

目录

一 服务发现基础概念

1.1  基础概念

1.2服务发现的两种模式

二 grpc中的服务发现

1.1 grpc客户端发起调用

1.2  grpc reslover调用流程

三 总结 relover调用时序图:



一 服务发现基础概念

为什么需要服务发现
在微服务架构中,在生产环境中服务提供方都是以集群的方式对外提供服务,集群中服务的IP随时都可能发生变化,如服务重启,发布,扩缩容等,因此我们需要及时获取到对应的服务节点,这个获取的过程其实就是“服务发现”。

1.1  基础概念

  • 服务的注册(Service Registration)

    当服务启动的时候,应该通过某种形式(比如调用API、产生上线事件消息、在Etcd中记录、存数据库等等)把自己(服务)的信息通知给服务注册中心,这个过程一般是由微服务框架来完成,业务代码无感知。

  • 服务的维护(Service Maintaining)

    尽管在微服务框架中通常都提供下线机制,但并没有办法保证每次服务都能优雅下线(Graceful Shutdown),而不是由于宕机、断网等原因突然失联,所以,在微服务框架中就必须要尽可能的保证维护的服务列表的正确性,以避免访问不可用服务节点的尴尬。

  • 服务的发现(Service Discovery)

    这里所说的发现特指消费者从微服务框架(服务发现模块)中,把一个服务标识(一般是服务名)转换为服务实际位置(一般是ip地址)的过程。这个过程(可能是调用API,监听Etcd,查询数据库等)业务代码无感知。

1.2服务发现的两种模式

服务端服务发现

服务调用方无需关注服务发现的具体细节,只需要知道服务的DNS域名即可。

对基础设施来说,需要专门支持负载均衡器,对于请求链路来说多了一次网络跳转,会有性能损耗。

客户端服务发现

对于客户端服务发现来说,由于客户端和服务端采用了直连的方式,比服务端服务发现少了一次网络跳转,对于服务调用方来说需要内置负载均衡器,不同的语言需要各自实现。

客户端服务发现&服务端服务发现对比

对于微服务架构来说,我们期望的是去中心化依赖,中心化的依赖会让架构变得复杂,当出现问题的时候也会让整个排查链路变得繁琐,所以在 go-zero 中采用的是客户端服务发现的模式。

二 grpc中的服务发现

1.1 grpc客户端发起调用

在介绍Resolver之前先看下grpc官方给出的客户端调用列子。

代码地址:grpc-go/examples/helloworld/greeter_client at master · grpc/grpc-go · GitHub

核心内容分三块:

  • 调用 grpc.Dial 方法,指定服务端 target,创建 grpc 连接代理对象 ClientConn
  • 调用 proto.NewGreeterClient 方法,基于 pb 桩代码构造客户端实例
  • 调用 client.SayHello 方法,真正发起 grpc 请求
package main

import (
	"context"
	"flag"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
	flag.Parse()
	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

最终通过 ClientConn 的Invoke 方法真正发起调用请求。

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
	out := new(HelloReply)
	err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}


1.2  grpc reslover调用流程

dail创建连接源码入口位于grpc/client.go 中, 我这里先知关注parseTargetAndFindResolver方法

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {

    //.......
    	// Determine the resolver to use.
	if err := cc.parseTargetAndFindResolver(); err != nil {
		return nil, err
	}
    //.......
}

target句法可参考gprc官方文档
// The target name syntax is defined in
https://github.com/grpc/grpc/blob/master/doc/naming.md.

parseTargetAndFindResolver解析用户的拨号目标 并存储在`cc.parsedTarget`中。

要使用的解析器是根据解析的目标中的方案确定的并将其存储在“cc.resolverBuilder”中。 

// parseTargetAndFindResolver parses the user's dial target and stores the
// parsed target in `cc.parsedTarget`.
//
// The resolver to use is determined based on the scheme in the parsed target
// and the same is stored in `cc.resolverBuilder`.
//
// Doesn't grab cc.mu as this method is expected to be called only at Dial time.
func (cc *ClientConn) parseTargetAndFindResolver() 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.URL.Scheme)
		if rb != nil {
			cc.parsedTarget = parsedTarget
			cc.resolverBuilder = rb
			return nil
		}
	}

	// We are here because the user's dial target did not contain a scheme or
	// specified an unregistered scheme. We should fallback to the default
	// scheme, except when a custom dialer is specified in which case, we should
	// always use passthrough scheme.
	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 err
	}
	channelz.Infof(logger, cc.channelzID, "parsed dial target is: %+v", parsedTarget)
	rb = cc.getResolver(parsedTarget.URL.Scheme)
	if rb == nil {
		return fmt.Errorf("could not get resolver for default scheme: %q", parsedTarget.URL.Scheme)
	}
	cc.parsedTarget = parsedTarget
	cc.resolverBuilder = rb
	return nil
}

阅读完parseTargetAndFindResolver 函数,可以看到会将builder存储到cc.resolverBuilder字段中,返回DialContext函数,发现newCCResolverWrapper会使用cc.resolverBuilder字段。

我们继续阅读newCCResolverWrapper函数源码:
newCCResolverWrapper使用resolver.Builder来构建一个resolver,返回一个ccResolverWrapper对象,该对象包装新构建的解析器。

// newCCResolverWrapper uses the resolver.Builder to build a Resolver and
// returns a ccResolverWrapper object which wraps the newly built resolver.
func newCCResolverWrapper(cc resolverStateUpdater, opts ccResolverWrapperOpts) (*ccResolverWrapper, error) {
	ctx, cancel := context.WithCancel(context.Background())
	ccr := &ccResolverWrapper{
		cc:                  cc,
		channelzID:          opts.channelzID,
		ignoreServiceConfig: opts.bOpts.DisableServiceConfig,
		serializer:          grpcsync.NewCallbackSerializer(ctx),
		serializerCancel:    cancel,
	}

	r, err := opts.builder.Build(opts.target, ccr, opts.bOpts)
	if err != nil {
		cancel()
		return nil, err
	}
	ccr.resolver = r
	return ccr, nil
}

 在newCCResolverWrapper 方法 opts.builder.Build(opts.target, ccr, opts.bOpts)调用了我们自定义的Build方法。

三 总结 relover调用时序图:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
gRPC是一款开源的高性能远程过程调用(RPC)框架,由Google开发并开源。它基于HTTP/2和Protocol Buffers来实现跨平台、多语言的远程方法调用。grpc-go是gRPC的Go语言实现。 首先,我们来分析grpc-go的源码结构。grpc的核心代码位于grpc-go目录下,包括server、client、metadata等模块的代码实现。其中,server目录下的代码主要负责服务端的初始化、启动和处理请求;client目录下的代码则主要负责客户端的连接和发送请求;metadata目录下的代码保存了gRPC使用的元数据信息。 接着,我们来看一下grpc-go的基本工作流程。在服务端,首先要创建一个grpc.Server对象,然后通过调用Server的RegisterService方法注册一个或多个服务;然后通过调用Server的Serve方法启动服务。在客户端,首先要建立与服务端的连接,通过调用grpc.Dial方法创建一个grpc.ClientConn对象;然后通过该对象创建一个或多个服务的Client对象,最后通过Client对象调用远程方法。 grpc-go的底层代码主要依赖于Go语言的标准库和一些第三方库。其中,标准库主要包括net、http、io等模块;第三方库主要包括golang/protobuf、google.golang.org/grpc等。grpc-go通过protobuf编译器生成的代码来对消息进行序列化和反序列化,利用HTTP/2协议的多路复用特性来提高通信效率。 grpc-go的源码解析还涉及一些高级特性,如流式RPC、拦截器、错误处理等。流式RPC可以实现客户端和服务端之间的双向流式通信,通过使用流来传输大量的数据。拦截器可以用于对请求和响应进行预处理和后处理,对于日志记录、认证、鉴权等方面非常有用。错误处理可以帮助程序员更好地处理可能发生的异常情况,提高代码的可靠性。 总而言之,grpc-go的源码解析涉及了很多基础知识和高级特性,需要深入理解和掌握。通过对grpc-go源码的分析,我们可以更好地理解它的工作原理,从而能够更好地使用和扩展该框架。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值