设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间00 现在时间00 缓存数据 对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

查看: 1631|回复: 5

Golang使用channel实现一个优雅退出功能

[复制链接]

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:27
  • 打卡月天数:0
  • 打卡总奖励:360
  • 最近打卡:2025-09-26 06:00:19
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
377
主题
337
精华
0
金钱
1457
积分
770
注册时间
2023-2-10
最后登录
2025-9-26

发表于 2023-5-19 22:47:47 | 显示全部楼层 |阅读模式
前言

最近补 Golang channel 方面八股的时候发现用 channel 实现一个优雅退出功能好像不是很难,之前写的 HTTP 框架刚好也不支持优雅退出功能,于是就参考了 Hertz 优雅退出方面的代码,为我的 PIANO 补足了这个 feature。
Hertz
字节跳动开源社区 CloudWeGo 开源的一款高性能 HTTP 框架,具有高易用性、高性能、高扩展性等特点。
PIANO
笔者自己实现的轻量级 HTTP 框架,具有中间件,三种不同的路由(静态,通配,参数)方式,路由分组,优雅退出等功能,迭代发展中。

实现思路

通过一个
  1. os.Signal
复制代码
类型的
  1. chan
复制代码
接收退出信号,收到信号后进行对应的退出收尾工作,利用
  1. context.WithTimeout
复制代码
  1. time.After
复制代码
等方式设置退出超时时间防止收尾等待时间过长。

读源码

由于 Hertz 的 Hook 功能中的 ShutdownHook 是 graceful shutdown 的一环,并且 Hook 功能的实现也不是很难所以这里就一起分析了,如果不想看直接跳到后面的章节即可 :)

Hook

Hook 函数是一个通用的概念,表示某事件触发时所伴随的操作,Hertz 提供了 StartHook 和 ShutdownHook 用于在服务触发启动后和退出前注入用户自己的处理逻辑。
两种 Hook 具体是作为两种不同类型的 Hertz Engine 字段,用户可以直接以
  1. append
复制代码
的方式添加自己的 Hooks,下面是作为 Hertz Engine 字段的代码:
  1. type Engine struct {
  2.     ...
  3.    
  4.     // Hook functions get triggered sequentially when engine start
  5.         OnRun []CtxErrCallback

  6.         // Hook functions get triggered simultaneously when engine shutdown
  7.         OnShutdown []CtxCallback
  8.    
  9.     ...
  10. }
复制代码
可以看到两者都是函数数组的形式,并且是公开字段,所以可以直接
  1. append
复制代码
,函数的签名如下,
  1. OnShutdown
复制代码
的函数不会返回 error 因为都退出了所以没法对错误进行处理:
  1. // OnRun
  2. type CtxCallback func(ctx context.Context)

  3. // OnShutdown
  4. type CtxErrCallback func(ctx context.Context) error
复制代码
并且设置的 StartHook 会按照声明顺序依次调用,但是 ShutdownHook 会并发的进行调用,这里的实现后面会讲。
StartHook 的执行时机
触发 Server 启动后,框架会按函数声明顺序依次调用所有的
  1. StartHook
复制代码
函数,完成调用之后,才会正式开始端口监听,如果发生错误,则立刻终止服务。
上面是官方文档中描述的 StartHook 的执行时机,具体在源码中就是下面的代码:
  1. func (engine *Engine) Run() (err error) {
  2.         ...

  3.         // trigger hooks if any
  4.         ctx := context.Background()
  5.         for i := range engine.OnRun {
  6.                 if err = engine.OnRun[i](ctx); err != nil {
  7.                         return err
  8.                 }
  9.         }

  10.         return engine.listenAndServe()
  11. }
复制代码
熟悉或使用过 Hertz 的同学肯定知道
  1. h.Spin()
复制代码
方法调用后会正式启动 Hertz 的 HTTP 服务,而上面的
  1. engine.Run
复制代码
方法则是被
  1. h.Spin
复制代码
异步调用的。可以看到在
  1. engine.Run
复制代码
方法里循环调用
  1. engine.OnRun
复制代码
数组中注册的函数,最后执行完成完成并且没有 error 的情况下才会执行
  1. engine.listenAndServe()
复制代码
正式开始端口监听,和官方文档中说的一致,并且这里是通过 for 循环调用的所以也正如文档所说框架会按函数声明顺序依次调用。
ShutdownHook 的执行时机
Server 退出前,框架会并发地调用所有声明的
  1. ShutdownHook
复制代码
函数,并且可以通过
  1. server.WithExitWaitTime
