一 服务发现基础概念
为什么需要服务发现
在微服务架构中,在生产环境中服务提供方都是以集群的方式对外提供服务,集群中服务的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方法。