Kitex 源码解析 —— 将服务注册进入注册中心的细节

1,718 阅读7分钟

1. 前言

大家好,我是 skyenought, 这是我关于 Kitex 的第三篇文章,之前的文章主要分析了 Kitex tool 代码生成工具的源码。

这次我开始向 Kitex Core 的部分进军了!由于本人水平有限,所以分析可能十分粗糙,甚至让大家必须打开 Kitex 的源码才能完全看明白,但这也是我写这篇文章的想法。不要光看,还需要亲自去分析,去思考,这样才能学到真正的知识!

如若觉得写的还写,望大家可以给我点个赞,谢谢!

1. 什么是 CloudWeGo-Kitex

Kitex[kaɪt’eks] 为字节跳动内部的 Golang 微服务 RPC 框架,具有高性能强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。

2. 正文

这次我们可以从官方示例 中的 easy_note 这个demo 开始分析,因为它基本展示了 Kitex 的基本使用方法。

下面图例为官方在 demo 中展示的架构图,通过简单的分析可得, note, user 通过注册中心 (Etcd) 进行注册 , api 通过 注册中心 来发现 note, user 两个 rpc 服务, 并进行业务处理。

                                     http
                            ┌────────────────────────┐
  ┌─────────────────────────┤                        ├───────────────────────────────┐
  │                         │         demoapi        │                               │
  │      ┌──────────────────►                        │◄──────────────────────┐       │
  │      │                  └───────────▲────────────┘                       │       │
  │      │                              │                                    │       │
  │      │                              │                                    │       │
  │      │                              │                                    │       │
  │      │                           resolve                                 │       │
  │      │                              │                                    │       │
 req    resp                            │                                   resp    req
  │      │                              │                                    │       │
  │      │                              │                                    │       │
  │      │                              │                                    │       │
  │      │                   ┌──────────▼─────────┐                          │       │
  │      │                   │                    │                          │       │
  │      │       ┌───────────►       Etcd         ◄─────────────────┐        │       │
  │      │       │           │                    │                 │        │       │
  │      │       │           └────────────────────┘                 │        │       │
  │      │       │                                                  │        │       │
  │      │     register                                           register   │       │
  │      │       │                                                  │        │       │
  │      │       │                                                  │        │       │
  │      │       │                                                  │        │       │
  │      │       │                                                  │        │       │
 ┌▼──────┴───────┴───┐                                           ┌──┴────────┴───────▼─┐
 │                   │───────────────── req ────────────────────►│                     │
 │       demonote    │                                           │        demouser     │
 │                   │◄──────────────── resp ────────────────────│                     │
 └───────────────────┘                                           └─────────────────────┘
       thrift                                                           protobuf

2. 为什么要使用注册中心?

kitex-examples/hello 这个最简单示例分析,从 cloudwego/kitex 的快速上手可知,这里用了最简单的 直链 来链接 server 和 client。而这次的 easy_note 中使用了 注册中心来作为 服务之间的 桥梁 (middleware), 为什么不使用之前的方式而是使用了注册中心?

我们搜索一下注册中心的作用,可知: 服务注册中心的主要作用就是「服务的注册」和「服务的发现」

我们将服务交给注册中心管理,虽然可以避免处理复杂的手动管理,我们也许需要还要考虑更多问题,例如:

  • 服务注册后,如何被及时发现

  • 服务宕机后,如何及时下线

  • 服务如何有效的水平扩展

  • 服务发现时,如何进行路由

  • 服务异常时,如何进行降级

  • 注册中心如何实现自身的高可用

    所以再看看 官方的架构图:这只是其中的 一小部分 ! 可以说注册发现是kitex中必不可少的一环。

    以后希望我可以为大家继续分析,东西很多,这次先说 register

discovery_and_register.png

3. 服务注册的配置是怎么填写的 ?