复制代码
配置最大等待时长,默认为5秒,如果超时,则立刻终止服务。
上面是官方文档中描述的 ShutdownHook 的执行时机,具体在源码中就是下面的代码:
  1. func (engine *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) {
  2.         wg := sync.WaitGroup{}
  3.         for i := range engine.OnShutdown {
  4.                 wg.Add(1)
  5.                 go func(index int) {
  6.                         defer wg.Done()
  7.                         engine.OnShutdown[index](ctx)
  8.                 }(i)
  9.         }
  10.         wg.Wait()
  11.         ch <- struct{}{}
  12. }
复制代码
通过
  1. sync.WaitGroup
复制代码
保证每个 ShutdownHook 函数都执行完毕后给形参
  1. ch
复制代码
发送信号通知,注意这里每个 ShutdownHook 都起了一个协程,所以是并发执行,这也是官方文档所说的并发的进行调用。
服务注册与下线的执行时机
服务注册
Hertz 虽然是一个 HTTP 框架,但是 Hertz 的客户端和服务端可以通过注册中心进行服务发现并进行调用,并且 Hertz 也提供了大部分常用的注册中心扩展,在下面的
  1. initOnRunHooks
复制代码
方法中,通过注册一个
  1. StartHook
复制代码
调用
  1. Registry
复制代码
接口的
  1. Register
复制代码
方法对服务进行注册。
  1. func (h *Hertz) initOnRunHooks(errChan chan error) {
  2.         // add register func to runHooks
  3.         opt := h.GetOptions()
  4.         h.OnRun = append(h.OnRun, func(ctx context.Context) error {
  5.                 go func() {
  6.                         // delay register 1s
  7.                         time.Sleep(1 * time.Second)
  8.                         if err := opt.Registry.Register(opt.RegistryInfo); err != nil {
  9.                                 hlog.SystemLogger().Errorf("Register error=%v", err)
  10.                                 // pass err to errChan
  11.                                 errChan <- err
  12.                         }
  13.                 }()
  14.                 return nil
  15.         })
  16. }
复制代码
取消注册
  1. Shutdown
复制代码
方法中进行调用
  1. Deregister
复制代码
取消注册,可以看到刚刚提到的
  1. executeOnShutdownHooks
复制代码
的方法在开始异步执行后就会进行取消注册操作。
  1. func (engine *Engine) Shutdown(ctx context.Context) (err error) {
  2.         ...

  3.         ch := make(chan struct{})
  4.         // trigger hooks if any
  5.         go engine.executeOnShutdownHooks(ctx, ch)

  6.         defer func() {
  7.                 // ensure that the hook is executed until wait timeout or finish
  8.                 select {
  9.                 case <-ctx.Done():
  10.                         hlog.SystemLogger().Infof("Execute OnShutdownHooks timeout: error=%v", ctx.Err())
  11.                         return
  12.                 case <-ch:
  13.                         hlog.SystemLogger().Info("Execute OnShutdownHooks finish")
  14.                         return
  15.                 }
  16.         }()

  17.         if opt := engine.options; opt != nil && opt.Registry != nil {
  18.                 if err = opt.Registry.Deregister(opt.RegistryInfo); err != nil {
  19.                         hlog.SystemLogger().Errorf("Deregister error=%v", err)
  20.                         return err
  21.                 }
  22.         }

  23.         ...
  24. }
复制代码
Engine Status

讲 graceful shutdown 之前最好了解一下 Hertz Engine 的
  1. status
复制代码
字段以获得更好的阅读体验ww
  1. type Engine struct {
  2.     ...
  3.    
  4.     // Indicates the engine status (Init/Running/Shutdown/Closed).
  5.     status uint32
  6.    
  7.     ...
  8. }
复制代码
如上所示,
  1. status
复制代码
是一个
  1. uint32
复制代码
类型的内部字段,用来表示 Hertz Engine 的状态,具体具有四种状态(Init 1, Running 2, Shutdown 3, Closed 4),由下面的常量定义。
  1. const (
  2.         _ uint32 = iota
  3.         statusInitialized
  4.         statusRunning
  5.         statusShutdown
  6.         statusClosed
  7. )
复制代码
下面列出了 Hertz Engine 状态改变的时机:
函数状态改变前状态改变后engine.Init0Init (1)engine.RunInit (1)Running (2)engine.ShutdownRunning (2)Shutdown (3)engine.Run defer?Closed (4)对状态的改变都是通过
  1. atomic
复制代码
包下的函数进行更改的,保证了并发安全。

优雅退出

Hertz Graceful Shutdown 功能的核心方法如下,
  1. signalToNotify
复制代码
数组包含了所有会触发退出的信号,触发了的信号会传向
  1. signals
