乐趣区

关于go:服务发现原理分析与源码解读

在微服务架构中,有许多绕不开的技术话题。比方服务发现、负载平衡、指标监控、链路追踪,以及服务治理相干的超时管制、熔断、降级、限流等,还有 RPC 框架。这些都是微服务架构的根底,只有打牢这些根底,才敢说对微服务是有了一点了解,出门也好意思和他人打招呼了,被人发问的时候也能娓娓而谈了,线上出了问题往往也能寻根溯源心田不慌了,旁边的女同事小芳看着你的时候也是满眼的小可爱了。

在《微服务实际》公众号,之前写了《go-zero 微服务实战系列》的系列文章,这个系列的文章更多的是偏差业务性能和高并发下的服务优化等。自己程度无限,不免有写的有余的中央,但也仍然失去了大家的反对与激励,倍感荣幸,所以决定趁热打铁,乘胜追击,持续给大家输入干货。

《彻底搞懂系列》会基于 go-zero v1.3.5 和 grpc-go v1.47.0 和大家一起学习微服务架构的方方面面,次要模式是实践 + 源码 + 案例,如果工夫容许也可能会加上配套视频。

本篇文章作为该系列的第一篇,会先介绍绝对比较简单的服务发现相干内容。

撸袖子开搞,奥利给!!!

服务发现

为什么在微服务架构中,须要引入服务发现呢?实质上,服务发现的目标是解耦程序对服务具体位置的依赖,对于微服务架构来说,服务发现不是可选的,而是必须的。因为在生产环境中服务提供方都是以集群的形式对外提供服务,集群中服务的 IP 随时都可能发生变化,比方服务重启,公布,扩缩容等,因而咱们须要用一本“通讯录”及时获取到对应的服务节点,这个获取的过程其实就是“服务发现”。

要了解服务发现,须要晓得服务发现解决了如下三个问题:

  • 服务的注册(Service Registration)

    当服务启动的时候,应该通过某种模式(比方调用 API、产生上线事件音讯、在 Etcd 中记录、存数据库等等)把本人(服务)的信息告诉给服务注册核心,这个过程个别是由微服务框架来实现,业务代码无感知。

  • 服务的保护(Service Maintaining)

    只管在微服务框架中通常都提供下线机制,但并没有方法保障每次服务都能优雅下线(Graceful Shutdown),而不是因为宕机、断网等起因忽然失联,所以,在微服务框架中就必须要尽可能的保障保护的服务列表的正确性,以防止拜访不可用服务节点的难堪。

  • 服务的发现(Service Discovery)

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

服务发现有两种模式,别离是服务端服务发现和客户端服务发现,上面别离进行介绍。

服务端服务发现

对于服务端服务发现来说,服务调用方无需关注服务发现的具体细节,只须要晓得服务的 DNS 域名即可,反对不同语言的接入,对基础设施来说,须要专门反对负载均衡器,对于申请链路来说多了一次网络跳转,可能会有性能损耗。也能够把咱们比拟相熟的 nginx 反向代理了解为服务端服务发现。

客户端服务发现

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

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

gRPC 的服务发现

gRPC 提供了自定义 Resolver 的能力来实现服务发现,通过 Register办法来进行注册自定义的 Resolver,自定义的 Resolver 须要实现 Builder 接口,定义如下:

grpc-go/resolver/resolver.go:261

type Builder interface {Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
    Scheme() string}

先说下 Scheme() 办法的作用,该办法返回一个 stirng。注册的 Resolver 会被保留在一个全局的变量 m 中,m 是一个 map,这个 map 的 key 即为 Scheme() 办法返回的字符串。也就是多个 Resolver 是通过 Scheme 来进行辨别的,所以咱们定义 Resolver 的时候 Scheme 不要反复,否则 Resolver 就会被笼罩。

grpc-go/resolver/resolver.go:49

func Register(b Builder) {m[b.Scheme()] = b
}

再来看下 Build 办法,Build 办法有三个参数,还有 Resolver 返回值,乍一看不晓得这些参数是干嘛的,遇到这种状况该怎么办呢?其实也很简略,去源码里看一下 Build 办法在哪里被调用的,就晓得传入的参数是哪里来的,是什么含意了。

