通过 layout 探索 kratos 运行原理
创建项目
首先需要安装好对应的依赖环境,以及工具:
- go
- protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
1# 创建项目模板
2kratos new helloworld
3
4cd helloworld
5# 拉取项目依赖
6go mod download
7# 生成proto模板
8kratos proto add api/helloworld/helloworld.proto
9# 生成proto源码
10kratos proto client api/helloworld/helloworld.proto
11# 生成server模板
12kratos proto server api/helloworld/helloworld.proto -t internal/service
执行命令后,会在当前目录下生成一个 service 工程,工程骨架如下,具体的工程骨架说明可以访问 layout
运行项目
1# 生成所有proto源码、wire等等
2go generate ./...
3
4# 编译成可执行文件
5go build -o ./bin/ ./...
6
7# 运行项目
8./bin/helloworld -conf ./configs
看到如下输出则证明项目启动正常
1level=INFO module=app service_id=7114ad8a-b3bf-11eb-a1b9-f0189850d2cb service_name= version=
2level=INFO module=transport/grpc msg=[gRPC] server listening on: [::]:9000
3level=INFO module=transport/http msg=[HTTP] server listening on: [::]:8000
测试接口
1curl 'http://127.0.0.1:8000/helloworld/krtaos'
2
3输出:
4{
5 "message": "Hello kratos"
6}
应用是如何跑起来的?
通过上面的图例👆,我们可以直观观察到应用的调用链,简化来说如下图流程所示👇
1. 注入依赖并调用 newApp() 方法
1// helloword/cmd/main.go
2func main() {
3 flag.Parse()
4 logger := log.NewStdLogger(os.Stdout)
5
6 // 调用 go-kratos/kratos/v2/config,创建 config 实例,并指定了来源和配置解析方法
7 c := config.New(
8 config.WithSource(
9 file.NewSource(flagconf),
10 ),
11 config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
12 return yaml.Unmarshal(kv.Value, v)
13 }),
14 )
15 if err := c.Load(); err != nil {
16 panic(err)
17 }
18
19 // 将配置扫描到,通过 proto 声明的 conf struct 上
20 var bc conf.Bootstrap
21 if err := c.Scan(&bc); err != nil {
22 panic(err)
23 }
24
25 // 通过 wire 将依赖注入,并调用 newApp 方法
26 app, cleanup, err := initApp(bc.Server, bc.Data, logger)
27 if err != nil {
28 panic(err)
29 }
30 // 省略代码...
31}
2. 创建 kratos 实例
项目 main.go 的 newApp() 方法中,调用了 go-kratos/kratos/v2/app.go 中的 kratos.New() 方法
1// helloword/cmd/main.go
2func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
3 return kratos.New(
4 // 配置应用
5 kratos.Name(Name),
6 kratos.Version(Version),
7 kratos.Metadata(map[string]string{}),
8 kratos.Logger(logger),
9 // kratos.Server() 传入的 http/grpc 服务会通过 buildInstance() 转换成registry.ServiceInstance struct*
10 kratos.Server(
11 hs,
12 gs,
13 ),
14 )
15}
该方法会返回一个 App struct,包含 Run() 和 Stop() 方法
1// go-kratos/kratos/v2/app.go
2type App struct {
3 opts options //配置
4 ctx context.Context // 上下文
5 cancel func() // context 的取消方法
6 instance *registry.ServiceInstance //通过 kratos.Server()声明的实例,并通过 buildInstance() 转换后的 *registry.ServiceInstance struct
7 log *log.Helper // 日志
8}
9
10// Run executes all OnStart hooks registered with the application's Lifecycle.
11func (a *App) Run() error {
12 // 省略代码...
13}
14
15// Stop gracefully stops the application.
16func (a *App) Stop() error {
17 // 省略代码...
18}
3. 调用 Run() 方法 #
项目在 main 方法中调用了 kratos.App struct 的 Run() 方法.
1// helloword/cmd/main.go
2// 省略代码...
3// 启动 Kratos
4if err := app.Run(); err != nil {
5 panic(err)
6}
Run() 方法的实现细节
1// go-kratos/kratos/v2/app.go
2func (a *App) Run() error {
3 a.log.Infow(
4 "service_id", a.opts.id,
5 "service_name", a.opts.name,
6 "version", a.opts.version,
7 )
8 g, ctx := errgroup.WithContext(a.ctx)
9 // 遍历通过 kratos.Server() 声明的服务实例
10 for _, srv := range a.opts.servers {
11 srv := srv
12 // 执行两个goroutine, 用于处理服务启动和退出
13 g.Go(func() error {
14 <-ctx.Done() // 阻塞,等待调用 cancel 方法
15 return srv.Stop() // 协程退出后,调用实例的停止方法
16 })
17 g.Go(func() error {
18 return srv.Start() // 调用实例的运行方法
19 })
20 }
21 // 判断是否调用 kratos.Registrar() 配置了注册发现中心
22 if a.opts.registrar != nil {
23 // 将实例注册到注册中心
24 if err := a.opts.registrar.Register(a.opts.ctx, a.instance); err != nil
25 return err
26 }
27 }
28 // 监听进程退出信号
29 c := make(chan os.Signal, 1)
30 signal.Notify(c, a.opts.sigs...)
31
32 // 处理进程退出和 context 退出
33 g.Go(func() error {
34 for {
35 select {
36 case <-ctx.Done():
37 return ctx.Err()
38 case <-c:
39 // 调用 kratos.App 的停止方法
40 a.Stop()
41 }
42 }
43 })
44 if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
45 return err
46 }
47 return nil
48}
4. 应用退出
Kratos 实例在启动时,监听了系统的进程退出信号,当收到退出信号时,kratos 会调用 App struct 的 Stop() 方法
1// go-kratos/kratos/v2/app.go
2func (a *App) Stop() error {
3 // 判断是否有注册中心配置
4 if a.opts.registrar != nil {
5 // 在注册中心中将实例注销
6 if err := a.opts.registrar.Deregister(a.opts.ctx, a.instance); err != nil {
7 return err
8 }
9 }
10 // 控制 goroutine 的退出,当调用 a.cancel()时,Run()方法中 监听的 <-ctx.Done() 收到消息后,没有阻塞后,方法会调用 server 的 Stop()方法,停止服务
11 if a.cancel != nil {
12 a.cancel()
13 }
14 return nil
15}