复制代码
这个 channel,并且 Hertz 会根据收到信号类型决定进行优雅退出还是强制退出。
  1. // Default implementation for signal waiter.
  2. // SIGTERM triggers immediately close.
  3. // SIGHUP|SIGINT triggers graceful shutdown.
  4. func waitSignal(errCh chan error) error {
  5.         signalToNotify := []os.Signal{syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM}
  6.         if signal.Ignored(syscall.SIGHUP) {
  7.                 signalToNotify = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
  8.         }

  9.         signals := make(chan os.Signal, 1)
  10.         signal.Notify(signals, signalToNotify...)

  11.         select {
  12.         case sig := <-signals:
  13.                 switch sig {
  14.                 case syscall.SIGTERM:
  15.                         // force exit
  16.                         return errors.New(sig.String()) // nolint
  17.                 case syscall.SIGHUP, syscall.SIGINT:
  18.                         hlog.SystemLogger().Infof("Received signal: %s\n", sig)
  19.                         // graceful shutdown
  20.                         return nil
  21.                 }
  22.         case err := <-errCh:
  23.                 // error occurs, exit immediately
  24.                 return err
  25.         }

  26.         return nil
  27. }
复制代码
如果
  1. engine.Run
复制代码
方法返回了一个错误则会通过
  1. errCh
复制代码
传入
  1. waitSignal
复制代码
函数然后触发立刻退出。前面也提到
  1. h.Spin()
复制代码
是以异步的方式调用
  1. engine.Run
复制代码
  1. waitSignal
复制代码
则由
  1. h.Spin()
复制代码
直接调用,所以运行后 Hertz 会阻塞在
  1. waitSignal
复制代码
函数的
  1. select
复制代码
这里等待信号。
三个会触发 Shutdown 的信号区别如下:

    1. syscall.SIGINT
    复制代码
    表示中断信号,通常由用户在终端上按下 Ctrl+C 触发,用于请求程序停止运行;
    1. syscall.SIGHUP
    复制代码
    表示挂起信号,通常是由系统发送给进程,用于通知进程它的终端或控制台已经断开连接或终止,进程需要做一些清理工作;
    1. syscall.SIGTERM
    复制代码
    表示终止信号,通常也是由系统发送给进程,用于请求进程正常地终止运行,进程需要做一些清理工作;
如果
  1. waitSignal
复制代码
的返回值为
  1. nil
复制代码
  1. h.Spin()
