6 服务注册

6.1 术语

  • 注册中心 registry
  • 服务 service
  • 服务元数据 service metadata
  • 服务提供方 provider
  • 服务消费方 consumer
  • 名字 name,标示应用唯一性的名字
  • 地址 ip
  • 端口 port

6.2 背景

当我们的服务规模数量比较小的时候,我们通常都会在应用中使用配置文件去调用服务。例如某个consumer需要通过provider获取用户信息。我们可以将这个行为拆解为三个步骤

  • 人工将provider的ip、port信息填入到consumer的配置
  • consumer根据配置里的provider name,获取其ip port
  • 调用provider的ip port的用户方法,拿到用户数据。 img_3.png

但是当集群规模达到一定量级之后,如果仍然采用这种配置方式去寻找服务,会让研发陷入到无尽的配置中,并且这种人工配置是不可靠的,会因为配置错误,导致服务不可访问,影响线上服务的稳定性。

为了减少这种繁琐的人工配置,因此我们需要Consumer能够动态的感知到Provider的地址。如果Provider在启动的时候将自己的名字、ip、port写入到中间件,那么Consumer就可以通过该中间件获取到Provider信息。这种能够提供注册服务信息、获取服务信息的中间件,我们称之为注册中心。缺图。

目前服务注册业界有多种方式,如下所示:

  • Provider通过框架的sdk,将name、ip、port写入到注册中心,consumer访问provider,通过name,获取ip port,例如ego
  • 通过边车模式,拿到Provider的name、ip、port写入到注册中心,consumer访问provider,通过name,获取ip port,例如istio
  • 通过边车模式,将name、ip写入到注册中心,dns关联上ip,consumer访问provider,通过name,获取ip。例如k8s的service

6.3 服务注册原理

在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图。

  • RPC Server 提供服务,在启动时,根据服务的编译和配置信息,向 Registry 注册服务,并向 Registry 定期发送心跳汇报存活状态。
  • RPC Client 调用服务,在启动时,根据配置文件的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
  • 当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
  • RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。

img_4.png

6.4 服务注册概念

我们刚才讲到Consumer能够通过Provider的name获取他的ip port信息,这些信息确定一个服务的基本情况是远远不够的。我们通常会在注册信息写入以下信息:

名称英文示例类型
环境envdev环境变量
地区regionbeijing环境变量
可用区zonezone1环境变量
地址ip192.168.1.1环境变量
端口port8080环境变量
协议schemegRPC配置变量
权重weight100配置变量
部署组deploymentred配置变量
框架版本号frameVersion2.0编译变量
应用版本号appVersionahkfasgasdf编译变量
编译时间buildTime2020-10-14 10:00:00编译变量
启动时间startTime2020-10-14 11:00:00编译变量

通过这些信息,我们能够很方便的获得Provider的基本情况,并对他进行改变。例如可以调节流量比例、调度流量区域、灰度版本、排查应用的版本、编译时间、启动时间。

6.5 服务注册最小实现

本节我们主要以beego2的实现方式讲解微服务注册。

  • 服务信息
    • key: name
    • value: ip port
  • 服务注册
    • Grant 创建一个租期
    • Put 根据name、写入value
    • Keepalive 与注册中心保持连接
  • 服务注销
    • Delete 删除name

6.5.1 注册服务

