如何在go中实现程序的优雅退出,go-kratos源码解析

使用kratos这个框架有近一年了,最近了解了一下kratos关于程序优雅退出的具体实现。

这部分逻辑在app.go文件中,在main中,找到app.Run方法,点进入就可以了

它包含以下几个部分:

  1. App结构体:包含应用程序的配置选项和运行时状态。

  2. New函数:创建一个App实例。

  3. Run方法:启动应用程序。主要步骤包括:

    • 构建ServiceInstance注册实例
    • 启动Server
    • 注册实例到服务发现
    • 监听停止信号
  4. Stop方法:优雅停止应用程序。主要步骤包括:

    • 从服务发现中注销实例
    • 取消应用程序上下文
    • 停止Server
  5. buildInstance方法:构建用于服务发现注册的实例。

  6. NewContext和FromContext函数:给Context添加AppInfo,便于后续从Context获取。

核心的逻辑流程是:

  1. 创建App实例
  2. 在App.Run()里面启动Server,注册实例,监听信号
  3. 接收到停止信号后会调用App.Stop()停止应用

我们先对Run方法进行一个源码进行查看

go 复制代码
// Run executes all OnStart hooks registered with the application's Lifecycle.
func (a *App) Run() error {

  // 构建服务发现注册实例
  instance, err := a.buildInstance() 
  if err != nil {
    return err
  }

  // 保存实例  
  a.mu.Lock()
  a.instance = instance
  a.mu.Unlock()

  // 创建错误组
  eg, ctx := errgroup.WithContext(NewContext(a.ctx, a))

  // 等待组,用于等待Server启动完成
  wg := sync.WaitGroup{}

  // 启动每个Server
  for _, srv := range a.opts.servers {
    srv := srv 
    eg.Go(func() error {
      // 等待停止信号
      <-ctx.Done()  
      // 停止Server
      stopCtx, cancel := context.WithTimeout(a.opts.ctx, a.opts.stopTimeout)
      defer cancel()
      return srv.Stop(stopCtx)
    })

    wg.Add(1)
    eg.Go(func() error {
      // Server启动完成
      wg.Done() 
      // 启动Server  
      return srv.Start(NewContext(a.opts.ctx, a)) 
    })
  }

  // 等待所有Server启动完成
  wg.Wait()

  // 注册服务实例
  if a.opts.registrar != nil {
    rctx, rcancel := context.WithTimeout(ctx, a.opts.registrarTimeout)
    defer rcancel()
    if err := a.opts.registrar.Register(rctx, instance); err != nil {
      return err
    }
  }
  
  // 监听停止信号
  c := make(chan os.Signal, 1)
  signal.Notify(c, a.opts.sigs...)
  eg.Go(func() error {
    select {
    case <-ctx.Done():
      return nil
    case <-c:
      // 收到停止信号,停止应用------------- ⬅️注意此时
      return a.Stop() 
    }
  })

  // 等待错误组执行完成
  if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {
    return err
  }

  return nil
}

核心逻辑就是这里⬇️,使用signal.Notify去监听操作系统给出的停止信号。

go 复制代码
  // 监听停止信号
  c := make(chan os.Signal, 1)
  signal.Notify(c, a.opts.sigs...)
  eg.Go(func() error {
    select {
    case <-ctx.Done():
      return nil
    case <-c:
      // 收到停止信号,停止应用
      return a.Stop() 
    }
  })

然后调用了Stop方法,我们再看下Stop的源码

go 复制代码
// Stop gracefully stops the application.
func (a *App) Stop() error {

  // 获取服务实例 
  a.mu.Lock()
  instance := a.instance
  a.mu.Unlock()

  // 从服务发现注销实例
  if a.opts.registrar != nil && instance != nil {
    ctx, cancel := context.WithTimeout(NewContext(a.ctx, a), a.opts.registrarTimeout)
    defer cancel()
    if err := a.opts.registrar.Deregister(ctx, instance); err != nil {
      return err
    }
  }

  // 取消应用上下文
  if a.cancel != nil {
    a.cancel() 
  }

  return nil
}

主要步骤是:

1. 获取已经保存的服务实例
2. 如果配置了服务发现,则从服务发现中注销该实例
3. 取消应用上下文来通知应用停止

在Run方法中,我们通过context.WithCancel创建的可取消的上下文Context,在这里通过调用cancel函数来取消该上下文,以通知应用停止。

取消上下文会导致在Run方法中启动的协程全部退出,从而优雅停止应用。

所以Stop方法比较简单,关键是利用了Context来控制应用生命周期。

我们可以注意到,在Run方法中,我们使用到了一个signal包下的Notify方法来对操作系统的关闭事件进行监听,这个是我们动作的核心,我把这部分单独整理在了另一篇文章中。

通过对操作系统事件的监听,我们就可以对一些必须完成的任务进行优雅地停止,如果有一些任务必须完成,我们可以在任务开始使用 wg := sync.WaitGroup{} 来对任务进行一个Add操作,当所有任务完成,监听到操作系统的关闭动作,我们需要使用wg.wait() 等待任务完成再进行退出。以实现一个优雅地启停。

相关推荐
落落落sss10 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
简单.is.good27 分钟前
【测试】接口测试与接口自动化
开发语言·python
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
我是陈泽1 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
优雅的小武先生1 小时前
QT中的按钮控件和comboBox控件和spinBox控件无法点击的bug
开发语言·qt·bug
虽千万人 吾往矣1 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
创作小达人1 小时前
家政服务|基于springBoot的家政服务平台设计与实现(附项目源码+论文+数据库)
开发语言·python
郭二哈1 小时前
C++——list
开发语言·c++·list
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源