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
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 = ®istry.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 这个注册中心 注册这一个选择,只要实现了相关接口,向其他的注册中心注册应该也是没有问题吧? - 这次分析的代码大多可以 见名知意 ,在让人会产生疑惑的地方会有适当的注释,这值得学习并应用到日常项目之中
希望大家阅读完文章可以帮助到需要的人,让阅读的朋友有所收获,谢谢各位耐心看完!