Service Discovery
本质上服务发现的目的是解耦程序对服务具体位置的依赖,对于微服务架构来说,服务发现不是可选的,而是必须的。 要理解服务发现,需要知道服务发现解决了如下三个问题:
服务注册(Service Registration)当服务启动的时候,通过某种形式(比如调用API、产生上线事件消息、在Etcd中记录、存数据库等等)把自己(服务)的信息通知给服务注册中心,这个过程一般是由微服务框架来完成,业务代码无感知。
服务维护(Service Maintaining)尽管在微服务框架中通常都提供下线机制,但并没有办法保证每次服务都能优雅下线(Graceful Shutdown),而不是由于宕机、断网等原因突然失联,所以,在微服务框架中就必须要尽可能的保证维护的服务列表的正确性,以避免访问不可用服务节点的尴尬。
服务发现(Service Discovery)这里所说的发现是狭义的,它特指消费者从微服务框架(服务发现模块)中,把一个服务标识(一般是服务名)转换为服务实际位置(一般是ip地址)的过程。这个过程(可能是调用API,监听Etcd,查询数据库等)业务代码无感知。
服务发现有两种模式,分别是服务端服务发现和客户端服务发现。
对于服务端服务发现来说,服务调用方无需关注服务发现的具体细节,只需要知道服务的DNS域名即可,支持不同语言的接入,对基础设施来说,需要专门支持负载均衡器,对于请求链路来说多了一次网络跳转,可能会有性能损耗。
对于客户端服务发现来说,由于客户端和服务端采用了直连的方式,比服务端服务发现少了一次网络跳转,对于服务调用方来说需要内置负载均衡器,不同的语言需要各自实现。
对于微服务架构来说,我们期望的是去中心化依赖,中心化的依赖会让架构变得复杂,当出现问题的时候也会让整个排查链路变得繁琐,所以一般采用的是客户端服务发现的模式。
服务发现
gRPC 中进行服务发现的时候,涉及到的接口和类有如下这些。

gRPC 提供了自定义 Resolver 的能力来实现服务发现,通过 Register 方法来进行注册自定义的 Resolver,自定义的 Resolver 需要实现 Builder 接口,定义如下:
Scheme()返回一个stirng。注册的 Resolver 会被保存在一个全局的变量 m 中,m 是一个 map,这个 map 的 key 即为 Scheme() 方法返回的字符串。也就是多个 Resolver 是通过Scheme 来进行区分的,所以我们定义 Resolver 的时候 Scheme 不要重复,否则 Resolver 就会被覆盖。 通过下面的示例代码,来看一下自定义的 Builer 是在哪里被执行的。
整个的流程如下图所示。

实例代码
如上的示例代码,使用 gRPC 进行服务调用前:
先调用
grpc.Dial()方法创建一个grpc.ClientConn对象,底层调用DialContext。
其中
ClientConn是通过调用NewGreeterClient传入的,NewGreeterClient为 protoc 自动生成的代码,并赋值给cc属性。
最终发起调用时,调用了
ClientConn的Invoke()方法。
在整个客户端的调用流程中,核心就是 ClientConn 这个对象,在 DialContext() 方法种创建完成。
target 字段就是传入的地址,具体的采用的是 URI 的格式,如dns:[//authority/]host[:port],参看详细说明文档。通过调用 ClientConn 的 parseTargetAndFindResolver() 来获取 Resolver,在这个方法中主要就是把 target 中的 resolver name 解析出来,然后根据 resolver name 去保存 Resolver 的全局变量 m 中去找对应的 Resolver。
在获取到 Resolver 后在 newCCResolverWrapper() 方法中调用该 Resolver 的 Build()方法,也就是上面 Builder 接口中的方法。Build() 方法中的一个参数 target 如下所示。
Build()方法中的第二个参数,是 resoulver.ClientConn 接口(与上面的 grpc.ClientConn结构体要区分一下),它的实现 ccResolverWrapper的第一个参数就是 grpc.ClientConn。
Build()方法中的第三个参数,是一些 BuildOptions。
到这里我们已经知道了自定 Resolver 的 Build() 方法的调用逻辑,以及传入的参数的由来以及含义。
Last updated