1. 了解注册中心

微服务中服务发现注册是较为核心的话题,比如在当前项目中,社交api服务在实现好友列表查询的时候需要调用到用户rpc服务和社交rpc服务中的方法和功能才能完成业务。此时对社交api服务而言就需要知道用户rpc服务和社交rpc服务的地址,如果因需求更换了用户rpc服务的信息,而社交api没有发现则会直接影响到整个系统。

关于服务地址的方式获取方式有两种:

  1. 静态:基于在配置文件中定义好各个服务的地址集合,但是存在致命的问题,如果某一个服务出现异常更换了一个新的服务并更换了新的服务地址,此时对系统而言就无法发现新的服务。

  2. 动态:将服务地址存储在某一个位置如注册中心,需要的时候从某一个位置获取服务的地址信息,优势则可以获取到最新服务。

ef9d66df8ef8482ee35b25fb875fb7a6.png

2. 服务发现的流程

b0ffab90b34971de0173e1ffcdcbbd71.png

在服务发现的流程中主要的流程为:

  1. 服务向注册中心注册自己的服务信息主要信息为请求地址

  2. 客户端通过服务注册中心获取到需要请求连接的服务对象

  3. 客户端获取到服务的地址之后即可与服务端发起连接

而服务发现的方式可分为两种方式,客户端发现和基于中心发现

客户端发现

客户端发现的方式是指,客户端从注册中心中获取到服务端的地址信息列表,在客户端自己的内部实现负载均衡的机制处理,这种方式的优点是去中心化,在请求调度中会少一次的网络调整请求过程,但需要客户端自己实现发现机制,目前go-zero就是采用这种模式。

796d20894f726289dc76faeceffae12f.png

基于中心发现

在基于中心的发现方式中,客户端不需要自己取实现服务发现及负载均衡的功能,以及服务的维护工作,只需要知道服务的中心地址即可,这里的中心对象典型代表就是nginx,在服务中心中会维护注册的服务状态并对服务新服务发现提供负载均衡,但对整个系统而言会增加对中心的依赖。

b5feef858bf6590c8b18b85b68006143.png

3. 服务注册中心的方案及对比

在目前的业界中,关于服务注册中心的实现主流技术有consul、etcd、Zookeeper。

zookeeper:是这类型项目中发展最久的, 基于java开发,整体生态非常成熟,可靠,也被许多公司所应用。其数据存储的格式类似于文件系统,如果运行在一个服务器集群中,Zookeper将跨所有节点共享配置状态,每个集群选举一个领袖,客户端可以连接到任何一台服务器获取数据。

  • 优点:成熟、健壮、可靠

  • 缺点:复杂性较大、维护难度大、功能繁多但实际需要单一

etcd:是go语言开发采用http协议的键值对的存储系统,,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。其很容易部署、安装和使用,提供了可靠的数据持久化特性。它是安全的并且文档也十分齐全

consul:也是基于go语言开发,是强一致性的数据存储,使用gossip形成动态集群。它提供分级键/值存储方式、同时提供了服务发现及服务健康检测机制。而zookeeper与etcd只提供了键值对的方式,也就意味着开发人员构建服务发现注册的时候需要自己取实现,而consul因为内部实现顾客户端只需要通过http接口获取发现即可。

4. go-zero服务注册

首先在分析的机制上我们需先从服务的注册开始,而服务的注册可通过在服务运行启动的文件作为入口进入分析。在之前的案例中就已分析过go-zero的启动过程, 因此根据go-zero的启动过程在NewServer中就可以看到服务注册调度位置。

/go-zero/zrpc/server.go

89c918f50241707c884f1518b73f7c7a.png

在NewServer中会验证是否使用了etcd,如果应用则会创建NewRpcPubServer,如果没有则会直接创建NewRpcServer作为服务。

根据之前的分析是已知NewRPCServer是go-zero的服务核心对象,而NewRpcPubServer本质在底层中也是有应用它

/go-zero/zrpc/internal/rpcpubserver.go

6f70bae0828755169f85cfa39e00d79a.png

在registerEtcd方法共可分为四步理解

  1. 确定注册监听IP

  2. 设置etcd配置操作事项如tls

  3. 创建publisher对象

  4. 通过publisher调用etcd客户端并完成注册

关于registerEtcd的执行在当前代码往下即有调用

/go-zero/zrpc/internal/rpcpubserver.go

62333cd037a47520d4e8faf5c01c39a3.png