这次目标之一就是来解析解析服务是如何在服务启动时进行 注册 (Register) 这个操作的, 这次我们从 easy_note/cmd/user 这个服务开始分析, 因为它是被 注册 进入 Etcd 的服务之一。

 ├── cmd
     ├── api   // 分发业务逻辑
     ├── note  // 提供 笔记服务
     └── user  // 提供 用户服务

我们从其中的 main.go 开始下手,以下的内容是经过简化后的文件,是实现配置服务,启动服务的文件

 package main
 ​
 import (
   ···
 )
 ​
 ···
 ​
 func main() {
   // 返回的 是 registry.Registry 类型,是用于向 Etcd 注册和注销的方法
   r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress})
   ···
   addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8889")
   ···
   svr := user.NewServer(new(UserServiceImpl),
     // 设置服务的名称
     server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), 
     // 设置访问的地址
     server.WithServiceAddr(addr),               
     // 进行注册的配置
     server.WithRegistry(r),                           
   )
   ···
 }

由注释可知 WithRegister() 为配置 注册信息 的函数 ,那为什么直接就看这个函数呢?

一是因为主要配置注册的主要逻辑在其中,二是因为 With... 的函数结构都是大同小异,十分相似的。

再然后我们进入 kitex/server/option.go ,先看看 di.Push(fmt.Sprintf("WithRegistry(%T)", r)) 这一行,

这个 *util.Slice 是什么 ?进去看看?

 // Option is the only way to config server.
 type Option = internal_server.Option
 ​
 // WithRegistry to set a Registry to register service
 func WithRegistry(r registry.Registry) Option {
   return Option{F: func(o *internal_server.Options, di *utils.Slice) {
     // 注意这一行
     di.Push(fmt.Sprintf("WithRegistry(%T)", r))
     o.Registry = r
   }}
 }

进入 kitex/pkg/utils/slice.go, 我发现它很简短。但是它好眼熟,它好像是一个非常常见的数据结构 —— Stack (栈)

在这个文件之下有它的 slice__test.go 文件,看到这里的朋友可以去试验一下是否这个 Slice 和我的想法是否一致,大家看文章是要思考的嘛!最好可以动动手!

 package utils
 ​
 // Slice is an abstraction of []interface{}.
 type Slice []interface{}
 ​
 // Push pushes a interface to the slice.
 func (s *Slice) Push(any interface{}) {
   *s = append(*s, any)
 }
 ​
 // Pop pops the last element from the slice.
 func (s *Slice) Pop() (res interface{}) {
   if size := len(*s); size > 0 {
     res = (*s)[size-1]
     *s = (*s)[:size-1]
   }
   return
 }

我们再进入 o.Registry = r 这一行,可以得知 Options 用于初始化 server, Option 用于配置 Options (我觉得这种命名方式很巧妙,我感觉基本达到了 见名知意 的作用),里面东西很多,我们今天只看 Register 部分

 // Option is the only way to config a server.
 type Option struct {
   F func(o *Options, di *utils.Slice)
 }
 ​
 // Options is used to initialize the server.
 type Options struct {
   Svr      *rpcinfo.EndpointBasicInfo
   Configs  rpcinfo.RPCConfig
   LockBits int
   Once     *configutil.OptionOnce
 ​
   MetaHandlers []remote.MetaHandler
 ​
   RemoteOpt  *remote.ServerOption
   ErrHandle  func(error) error
   ExitSignal func() <-chan error
   Proxy      proxy.ReverseProxy
   ============================================
   // Registry is used for service registry.
   Registry registry.Registry
   // RegistryInfo is used to in registry.
   RegistryInfo *registry.Info
   ============================================
   ACLRules      []acl.RejectFunc
   Limits        *limit.Option
   LimitReporter limiter.LimitReporter
 ​
   MWBs []endpoint.MiddlewareBuilder
 ​
   Bus    event.Bus
   Events event.Queue
 ​
   // DebugInfo should only contains objects that are suitable for json serialization.
   DebugInfo    utils.Slice
   DebugService diagnosis.Service
 ​
   // Observability
   TracerCtl  *internal_stats.Controller
   StatsLevel *stats.Level
 }