复制代码
会进行优雅退出:
  1. func (h *Hertz) Spin() {
  2.         errCh := make(chan error)
  3.         h.initOnRunHooks(errCh)
  4.         go func() {
  5.                 errCh <- h.Run()
  6.         }()

  7.         signalWaiter := waitSignal
  8.         if h.signalWaiter != nil {
  9.                 signalWaiter = h.signalWaiter
  10.         }

  11.         if err := signalWaiter(errCh); err != nil {
  12.                 hlog.SystemLogger().Errorf("Receive close signal: error=%v", err)
  13.                 if err := h.Engine.Close(); err != nil {
  14.                         hlog.SystemLogger().Errorf("Close error=%v", err)
  15.                 }
  16.                 return
  17.         }

  18.         hlog.SystemLogger().Infof("Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)

  19.         ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
  20.         defer cancel()

  21.         if err := h.Shutdown(ctx); err != nil {
  22.                 hlog.SystemLogger().Errorf("Shutdown error=%v", err)
  23.         }
  24. }
复制代码
并且 Hertz 通过
  1. context.WithTimeout
复制代码
的方式设置了优雅退出的超时时长,默认为 5 秒,用户可以通过
  1. WithExitWaitTime
复制代码
方法配置 server 的优雅退出超时时长。将设置了超时时间的
  1. ctx
复制代码
传入
  1. Shutdown
复制代码
方法,如果 ShutdownHook 先执行完毕则
  1. ch
复制代码
channel 收到信号后返回退出,否则 Context 超时收到信号强制返回退出。
  1. func (engine *Engine) Shutdown(ctx context.Context) (err error) {
  2.         ...

  3.         ch := make(chan struct{})
  4.         // trigger hooks if any
  5.         go engine.executeOnShutdownHooks(ctx, ch)

  6.         defer func() {
  7.                 // ensure that the hook is executed until wait timeout or finish
  8.                 select {
  9.                 case <-ctx.Done():
  10.                         hlog.SystemLogger().Infof("Execute OnShutdownHooks timeout: error=%v", ctx.Err())
  11.                         return
  12.                 case <-ch:
  13.                         hlog.SystemLogger().Info("Execute OnShutdownHooks finish")
  14.                         return
  15.                 }
  16.         }()

  17.         ...
  18.         return
  19. }
复制代码
以上就是 Hertz 优雅退出部分的源码分析,可以发现 Hertz 多次利用了协程,通过 channel 传递信号进行流程控制和信息传递,并通过 Context 的超时机制完成了整个优雅退出流程。

自己实现

说是自己实现实际上也就是代码搬运工,把 Hertz 的 graceful shutdown 及其相关功能给 PIANO 进行适配罢了ww
代码实现都差不多,一些小细节根据我个人的习惯做了修改,完整修改参考这个 commit,对 PIANO 感兴趣的话欢迎 Star !

适配 Hook
  1. type Engine struct {
  2.     ...

  3.         // hook
  4.         OnRun      []HookFuncWithErr
  5.         OnShutdown []HookFunc

  6.         ...
  7. }

  8. type (
  9.         HookFunc        func(ctx context.Context)
  10.         HookFuncWithErr func(ctx context.Context) error
  11. )

  12. func (e *Engine) executeOnRunHooks(ctx context.Context) error {
  13.         for _, h := range e.OnRun {
  14.                 if err := h(ctx); err != nil {
  15.                         return err
  16.                 }
  17.         }
  18.         return nil
  19. }

  20. func (e *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) {
  21.         wg := sync.WaitGroup{}
  22.         for _, h := range e.OnShutdown {
  23.                 wg.Add(1)
  24.                 go func(hook HookFunc) {
  25.                         defer wg.Done()
  26.                         hook(ctx)
  27.                 }(h)
  28.         }
  29.         wg.Wait()
  30.         ch <- struct{}{}
  31. }
复制代码
适配 Engine Status
  1. type Engine struct {        ...            // initialized | running | shutdown | closed        status uint32    ...}const (
  2.         _ uint32 = iota
  3.         statusInitialized
  4.         statusRunning
  5.         statusShutdown
  6.         statusClosed
  7. )
复制代码
适配 Graceful Shutdown
  1. // Play the PIANO now
  2. func (p *Piano) Play() {
  3.         errCh := make(chan error)
  4.         go func() {
  5.                 errCh <- p.Run()
  6.         }()
  7.         waitSignal := func(errCh chan error) error {
  8.                 signalToNotify := []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM}
  9.                 if signal.Ignored(syscall.SIGHUP) {
  10.                         signalToNotify = signalToNotify[1:]
  11.                 }
  12.                 signalCh := make(chan os.Signal, 1)
  13.                 signal.Notify(signalCh, signalToNotify...)
  14.                 select {
  15.                 case sig := <-signalCh:
  16.                         switch sig {
  17.                         case syscall.SIGTERM:
  18.                                 // force exit
  19.                                 return errors.New(sig.String())
  20.                         case syscall.SIGHUP, syscall.SIGINT:
  21.                                 // graceful shutdown
  22.                                 log.Infof("---PIANO--- Receive signal: %v", sig)
  23.                                 return nil
  24.                         }
  25.                 case err := <-errCh:
  26.                         return err
  27.                 }
  28.                 return nil
  29.         }
  30.         if err := waitSignal(errCh); err != nil {
  31.                 log.Errorf("---PIANO--- Receive close signal error: %v", err)
  32.                 return
  33.         }
  34.         log.Infof("---PIANO--- Begin graceful shutdown, wait up to %d seconds", p.Options().ShutdownTimeout/time.Second)
  35.         ctx, cancel := context.WithTimeout(context.Background(), p.Options().ShutdownTimeout)
  36.         defer cancel()
  37.         if err := p.Shutdown(ctx); err != nil {
  38.                 log.Errorf("---PIANO--- Shutdown err: %v", err)
  39.         }
  40. }
复制代码
总结

本文通过对 Hertz 优雅退出功能的实现做了源码分析并对自己的 HTTP 框架进行了适配,希望可以帮助读者利用 channel 实现一个优雅退出功能提供参考和思路,如果哪里有问题或者错误欢迎评论或者私信,以上。
以上就是Golang使用channel实现一个优雅退出功能的详细内容,更多关于Golang channel退出的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
19
积分
18
注册时间
2022-12-26
最后登录
2022-12-26

发表于 2023-6-10 23:34:25 | 显示全部楼层
谢谢分享~~~~~
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2024-10-4 19:13:42 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:18
  • 打卡月天数:1
  • 打卡总奖励:203
  • 最近打卡:2025-12-03 13:50:25
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
241
积分
40
注册时间
2023-2-21
最后登录
2025-12-3

发表于 2025-2-14 21:25:03 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:无名新人
  • 打卡总天数:1
  • 打卡月天数:0
  • 打卡总奖励:16
  • 最近打卡:2025-08-09 22:38:01
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
31
积分
10
注册时间
2023-5-10
最后登录
2025-8-9

发表于 2025-3-27 22:25:49 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
25
积分
30
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-9-2 04:32:11 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼
5楼
6楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表