NewServer中获取到通过NewRpcPubServer返回的keepAliveServer对象的时候,在入口就会调用Start启动服务,从而触发注册动作。

/go-zero/core/discov/publisher.go

edccb0248afba35510ee9f5260544a52.png

e6119f9b3b3dc5ca544e7e50d473eb5d.png

在publisher中调用KeepAlive会先获取连接请求,然后再向etcd进行注册,而在注册之后会与etcd保持异步的连接,当收到新的变更信息之后又会重新的向etcd注册服务。

另外需注意:在完成etcd注册之后,go-zero会通过keepAliveAsync异步与etcd基于p.leaseId保持长连接,以确定是存活状态,如果服务异常退出了,那么也就无法进行续期,服务发现也就能自动识别到该服务异常下线了。

5. grpc的服务发现注册原理

原本接下来应对go-zero的服务发现注册原理进行分析的,但实际上此时我们需要先了解grpc的处理方式,因为在go-zero中无论是server还是client在本质上都是对grpc的使用进行封装,因此对程序的本质来说实际就是在使用grpc。

而go-zero的client就是在grpc的基础上封装,因此我们需要先了解grpc是如何做服务发现注册的,及如何自定义服务发现注册机制,这样就更容易理解go-zero的内部实现。

grpc内部服务发现机制实现较为复杂,我们将整个发现过程分为三个点,

  1. 框架会如何设计服务发现机制

  2. grpc服务发现机制的加载流程

  3. grpc的调度如何获取客户端连接的

5.1. 框架会如何设计服务发现机制

首先需要知道的是grpc是以客户端的方式去发现服务端,即客户端会从注册中心中拉群到服务端的ip地址信息,然后内部保持维护更新。

在分析前首先我们可以思考如果我们自己来做应如何去实现?从功能业务上我们需关注话题有。

  1. 注册中心的多样性如何适配

  2. 如何维护服务列表并更新最新的服务

  3. 请求调度如何从服务列表中获取连接完成请求

5be36057fb006cbbc54a1b9a4bf31e55.png

从实现的执行流程上即开始对服务初始化→加载服务发现机制→调用服务维护机制→结束;而对服务的维护处理上会用到异步机制,方式可以是定时也可以是订阅。

而在调度中会基于维护的服务列表信息,通过负载均衡机制从中获取到连接的服务信息,并且完成请求功能。

考虑到多种注册机制为了方便扩展,可以给服务发现机制定义一个统一的接口,并要求集成注册机制的对象进行实现。

比如在grpc中就定义了统一的接口, 这样要求采用各个注册服务机制的时候需要实现如下接口。

/grpc/resolver/discov/resolver.go

27ed561fde47861ef763c51ae11a89d9.png

  • build:在grpc中对于它的定义是根据传递的Target(目标)、连接构建出解析,但依据我们目前分析的流程,可以理解到它的功能中就会包含有对服务发现维护的更新功能。

  • scheme:因为没有参数的传递,只有返回string,从目前的流程上显然不会影响到服务的维护工作。而grpc用它的作用主要是作为服务发现机制的标识。

此处我们还少了一个问题,即服务发现机制如何集成注册到当前的系统中,针对该问题有从代码的编程上有两种方案

  1. 在创建的时候引用:该方式的特定简单明了,在代码编写中需将其存入在一个全局的属性或对象中。

de68c9ceffea2595ab6d1030a87a9fb4.png

  1. 使用注册表的方式:一般以map作为注册表key往往是字符串用于标识注册的对象,而value则存Builder

7a031be3a7cae789ad3108c9b470b31b.png

在go-grpc中实际上两种都有支持。

5.2 grpc服务发现机制的加载流程

325c2b67982f04df9620ab3531946da0.png

首先我们可以假定自己提前调用resolver.Register方法完成了基于etcd的服务发现注册机制

如在/go-zero/zrpc/resolver/internal/resolver.go:35中

ede260ce12aa78b3dd8b7b4b12ef36dc.png

具体调用grpc的代码/grpc/resolver/resolver.go:51

846055ed7a27d7e45f421e94404f7471.png

同时可以看到在下边针对m提供了一个Get方法

a27ec6b9c76f1aa87ab9e3d2cd855a26.png

在初始化的过程中grpc.DialContext通过调用parseTargetAndFindResolver方法会解析传递的target信息,同时还会去查找resolver。

/grpc/clientconn.go:1785

2ef35b17c88d87e86ec3e1deebdc371e.png