4. Register 发生的时机是什么时候?

到了这里我们可以暂停思考一下,到达这一步是怎么个过程呢?是通过 main.go/user.NewServer() 的方法进来的。

NewServer() 的作用是什么?是用于配置初始化服务器的可选参数

配置完了参数什么时候生效呢 (Register 是什么时候发生的呢) ?其实配置的实现就在 main.go NewServer() 的下一句,Run() !

 fun main() {
   // 注册中心的地址, etcd 是在 go 中常见的注册中心
   r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress})
   ···
   addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8889")
   ···
   svr := user.NewServer(new(UserServiceImpl),
     // 设置服务的名称
     server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), 
     // 设置访问的地址
     server.WithServiceAddr(addr),               
     // 进行注册
     server.WithRegistry(r),                           
   )
   /
   err = svr.Run()
   ...
 }

进入Run方法的实现,可以得知 register 是发生在 server 启动成功后 的,停止也是会向 Etcd 进行注销操作的 (大家可以在同文件的 Stop() 中查看)

 // Run runs the server.
 func (s *server) Run() (err error) {
   // 注意 server开始运行了
   errCh := s.svr.Start()
   muStartHooks.Lock()
   for i := range onServerStart {
     go onServerStart[i]()
   }
   s.Lock()
   s.buildRegistryInfo(s.svr.Address())
   s.Unlock()
   // 注意这里,在这里才是真正开始将服务注册到 注册中心 !!是发生在启动之后的
   if err = s.waitExit(errCh); err != nil {
     klog.Errorf("KITEX: received error and exit: error=%s", err.Error())
   }
   ···
   return
 }
 ​
 func (s *server) buildRegistryInfo(lAddr net.Addr) {
    if s.opt.RegistryInfo == nil {
       s.opt.RegistryInfo = &registry.Info{}
    }
    info := s.opt.RegistryInfo
    // notice: lAddr may be nil when listen failed
    info.Addr = lAddr
    // 注意,我们配置的时候只填入了address, 也就是没有填写 ServiceName, 所以这里的代码一定会执行
    if info.ServiceName == "" {
       info.ServiceName = s.opt.Svr.ServiceName
    }
    if info.PayloadCodec == "" {
       info.PayloadCodec = s.opt.RemoteOpt.SvcInfo.PayloadCodec.String()
    }
    // 设置权重
    if info.Weight == 0 {
       info.Weight = discovery.DefaultWeight
    }
 }
 ​
 ​
 func (s *server) waitExit(errCh chan error) error {
   exitSignal := s.opt.ExitSignal()
 ​
   // service may not be available as soon as startup.
   delayRegister := time.After(1 * time.Second)
   for {
     select {
     case err := <-exitSignal:
       return err
     case err := <-errCh:
       return err
     case <-delayRegister:
       s.Lock()
       // 向 etcd 进行注册
       if err := s.opt.Registry.Register(s.opt.RegistryInfo); err != nil {
         s.Unlock()
         return err
       }
       s.Unlock()
     }
   }
 }

至此 服务完成了向 Etcd 的注册,我忽略了许多其他细节,这些细节也很有意思,希望大家可以自己试着探索

5. 总结

这次文章其实向大家分析了如何配置服务,以及向注册中心进行注册的方法和时机。

虽然省略了许多细节,但是通过这篇文章可以学到什么呢?

  • 如何配置 kitex 服务,以及服务的具体配置项 (就算是项目文档没有写,大家也是可以通过阅读源码来查看具体配置项的)
  • r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress}) 中的 r 的类型为 Interface 表明其实不是只有向 Etcd 这个注册中心 注册这一个选择,只要实现了相关接口,向其他的注册中心注册应该也是没有问题吧?
  • 这次分析的代码大多可以 见名知意 ,在让人会产生疑惑的地方会有适当的注释,这值得学习并应用到日常项目之中

希望大家阅读完文章可以帮助到需要的人,让阅读的朋友有所收获,谢谢各位耐心看完!