微服務之服務註冊和服務發現篇

seth-shi發表於2022-06-17
有了服務註冊和發現機制,消費者不需要知道具體服務提供者的真實實體地址就可以進行呼叫,也無須知道具體有多少個服務者可用;而服務提供者只需要註冊到註冊中心,就可以對外提供服務,在對外服務時不需要知道具體是哪些服務呼叫了自己。

RPC 配置

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

被調方-服務註冊

  • mall/user/rpc/user.go 原始碼如下
package main

import (
    "flag"
    "fmt"

    "go-zero-demo-rpc/mall/user/rpc/internal/config"
    "go-zero-demo-rpc/mall/user/rpc/internal/server"
    "go-zero-demo-rpc/mall/user/rpc/internal/svc"
    "go-zero-demo-rpc/mall/user/rpc/types/user"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/core/service"
    "github.com/zeromicro/go-zero/zrpc"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

var configFile = flag.String("f", "etc/user.yaml", "the config file")

func main() {
    flag.Parse()
    
    var c config.Config
    conf.MustLoad(*configFile, &c)
    ctx := svc.NewServiceContext(c)
    svr := server.NewUserServer(ctx)
    
    s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
        user.RegisterUserServer(grpcServer, svr)
    
        if c.Mode == service.DevMode || c.Mode == service.TestMode {
            reflection.Register(grpcServer)
        }
    })
    defer s.Stop()
    
    fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
    s.Start()
}
  • MustNewServer內部實現呼叫了NewServer方法, 這裡我們關注NewServer通過internal.NewRpcPubServer方法例項化了internal.Server
if c.HasEtcd() {
    server, err = internal.NewRpcPubServer(c.Etcd, c.ListenOn, serverOptions...)
    if err != nil {
        return nil, err
    }
}
  • internal.NewRpcPubServer中的registerEtcd會呼叫Publisher.KeepAlive方法
// KeepAlive keeps key:value alive.
func (p *Publisher) KeepAlive() error {
    // 這裡獲取 etcd 的連線
    cli, err := internal.GetRegistry().GetConn(p.endpoints)
    if err != nil {
        return err
    }
    
    p.lease, err = p.register(cli)
    if err != nil {
        return err
    }
    
    proc.AddWrapUpListener(func() {
        p.Stop()
    })
    
    return p.keepAliveAsync(cli)
}
  • p.register這裡把自己註冊到服務中
func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, error) {

    // 這裡新建一個租約
    resp, err := client.Grant(client.Ctx(), TimeToLive)
    if err != nil {
        return clientv3.NoLease, err
    }
    
    // 得到租約的 ID
    lease := resp.ID
    
    // 這裡拼接出實際儲存的 key
    if p.id > 0 {
        p.fullKey = makeEtcdKey(p.key, p.id)
    } else {
        p.fullKey = makeEtcdKey(p.key, int64(lease))
    }

    // p.value 是前面的 figureOutListenOn 方法獲取到自己的地址
    _, err = client.Put(client.Ctx(), p.fullKey, p.value, clientv3.WithLease(lease))

    return lease, err
}
  • 註冊完之後, keepAliveAsync開了一個協程保活這個服務
  • 當這個服務意外當機時, 就不會再向etcd保活, etcd就會刪除這個key
  • 註冊好的服務如圖

呼叫方-服務發現

  • order/api/order.go 原始碼如下
package main

import (
    "flag"
    "fmt"

    "go-zero-demo-rpc/order/api/internal/config"
    "go-zero-demo-rpc/order/api/internal/handler"
    "go-zero-demo-rpc/order/api/internal/svc"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/rest"
)

var configFile = flag.String("f", "etc/order.yaml", "the config file")

func main() {
    flag.Parse()

    var c config.Config
    conf.MustLoad(*configFile, &c)

    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    ctx := svc.NewServiceContext(c)
    handler.RegisterHandlers(server, ctx)

    fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
    server.Start()
}
  • svc.NewServiceContext方法內部又呼叫了zrpc.MustNewClient, zrpc.MustNewClient主要實現在zrpc.NewClient

    func NewServiceContext(c config.Config) *ServiceContext {
      return &ServiceContext{
          Config:  c,
          UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)),
      }
    }
  • 最後實際呼叫了internal.NewClient去例項化rpc client