在getResolver方法即获取具体的服务发现机制,而获取方式优先于通过option传递的方式,如果没有才去resolver.Get获取

/grpc/clientconn.go

9dabff61e81be671a7efb60545b5ed37.png

注意:代码此时会将获取到的服务解析对象置于cc的resolverBuilder属性中。

17cbb9ec6e678bda10170541eef488c4.png

回到grpc.DialContext中,会调用exitIdleMode方法它会创建新的通道负载均衡及解析器,而在其下的initResolverWrapper会初始化针对cc的ResovlerWrapper,而实际上就是通过传递的服务发现机制的Builder方法构建的Resolver。

/grpc/clientconn.go

a0621faf15ad01e3b3af14b7616d665f.png

5.3 resovler的构建处理

resovler.builder的实现对象很多,我们以go-zero中的discovBuilder作为示例进行分析,了解是如何解析的。

/go-zero/zrpc/resolver/discovbuilder.go:14

a016cbd7a067be2ab88f518cd8882242.png

针对上面的方法需分两步了解

  1. grpc中的updateState: 该方法主要是修改grpc中客户端连接服务的地址信息及状态情况

  2. go-zero中的monitor: 该方法主要是监听etcd的服务信息的变化,然后调用grpc的updateState进行修改【是异步维护】

b1ad01ef798d986192da3ce16043324f.png

go-zero中的monitor

这里直接根据调度链路可以看到在handleWatchEvents方法中会监听key的put、delete操作,最终通过l.OnAdd或l.OnDelete调用grpc的grpc.updateState修改【注意调度过程是异步的】

/go-zero/core/discov/internal/registry.go

abd1d9a84899b40acdfc5013236932b0.png

grpc中的updateState

grpc的updateState方法中会根据接收到的resolver.State信息变更解析器的状态,以及连接状态

/grpc/resolver_conn_wrapper.go

62600c6b99a4921862142d6b4fefb8a5.png

这其中有两个任务:

  • 倘若还没设置过负载均衡器,则会对其进行设置(本次使用到的负载均衡策略为 round-robin 轮询算法)——ClientConn.maybeApplyDefaultServiceConfig

  • 对负载均衡器中缓存的 endpoint 数据进行更新—ccBalancerWrapper.updateClientConnState

5.4. 请求调度如何从服务列表中获取连接完成请求

5f104a2aac6690643523a4570928c10a.png

请求调度相对就简单很多,从之前分析过grpc的服务启动流程的及调度的newClientStreamWithParams方法开始

/grpc/stream.go

f7b21233c3a4cb09f6aef46bb41a1cdb.png

在调度中会通过cc的getTransport返回出获取到请求的负载均衡机制信息

/grpc/stream.go

f157219ec757707be8738290e58510a2.png

这时调度就会使用已经更新好的负载均衡缓存内容就可以与服务端建立连接通信

/grpc/picker_wrapper.go

c483bf7573f41b02fd36fe559a836fbc.png

grpc的服务发现机制对初学者而言会较难理解,因为在内部的调度层次比较深,调度较为繁杂,对于这种情况的理解可以先从功能的本质出发。

  1. 需理解go语言中接口的设计,可以用于规范多样化需要功能的实现。本次案例中就运用接口结合注册的方式提升了服务发现机制的扩展能力。

  2. 基于功能实现的理论上出发,代码实现一般般往往较为复杂,但是它都会遵循某一理论,因此我们可以先推导理论,然后再分析过程。如上我们是先从理论上实现某一个功能应具备什么条件,以此分析。

  3. 基于理论推导,结合代码的链路调度分析过程,用理论辅导理解程序的执行分析以及对程序执行的意图,其中理论可能会有差异但并不妨碍我们对程序的分析。

b31dc369cd534173b8406ba27a3d8372.png

END

360智汇云是以"汇聚数据价值,助力智能未来"为目标的企业应用开放服务平台,融合360丰富的产品、技术力量,为客户提供平台服务。

目前,智汇云提供数据库、中间件、存储、大数据、人工智能、计算、网络、视联物联与通信等多种产品服务以及一站式解决方案,助力客户降本增效,累计服务业务1000+。

智汇云致力于为各行各业的业务及应用提供强有力的产品、技术服务,帮助企业和业务实现更大的商业价值。

官网:https://zyun.360.cn 或搜索“360智汇云”

客服电话:4000052360

欢迎使用我们的产品!😊

关注公众号,干货满满的前沿技术文章等你来。想看哪方面内容,也欢迎留言和我们交流!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