应用 gRPC 进行服务调用前,须要先创立一个 ClientConn 对象,最终发动调用的时候,其实是调用了 ClientConnInvoke 办法,能够看下如下代码,其中 ClientConn 是通过调用 NewGreeterClient 传入的,NewGreeterClientprotoc 主动生成的代码,并赋值给 cc 属性,示例代码中创立 ClientConn 调用的是 Dial 办法,底层也会调用 DialContext

grpc-go/clientconn.go:104

func Dial(target string, opts ...DialOption) (*ClientConn, error) {return DialContext(context.Background(), target, opts...)
}

创立 ClientConn 对象,并传递给主动生成的 greeterClient

grpc-go/examples/helloworld/greeter_client/main.go:42

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())
}

最终通过 Invoke 办法真正发动调用申请。

grpc-go/examples/helloworld/helloworld/helloworld_grpc.pb.go:39

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

在理解了客户端调用发动的流程后,咱们重点看下 ClientConn 办法,该办法巨长,只看咱们关注的 Resolver 局部。ClientConn 第二个参数 Target 的语法能够参考 https://github.com/grpc/grpc/…,采纳了 URI 的格局,其中第一局部示意 Resolver 的名称,即自定义 Builder 办法 Scheme 的返回值。格局如下:

dns:[//authority/]host[:port] -- DNS(默认)

持续往下看,通过调用 parseTargetAndFindResolver 办法来获取 Resolver

grpc-go/clientconn.go:251

resolverBuilder, err := cc.parseTargetAndFindResolver()

parseTargetAndFindResolver 办法中,次要就是把 target 中的 resolver name 解析进去,而后依据 resolver name 去下面咱们提到的保留 Resolver 的全局变量 m 中去找对应的 Resolver。

grpc-go/clientconn.go:1574

func (cc *ClientConn) parseTargetAndFindResolver() (resolver.Builder, error) {
    // 非关键代码省略 ...
  
    var rb resolver.Builder
    parsedTarget, err := parseTarget(cc.target)
  
    // 非关键代码省略 ...
  
    rb = cc.getResolver(parsedTarget.Scheme)
    if rb == nil {return nil, fmt.Errorf("could not get resolver for default scheme: %q", parsedTarget.Scheme)
    }
    cc.parsedTarget = parsedTarget
    return rb, nil
}

接着往下看,找到咱们本人注册的 Resolver 之后,又调用了 newCCResolverWrapper 办法,把咱们本人的 Resolver 也传了进去

grpc-go/clientconn.go:292

rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)

进入到 newCCResolverWrapper 办法中,在这个办法中终于找到了咱们自定义的 BuilderBuild 办法在哪里被调用了,在 grpc-go/resolver_conn_wrapper.go:72 调用了咱们自定义的 Build 办法,其中第一参数 target 传入的为 cc.parseTarget,cc 为 newCCResolverWrapper 第一个参数,即 ClientConn 对象。cc.parseTarget 是在下面提到的获取自定义 Resolver 办法 parseTargetAndFindResolver 中最初赋值的,其中 Scheme、Authority、Endpoint 别离对应 Target 语法中定义的三局部,这几个属性行将被废除,只保留 URL 属性,定义如下:

grpc-go/resolver/resolver.go:245

type Target struct {
    // Deprecated: use URL.Scheme instead.
    Scheme string
    // Deprecated: use URL.Host instead.
    Authority string
    // Deprecated: use URL.Path or URL.Opaque instead. The latter is set when
    // the former is empty.
    Endpoint string
    // URL contains the parsed dial target with an optional default scheme added
    // to it if the original dial target contained no scheme or contained an
    // unregistered scheme. Any query params specified in the original dial
    // target can be accessed from here.
    URL url.URL
}

URL 的 Scheme 对应 Target 的 Scheme,URL 的 Host 对应 Target 的 Authority,URL 的 Path 对应 Target 的 Endpoint

/usr/local/go/src/net/url/url.go:358