func NewClient(c RpcClientConf, options ...ClientOption) (Client, error) {
    var opts []ClientOption
    if c.HasCredential() {
        opts = append(opts, WithDialOption(grpc.WithPerRPCCredentials(&auth.Credential{
            App:   c.App,
            Token: c.Token,
        })))
    }
    if c.NonBlock {
        opts = append(opts, WithNonBlock())
    }
    if c.Timeout > 0 {
        opts = append(opts, WithTimeout(time.Duration(c.Timeout)*time.Millisecond))
    }

    opts = append(opts, options...)

    target, err := c.BuildTarget()
    if err != nil {
        return nil, err
    }

    client, err := internal.NewClient(target, opts...)
    if err != nil {
        return nil, err
    }

    return &RpcClient{
        client: client,
    }, nil
}
  • zrpc/internal/client.go檔案裡, 包含一個init方法, 這裡就是實際發現服務的地方, 在這裡註冊服務發現者
func init() {
    resolver.Register()
}
  • resolver.Register方法實現
package resolver

import (
    "github.com/zeromicro/go-zero/zrpc/resolver/internal"
)

// Register registers schemes defined zrpc.
// Keep it in a separated package to let third party register manually.
func Register() {
    internal.RegisterResolver()
}
  • 最後又回到interval包的internal.RegisterResolver方法, 這裡我們關注etcdResolverBuilder
func RegisterResolver() {
    resolver.Register(&directResolverBuilder)
    resolver.Register(&discovResolverBuilder)
    resolver.Register(&etcdResolverBuilder)
    resolver.Register(&k8sResolverBuilder)
}
  • etcdBuilder的內嵌了discovBuilder結構體,

    • Build方法呼叫過程:
    • 例項化服務端: internal.NewClient->client.dial->grpc.DialContext
    • 由於etcdresolver.BuildDiscovTarget生成的taget所以是類似這樣子的: discov://127.0.0.1:2379/user.rpc
    • 解析服務發現:ClientConn.parseTargetAndFindResolver->grpc.parseTarget->ClientConn.getResolver
    • 然後在grpc.newCCResolverWrapper呼叫resolver.Builder.Build方法去發現服務
  • 我們著重關注discovBuilder.Build方法
type etcdBuilder struct {
    discovBuilder
}


type discovBuilder struct{}

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
}

func (b *discovBuilder) Scheme() string {
    return DiscovScheme
}
  • discov.NewSubscriber方法呼叫internal.GetRegistry().Monitor最後呼叫Registry.monitor方法進行監視

    • cluster.getClient拿到etcd連線
    • cluster.load作為第一次載入資料
    • cluster.watchwatch監聽etcd字首key的改動
func (c *cluster) monitor(key string, l UpdateListener) error {
    c.lock.Lock()
    c.listeners[key] = append(c.listeners[key], l)
    c.lock.Unlock()

    cli, err := c.getClient()
    if err != nil {
        return err
    }

    c.load(cli, key)
    c.watchGroup.Run(func() {
        c.watch(cli, key)
    })

    return nil
}
  • 如下圖是cluster.load的實現, 就是根據字首拿到user.prc服務註冊的所有地址

Q

  • 為什麼不用Redis做註冊中心(反正只是把被調方的地址儲存, 過期 Redis也能勝任), 找了很久找到這個說法

    簡單從以下幾個方面說一下瑞迪斯為啥在微服務中不能取代 etcd:

    1、redis 沒有版本的概念,歷史版本資料在大規模微服務中非常有必要,對於狀態回滾和故障排查,甚至定鍋都很重要

    2、redis 的註冊和發現目前只能通過 pub 和 sub 來實現,這兩個命令完全不能滿足生產環境的要求,具體原因可以 gg 或看原始碼實現

    3、etcd 在 2.+版本時,watch 到資料官方文件均建議再 get 一次,因為會存在資料延遲,3.+版本不再需要,可想 redis 的 pub 和 sub 能否達到此種低延遲的要求

    4、樓主看到的微服務架構應該都是將 etcd 直接暴露給 client 和 server 的,etcd 的效能擺在那,能夠承受多少的 c/s 直連呢,更好的做法應該是對 etcd 做一層保護,當然這種做法會損失一些功能

    5、redis 和 etcd 的叢集實現方案是不一致的,etcd 採用的是 raft 協議,一主多從,只能寫主,底層採用 boltdb 作為 k/v 儲存,直接落盤

    6、redis 的持久化方案有 aof 和 rdb,這兩種方案在當機的時候都或多或少的會丟失資料

  • 引用自 https://www.v2ex.com/t/520367

原文連結 https://www.shiguopeng.cn/posts/2022061518/

相關文章