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

news2025/1/13 13:31:19

一 服务发现基础概念

为什么需要服务发现
在微服务架构中,在生产环境中服务提供方都是以集群的方式对外提供服务,集群中服务的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调用时序图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/541084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ThreadLocal使用和原理

ThreadLocal是线程本地变量,用来解决并发下数据隔离性的问题,不能解决共享。 他可以将一个变量拷贝的线程内,线程调用时再线程内进行使用,相当于给每个线程复制一个副本供各个线程使用。 ThreadLocal简单使用 他的目的很简单&a…

Unity用AI制作天空盒,并使用,详细图文教程

Unity用AI制作天空盒,并使用,详细图文教程 效果AI制作使用总结版权声明 效果 先上我自己做的效果 AI制作 首先登录AI制作的网站,打开就可以用,不需要登录 这是网址:https://skybox.blockadelabs.com/ 1.创建新的 2…

idea操作——如何format代码

1.选中需要format的类,然后右击,选择reformat code 2.出现的复选框根据自己的需求进行选择。然后点击OK即可。 Optimize imports 优化导入 选中此复选框可从所选范围内的代码中删除未使用的导入语句。 删除代码中没使用到的import 。使导入最优化 Rearr…

【C++】-模板初阶(函数和类模板)

作者:小树苗渴望变成参天大树 作者宣言:认真写好每一篇博客 作者gitee:gitee 作者专栏:C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧! 文章目录 前言一、为什么要模板&…

【面试题】谈谈你对vite的了解

大厂面试题分享 面试题库 前后端面试题库 (面试必备) 推荐:★★★★★ 地址:前端面试题库 web前端面试题库 VS java后端面试题库大全 1.什么是vite vite是新一代前端构建工具,能够显著提升前端开发体验。他是使用…

东邻到家小程序|东邻到家小程序源码|东邻到家小程序开发功能

上门服务这几年已经越来越火爆,不论是家政、按摩、美甲等等都在不断的发展上门服务,这几年东邻到家小程序系统在不断的摸索阶段,对于系统各方面的需求也在不断提升,东郊到家小程序通过线上匹配用户和技师的需求,让人们…

低代码开发打破CRM开发瓶颈,是否靠谱呢?

低代码开发平台是一种快速开发应用程序的新兴技术,它通过提供可视化开发工具和预配置组件,使开发者更加高效地创建应用程序。低代码开发平台的出现为企业开发带来了一次全新的机遇,尤其是在CRM领域。但是,低代码开发在CRM领域中是…

得物前端巡检平台的建设和应用(建设篇)

1.背景 我们所在的效能团队,对这个需求最原始的来源是在一次“小项目”的评审中,增长的业务同学提出来的,目的在于保障前端页面稳定性的同时减少大量测试人力的回归成本。 页面稳定性提升,之前迭代遇见过一些C端的线上问题&…

自学软件测试,我还是劝你算了吧。。。

本人8年测试经验,在学测试之前对电脑的认知也就只限于上个网,玩个办公软件。这里不能跑题,我为啥说:自学软件测试,一般人我还是劝你算了吧?因为我就是那个一般人! 软件测试基础真的很简单&…

乒乓测评:电视盒子哪个牌子最好?2023电视盒子品牌排行榜

这里是乒乓测评,致力于带来更客观、真实的数码产品体验。本期我们测评的主题是电视盒子哪个牌子最好,为此我们购入了二十多款热门电视盒子,从硬件配置、视频流畅度、系统界面、操作、广告程度等方面进行多维度的测评,根据结果整理…

C++每日一练:详解-买铅笔影分身三而竭

文章目录 前言一、买铅笔二、影分身三、三而竭总结 前言 这回又换成C了,Python要用C也要用,没有哪个正经程序员只会一门语言的,咱可是CSDN认证带V的全栈攻城狮。今天的题目除了买铅笔都还是有点难度的,虽然影分身主要是考验阅读理…

【matlab报错】:函数或变量 ‘randint‘ 无法识别。

问题产生 首先定位问题,这个问题是由matlab版本造成的,随着matlab版本的更新,matlab删除了 randint 这个函数。 怎么替代呢?鼠标悬浮在报错代码上面,如下: matlab提示我们对代码进行相应更改后改用randi了…

基于SSM+JSP校园二手交易系统

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…

5年测试经验华为社招:半月3次面试,成功拿到Offer

背景经历 当时我工作近5年,明显感觉到了瓶颈期。具体来说,感觉自己用过很多测试框架和测试工具、做过一些测试开发、也有过高并发的性能测试,但是从技术深度上感觉不足,到后期时做事也没有明显挑战,完全适应了公司节奏…

新的网络钓鱼即服务平台让网络犯罪分子生成令人信服的网络钓鱼页面

至少从2022年中期开始,网络犯罪分子就利用一个名为“伟大”的新型网络钓鱼即服务(PhaaS或PaaS)平台来攻击微软365云服务的企业用户,有效地降低了网络钓鱼攻击的门槛。 思科Talos研究员蒂亚戈佩雷拉表示:“目前,Greatness只专注于微软365钓鱼…

[Hadoop]大数据导论与Linux基础

目录 大数据导论 企业数据分析方向 数据分析基本步骤 大数据时代 分布式与集群 Linux操作系统概述 操作系统概念与分类 Linux起源与发展 Linux内核与发行版本 VMware Workstation虚拟机使用 VMware虚拟机概念 VMware虚拟机常规使用 Linux常用基础命令 Linux文件系…

Spring Boot单元测试

什么是单元测试? 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证的过程就叫单元测试。 单元测试是开发人员编写的一小段代码,用于检验被测代码的一个很小的、很明确的(代码) 功能是否正确。执行单元测试就是为了证明某…

Java面试知识点(全)- Java并发- Java并发基础一

Java面试知识点(全) 导航: https://nanxiang.blog.csdn.net/article/details/130640392 注:随时更新 多线程解决什么问题 CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异&#xff0c…

PMP课堂模拟题目及解析(第11期)

101. 一家咨询公司的负责人启动一个项目来扩大公司提供的服务数量,这公司具有竞争优势、出色的企业知识以及卓越的声誉,高管团队担心与增加新服务相关的负面业务结果的可能性。若要评估负面业务结果的可能性和影响,项目经理应该使用什么&…

matlab写入txt文件进行自动化测试总结:fopen、fclose和fprintf的用法

前言 日常学习的过程中使用了matlab读写txt文件,记录一下基本函数的使用,本文主要介绍了fopen、fclose和fprintf几个函数,这些主要是面向txt格式的文件保存数据。还有其他几个函数,比如fread和fwrite,用过但是他们是针…