type URL struct {
    Scheme      string
    Opaque      string    // encoded opaque data
    User        *Userinfo // username and password information
    Host        string    // host or host:port
    Path        string    // path (relative paths may omit leading slash)
    RawPath     string    // encoded path hint (see EscapedPath method)
    ForceQuery  bool      // append a query ('?') even if RawQuery is empty
    RawQuery    string    // encoded query values, without '?'
    Fragment    string    // fragment for references, without '#'
    RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

持续看传入自定义 Build 办法的第二个参数 cc,这个 cc 参数是一个接口 ClientConn,不要和咱们之前讲的创立客户端调用用的 ClientConn 混同,这个 ClientConn定义如下:

grpc-go/resolver/resolver.go:203

type ClientConn interface {UpdateState(State) error
    ReportError(error)
    NewAddress(addresses []Address)
    NewServiceConfig(serviceConfig string)
    ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult
}

ccResolverWrapper 实现了这个接口,并作为自定义 Build 办法的第二个参数传入

grpc-go/resolver_conn_wrapper.go:36

type ccResolverWrapper struct {
    cc         *ClientConn
    resolverMu sync.Mutex
    resolver   resolver.Resolver
    done       *grpcsync.Event
    curState   resolver.State

    incomingMu sync.Mutex // Synchronizes all the incoming calls.
}

自定义 Build 办法的第三个参数为一些配置项,newCCResolverWrapper 实现如下:

grpc-go/resolver_conn_wrapper.go:48

func newCCResolverWrapper(cc *ClientConn, rb resolver.Builder) (*ccResolverWrapper, error) {
    ccr := &ccResolverWrapper{
        cc:   cc,
        done: grpcsync.NewEvent(),}

    var credsClone credentials.TransportCredentials
    if creds := cc.dopts.copts.TransportCredentials; creds != nil {credsClone = creds.Clone()
    }
    rbo := resolver.BuildOptions{
        DisableServiceConfig: cc.dopts.disableServiceConfig,
        DialCreds:            credsClone,
        CredsBundle:          cc.dopts.copts.CredsBundle,
        Dialer:               cc.dopts.copts.Dialer,
    }

    var err error
    ccr.resolverMu.Lock()
    defer ccr.resolverMu.Unlock()
    ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, rbo)
    if err != nil {return nil, err}
    return ccr, nil
}

好了,到这里咱们曾经晓得了自定 Resolver 的 Build 办法在哪里被调用,以及传入的参数的由来以及含意,如果你是第一次看 gRPC 源码的话可能当初曾经有点懵了,能够多读几遍,为大家提供了时序图配合代码浏览成果更佳:

go-zero 中如何实现的服务发现

通过对 gRPC 服务发现相干内容的学习,咱们大略曾经晓得了服务发现是怎么回事了,有了实践,接下来咱们就一起看下 go-zero 是如何基于 gRPC 做服务发现的。

通过下面的时序图能够看到第一步是须要自定义 Resolver,第二步注册自定义的 Resolver。

go-zero 的服务发现是在客户端实现的。在创立 zRPC 客户端的时候,通过 init 办法进行了自定义 Resolver 的注册。

go-zero/zrpc/internal/client.go:23

func init() {resolver.Register()
}

在 go-zero 中默认注册了四个自定义的 Resolver。

go-zero/zrpc/resolver/internal/resolver.go:35

func RegisterResolver() {resolver.Register(&directResolverBuilder)
    resolver.Register(&discovResolverBuilder)
    resolver.Register(&etcdResolverBuilder)
    resolver.Register(&k8sResolverBuilder)
}

通过 goctl 主动生成的 rpc 代码默认应用的是 etcd 作为服务注册与发现组件的,因而咱们重点来看下 go-zero 是如何基于 etcd 实现服务注册与发现的。

etcdBuilder 返回的 Scheme 值为 etcd

go-zero/zrpc/resolver/internal/etcdbuilder.go:7

func (b *etcdBuilder) Scheme() string {return EtcdScheme}

go-zero/zrpc/resolver/internal/resolver.go:15

EtcdScheme = "etcd"