我们先来看下服务注册的代码,如下所示

  1. func (er *Registry) Register(ctx context.Context, serverName string, addr string, opts ...RegistryOptions) (err error) {
  2. var upBytes []byte
  3. info := resolver.Address{
  4. Addr: addr,
  5. ServerName: serverName,
  6. Attributes: nil,
  7. }
  8. // 服务信息
  9. if upBytes, err = json.Marshal(info); err != nil {
  10. return status.Error(codes.InvalidArgument, err.Error())
  11. }
  12. // 设置超时
  13. ctx, cancel := context.WithTimeout(context.TODO(), resolverTimeOut)
  14. er.cancel = cancel
  15. // 设置选项
  16. rgOpt := RegistryOption{TTL: DefaultRegInfTTL}
  17. for _, opt := range opts {
  18. opt(&rgOpt)
  19. }
  20. // 名字
  21. key := "/" + serverName + "/" + addr
  22. // 创建租期
  23. lsRsp, err := er.lsCli.Grant(ctx, int64(rgOpt.TTL/time.Second))
  24. if err != nil {
  25. return err
  26. }
  27. etcdOpts := []clientv3.OpOption{clientv3.WithLease(lsRsp.ID)}
  28. // 写入数据
  29. _, err = er.cli.KV.Put(ctx, key, string(upBytes), etcdOpts...)
  30. if err != nil {
  31. return err
  32. }
  33. // 设置keepalive
  34. lsRspChan, err := er.lsCli.KeepAlive(context.TODO(), lsRsp.ID)
  35. if err != nil {
  36. return err
  37. }
  38. go func() {
  39. for {
  40. _, ok := <-lsRspChan
  41. if !ok {
  42. grpclog.Fatalf("%v keepalive channel is closing", key)
  43. break
  44. }
  45. }
  46. }()
  47. return nil
  48. }

在gRPC的 resolver.Address里定义里三个属性,当注册类型为 resolver.Backend的时候。其解释如下所示:

  • Address 服务地址,用于拨号的地址
  • ServerName 服务名称,用于标示某个业务的服务名称
  • Attributes 元数据信息,例如编译时间、启动时间、机房等信息

通过这三个属性,我们可以定义一个服务的基本情况。然后我们可以根据该服务的名字+ip+port作为唯一key,服务的三个属性json编码后作为value值。写入到注册中心,同时在启动一个goroutine,启动keepalive,定时写入租期。

我们熟悉了框架的代码后,我们再来编写下业务方的注册代码,如下所示

  1. func main() {
  2. log.SetHeader(`{"time":"${time_rfc3339}","level":"${level}","file":"${short_file}","line":"${line}"}`) // 设置日志格式
  3. log.SetLevel(log.DEBUG) // 全局日志级别
  4. log.Infof("server start, pid = %d", os.Getpid())
  5. cc, err := clientv3.New(clientv3.Config{
  6. Endpoints: []string{"127.0.0.1:2379"}, // etcd节点ip
  7. AutoSyncInterval: Duration("60s"), // 自动同步etcd的member节点
  8. DialTimeout: Duration("1s"), // 拨号超时时间
  9. })
  10. if err != nil {
  11. panic(err)
  12. }
  13. var servOpts []grpc.ServerOption
  14. app := mygrpc.NewApp(
  15. mygrpc.WithAddress("127.0.0.1:4000"), // 设置服务Address
  16. mygrpc.WithRegistry(mygrpc.NewRegisty(cc)), // 设置服务注册中心
  17. mygrpc.WithServerName("micro"), // 设置服务名称
  18. mygrpc.WithGRPCServOption(servOpts), // 设置服务属性
  19. )
  20. app.Register(pb.RegisterGreeterServer, &Hello{})
  21. app.Start()
  22. log.Info("handle end")
  23. }
  24. func Duration(str string) time.Duration {
  25. dur, err := time.ParseDuration(str)
  26. if err != nil {
  27. panic(err)
  28. }
  29. return dur
  30. }
  31. type Hello struct{}
  32. // SayHello implements helloworld.GreeterServer
  33. func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
  34. log.Infof("receive req : %v", *in)
  35. return &pb.HelloReply{Message: "Hello " + in.Name}, nil
  36. }

在业务注册代码里,我们首先要创建一个注册中心的实例,我们需要设置三个etcd最基本的属性,节点ip、同步etcd的member list的时间和拨号超时时间。然后我们将注册中心的实例、服务Address、服务名称、服务属性 设置到gRPC的服务注册里。启动服务后,可以看到以下启动信息。 img_5.png 我们在访问下etcd里的信息。可以看到key是 /micro/127.0.0.1:4000 ,value是我们刚才写入的三个属性信息。

  1. /micro/127.0.0.1:4000
  2. {"Addr":"127.0.0.1:4000","ServerName":"micro","Attributes":null,"Type":0,"Metadata":null}