还记得咱们下面讲过的吗?在时序图的第五步和第六步,会通过 scheme 去全局的 m 中寻找自定义的 Resolver,而 scheme 是从 DialContext 第二个参数 target 中解析进去的,那咱们看下 go-zero 调用 DialContext 的时候,传入的 target 值是什么。target 是通过 BuildTarget 办法获取来的,定义如下:

go-zero/zrpc/config.go:72

func (cc RpcClientConf) BuildTarget() (string, error) {if len(cc.Endpoints) > 0 {return resolver.BuildDirectTarget(cc.Endpoints), nil
    } else if len(cc.Target) > 0 {return cc.Target, nil}

    if err := cc.Etcd.Validate(); err != nil {return "", err}

    if cc.Etcd.HasAccount() {discov.RegisterAccount(cc.Etcd.Hosts, cc.Etcd.User, cc.Etcd.Pass)
    }
    if cc.Etcd.HasTLS() {
        if err := discov.RegisterTLS(cc.Etcd.Hosts, cc.Etcd.CertFile, cc.Etcd.CertKeyFile,
            cc.Etcd.CACertFile, cc.Etcd.InsecureSkipVerify); err != nil {return "", err}
    }

    return resolver.BuildDiscovTarget(cc.Etcd.Hosts, cc.Etcd.Key), nil
}

最终生成 target 后果的办法如下,也就是对于 etcd 来说,最终生成的 target 格局为:

etcd://127.0.0.1:2379/product.rpc

go-zero/zrpc/resolver/target.go:17

func BuildDiscovTarget(endpoints []string, key string) string {
    return fmt.Sprintf("%s://%s/%s", internal.DiscovScheme,
        strings.Join(endpoints, internal.EndpointSep), key)
}

仿佛有点不对劲,scheme 不应该是 etcd 么?为什么是 discov?其实是因为 etcd 和 discov 共用了一套 Resolver 逻辑,也就是 gRPC 通过 scheme 找到曾经注册的 discov Resolver,该 Resolver 对应的 Build 办法同样实用于 etcd,discov 能够认为是对服务发现的一个形象,etcdResolver 的定义如下:

go-zero/zrpc/resolver/internal/etcdbuilder.go:3

type etcdBuilder struct {discovBuilder}

服务注册

在具体看基于 etcd 的自定义 Resolver 逻辑之前,咱们先来看下 go-zero 的服务注册,即如何把服务信息注册到 etcd 中的,咱们以 lebron/apps/product/rpc 这个服务为例进行阐明。

在 product-rpc 的配置文件中配置了 Etcd,包含 etcd 的地址和服务对应的 key,如下:

lebron/apps/product/rpc/etc/product.yaml:4

ListenOn: 127.0.0.1:9002

Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: product.rpc

调用 zrpc.MustNewServer 创立 gRPC server,接着会调用 NewRpcPubServer 办法,定义如下:

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

func NewRpcPubServer(etcd discov.EtcdConf, listenOn string, opts ...ServerOption) (Server, error) {registerEtcd := func() error {pubListenOn := figureOutListenOn(listenOn)
        var pubOpts []discov.PubOption
        if etcd.HasAccount() {pubOpts = append(pubOpts, discov.WithPubEtcdAccount(etcd.User, etcd.Pass))
        }
        if etcd.HasTLS() {
            pubOpts = append(pubOpts, discov.WithPubEtcdTLS(etcd.CertFile, etcd.CertKeyFile,
                etcd.CACertFile, etcd.InsecureSkipVerify))
        }
        pubClient := discov.NewPublisher(etcd.Hosts, etcd.Key, pubListenOn, pubOpts...)
        return pubClient.KeepAlive()}
    server := keepAliveServer{
        registerEtcd: registerEtcd,
        Server:       NewRpcServer(listenOn, opts...),
    }

    return server, nil
}

在启动 Server 的时候,调用 Start 办法,在 Start 办法中会调用 registerEtcd 进行真正的服务注册

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

func (s keepAliveServer) Start(fn RegisterFn) error {if err := s.registerEtcd(); err != nil {return err}

    return s.Server.Start(fn)
}

在 KeepAlive 办法中,首先创立 etcd 连贯,而后调用 register 办法进行服务注册,在 register 首先创立租约,租约默认工夫为 10 秒钟,最初通过 Put 办法进行注册。

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

func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, error) {resp, err := client.Grant(client.Ctx(), TimeToLive)
    if err != nil {return clientv3.NoLease, err}

    lease := resp.ID
    if p.id > 0 {p.fullKey = makeEtcdKey(p.key, p.id)
    } else {p.fullKey = makeEtcdKey(p.key, int64(lease))
    }
    _, err = client.Put(client.Ctx(), p.fullKey, p.value, clientv3.WithLease(lease))

    return lease, err
}

key 的规定定义如下,其中 key 为在配置文件中配置的 Key,这里为 product.rpc,id 为租约 id。value 为服务的地址。

go-zero/core/discov/clients.go:39

func makeEtcdKey(key string, id int64) string {return fmt.Sprintf("%s%c%d", key, internal.Delimiter, id)
}

在理解了服务注册的流程后,咱们启动 product-rpc 服务,而后通过如下命令查看服务注册的地址:

$ etcdctl get product.rpc --prefix
product.rpc/7587864068988009477
127.0.0.1:9002

KeepAlive 办法中,服务注册完后,最初会调用 keepAliveAsync 进行租约的续期,以保障服务始终是存活的状态,如果服务异样退出了,那么也就无奈进行续期,服务发现也就能自动识别到该服务异样下线了。

服务发现

当初曾经把服务注册到 etcd 中了,持续来看如何发现这些服务地址。咱们回到 etcdBuilder 的 Build 办法的实现。

还记得第一个参数 target 是什么吗?如果不记得了能够往上翻再温习一下,首先从 target 中解析出 etcd 的地址,和服务对应的 key。而后创立 etcd 连贯,接着执行 update 办法,在 update 办法中,通过调用 cc.UpdateState 办法进行服务状态的更新。

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

func (b *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {hosts := strings.FieldsFunc(targets.GetAuthority(target), func(r rune) bool {return r == EndpointSepChar})
    sub, err := discov.NewSubscriber(hosts, targets.GetEndpoints(target))
    if err != nil {return nil, err}

    update := func() {var addrs []resolver.Address
        for _, val := range subset(sub.Values(), subsetSize) {
            addrs = append(addrs, resolver.Address{Addr: val,})
        }
        if err := cc.UpdateState(resolver.State{Addresses: addrs,}); err != nil {logx.Error(err)
        }
    }
    sub.AddListener(update)
    update()

    return &nopResolver{cc: cc}, nil
}

如果遗记了 Build 办法第二个参数 cc 的话,能够往上翻翻再温习一下,cc.UpdateState 办法定义如下,最终会调用 ClientConnupdateResolverState 办法:

grpc-go/resolver_conn_wrapper.go:94

func (ccr *ccResolverWrapper) UpdateState(s resolver.State) error {ccr.incomingMu.Lock()
    defer ccr.incomingMu.Unlock()
    if ccr.done.HasFired() {return nil}
    ccr.addChannelzTraceEvent(s)
    ccr.curState = s
    if err := ccr.cc.updateResolverState(ccr.curState, nil); err == balancer.ErrBadResolverState {return balancer.ErrBadResolverState}
    return nil
}

持续看 Build 办法,update 办法会被增加到事件监听中,当有 PUT 和 DELETE 事件触发,都会调用 update 办法进行服务状态的更新,事件监听是通过 etcd 的 Watch 机制实现,代码如下:

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

func (c *cluster) watchStream(cli EtcdClient, key string) bool {rch := cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
    for {
        select {
        case wresp, ok := <-rch:
            if !ok {logx.Error("etcd monitor chan has been closed")
                return false
            }
            if wresp.Canceled {logx.Errorf("etcd monitor chan has been canceled, error: %v", wresp.Err())
                return false
            }
            if wresp.Err() != nil {logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err()))
                return false
            }

            c.handleWatchEvents(key, wresp.Events)
        case <-c.done:
            return true
        }
    }
}

当有事件触发的时候,会调用事件处理函数 handleWatchEvents,最终会调用 Build 办法中定义的 update 进行服务状态的更新:

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

func (c *cluster) handleWhandleWatchEventsatchEvents(key string, events []*clientv3.Event) {c.lock.Lock()
    listeners := append([]UpdateListener(nil), c.listeners[key]...)
    c.lock.Unlock()

    for _, ev := range events {
        switch ev.Type {
        case clientv3.EventTypePut:
            c.lock.Lock()
            if vals, ok := c.values[key]; ok {vals[string(ev.Kv.Key)] = string(ev.Kv.Value)
            } else {c.values[key] = map[string]string{string(ev.Kv.Key): string(ev.Kv.Value)}
            }
            c.lock.Unlock()
            for _, l := range listeners {
                l.OnAdd(KV{Key: string(ev.Kv.Key),
                    Val: string(ev.Kv.Value),
                })
            }
        case clientv3.EventTypeDelete:
            c.lock.Lock()
            if vals, ok := c.values[key]; ok {delete(vals, string(ev.Kv.Key))
            }
            c.lock.Unlock()
            for _, l := range listeners {
                l.OnDelete(KV{Key: string(ev.Kv.Key),
                    Val: string(ev.Kv.Value),
                })
            }
        default:
            logx.Errorf("Unknown event type: %v", ev.Type)
        }
    }
}

第一次会调用 load 办法,获取 key 对应的服务列表,通过 etcd 前缀匹配的形式获取,获取形式如下:

func (c *cluster) load(cli EtcdClient, key string) {
    var resp *clientv3.GetResponse
    for {
        var err error
        ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout)
        resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix())
        cancel()
        if err == nil {break}

        logx.Error(err)
        time.Sleep(coolDownInterval)
    }

    var kvs []KV
    for _, ev := range resp.Kvs {
        kvs = append(kvs, KV{Key: string(ev.Key),
            Val: string(ev.Value),
        })
    }

    c.handleChanges(key, kvs)
}

获取的服务地址列表,通过 map 存储在本地,当有事件触发的时候通过操作 map 进行服务列表的更新,这里有个暗藏的设计思考是当 etcd 连不上或者呈现故障时,内存里的服务地址列表不会被更新,保障了当 etcd 有问题时,服务发现仍然能够工作,保障服务持续失常运行。逻辑绝对比拟直观,这里就不再赘述,代码逻辑在 go-zero/core/discov/subscriber.go:76,上面是 go-zero 服务发现的时序图

结束语

到这里服务发现相干的内容曾经讲完了,内容还是有点多的,特地是代码局部须要重复仔细阅读能力加深了解。

咱们一起来简略回顾下本篇的内容:

  • 首先介绍了服务发现的概念,以及服务发现须要解决哪些问题
  • 服务发现的两种模式,别离是服务端发现模式和客户端发现模式
  • 接着一起学习了 gRPC 提供的注册 Resolver 的能力,通过注册 Resolver 来实现自定义的服务发现性能,以及 gRPC 外部是如何寻找到自定义的 Resolver 和触发调用自定义 Resolver 的逻辑
  • 最初学习了 go-zero 中服务发现的实现原理,

    • 先是介绍了 go-zero 的服务注册流程,演示了最终注册的成果
    • 接着从自定义 Resolver 的 Build 办法登程,理解到先是通过前缀匹配的形式获取对应的服务列表存在本地,而后调用 UpdateState 办法更新服务状态
    • 通过 Watch 的形式监听服务状态的变动,监听到变动后会触发调用 update 办法更新本地的服务列表和调用 UpdateState 更新服务的状态。

服务发现是了解微服务架构的根底,心愿大家能认真的浏览本文,如果有疑难能够随时找我探讨,在社区群中能够搜寻 dawn_zhou 找到我。

通过服务发现获取到服务列表后,接着就会通过 Invoke 办法进行服务调用,在服务调用的时候就波及到负载平衡,通过负载平衡抉择一个适合的节点发动申请。负载平衡是下一篇文章要讲的内容,敬请期待。

心愿本篇文章对你有所帮忙,你的点赞是作者继续输入的最大能源。

我的项目地址

https://github.com/zeromicro/go-zero

欢送应用 go-zerostar 反对咱们!

微信交换群

关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。

退出移动版