«

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

时间:2024-8-3 07:37     作者:韩俊     分类: Go语言


这篇文章主要介绍了Golang怎么使用channel实现一个优雅退出功能的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Golang怎么使用channel实现一个优雅退出功能文章都会有所收获,下面我们一起来看看吧。

    实现思路

    通过一个

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

    读源码

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

    Hook

    Hook 函数是一个通用的概念,表示某事件触发时所伴随的操作,Hertz 提供了 StartHook 和 ShutdownHook 用于在服务触发启动后和退出前注入用户自己的处理逻辑。

    两种 Hook 具体是作为两种不同类型的 Hertz Engine 字段,用户可以直接以

    append
    的方式添加自己的 Hooks,下面是作为 Hertz Engine 字段的代码:

    type Engine struct {
        ...
        
        // Hook functions get triggered sequentially when engine start
        OnRun []CtxErrCallback
    
        // Hook functions get triggered simultaneously when engine shutdown
        OnShutdown []CtxCallback
        
        ...
    }

    可以看到两者都是函数数组的形式,并且是公开字段,所以可以直接

    append
    ,函数的签名如下,
    OnShutdown
    的函数不会返回 error 因为都退出了所以没法对错误进行处理:

    // OnRun
    type CtxCallback func(ctx context.Context)
    
    // OnShutdown
    type CtxErrCallback func(ctx context.Context) error

    并且设置的 StartHook 会按照声明顺序依次调用,但是 ShutdownHook 会并发的进行调用,这里的实现后面会讲。

    StartHook 的执行时机

    触发 Server 启动后,框架会按函数声明顺序依次调用所有的

    StartHook
    函数,完成调用之后,才会正式开始端口监听,如果发生错误,则立刻终止服务。

    上面是官方文档中描述的 StartHook 的执行时机,具体在源码中就是下面的代码:

    func (engine *Engine) Run() (err error) {
        ...
    
        // trigger hooks if any
        ctx := context.Background()
        for i := range engine.OnRun {
            if err = engine.OnRun[i](ctx); err != nil {
                return err
            }
        }
    
        return engine.listenAndServe()
    }

    熟悉或使用过 Hertz 的同学肯定知道

    h.Spin()
    方法调用后会正式启动 Hertz 的 HTTP 服务,而上面的
    engine.Run
    方法则是被
    h.Spin
    异步调用的。可以看到在
    engine.Run
    方法里循环调用
    engine.OnRun
    数组中注册的函数,最后执行完成完成并且没有 error 的情况下才会执行
    engine.listenAndServe()
    正式开始端口监听,和官方文档中说的一致,并且这里是通过 for 循环调用的所以也正如文档所说框架会按函数声明顺序依次调用。

    ShutdownHook 的执行时机

    Server 退出前,框架会并发地调用所有声明的

    ShutdownHook
    函数,并且可以通过
    server.WithExitWaitTime
    配置最大等待时长,默认为5秒,如果超时,则立刻终止服务。

    上面是官方文档中描述的 ShutdownHook 的执行时机,具体在源码中就是下面的代码:

    func (engine *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) {
        wg := sync.WaitGroup{}
        for i := range engine.OnShutdown {
            wg.Add(1)
            go func(index int) {
                defer wg.Done()
                engine.OnShutdown[index](ctx)
            }(i)
        }
        wg.Wait()
        ch <- struct{}{}
    }

    通过

    sync.WaitGroup
    保证每个 ShutdownHook 函数都执行完毕后给形参
    ch
    发送信号通知,注意这里每个 ShutdownHook 都起了一个协程,所以是并发执行,这也是官方文档所说的并发的进行调用。

    服务注册与下线的执行时机

    服务注册

    Hertz 虽然是一个 HTTP 框架,但是 Hertz 的客户端和服务端可以通过注册中心进行服务发现并进行调用,并且 Hertz 也提供了大部分常用的注册中心扩展,在下面的

    initOnRunHooks
    方法中,通过注册一个
    StartHook
    调用
    Registry
    接口的
    Register
    方法对服务进行注册。

    func (h *Hertz) initOnRunHooks(errChan chan error) {
        // add register func to runHooks
        opt := h.GetOptions()
        h.OnRun = append(h.OnRun, func(ctx context.Context) error {
            go func() {
                // delay register 1s
                time.Sleep(1 * time.Second)
                if err := opt.Registry.Register(opt.RegistryInfo); err != nil {
                    hlog.SystemLogger().Errorf("Register error=%v", err)
                    // pass err to errChan
                    errChan <- err
                }
            }()
            return nil
        })
    }

    取消注册

    Shutdown
    方法中进行调用
    Deregister
    取消注册,可以看到刚刚提到的
    executeOnShutdownHooks
    的方法在开始异步执行后就会进行取消注册操作。

    func (engine *Engine) Shutdown(ctx context.Context) (err error) {
        ...
    
        ch := make(chan struct{})
        // trigger hooks if any
        go engine.executeOnShutdownHooks(ctx, ch)
    
        defer func() {
            // ensure that the hook is executed until wait timeout or finish
            select {
            case <-ctx.Done():
                hlog.SystemLogger().Infof("Execute OnShutdownHooks timeout: error=%v", ctx.Err())
                return
            case <-ch:
                hlog.SystemLogger().Info("Execute OnShutdownHooks finish")
                return
            }
        }()
    
        if opt := engine.options; opt != nil && opt.Registry != nil {
            if err = opt.Registry.Deregister(opt.RegistryInfo); err != nil {
                hlog.SystemLogger().Errorf("Deregister error=%v", err)
                return err
            }
        }
    
        ...
    }

    Engine Status

    讲 graceful shutdown 之前最好了解一下 Hertz Engine 的

    status
    字段以获得更好的阅读体验ww

    type Engine struct {
        ...
        
        // Indicates the engine status (Init/Running/Shutdown/Closed).
        status uint32
        
        ...
    }

    如上所示,

    status
    是一个
    uint32
    类型的内部字段,用来表示 Hertz Engine 的状态,具体具有四种状态(Init 1, Running 2, Shutdown 3, Closed 4),由下面的常量定义。

    const (
        _ uint32 = iota
        statusInitialized
        statusRunning
        statusShutdown
        statusClosed
    )

    下面列出了 Hertz Engine 状态改变的时机:

    函数状态改变前状态改变后
    engine.Init0Init (1)
    engine.RunInit (1)Running (2)
    engine.ShutdownRunning (2)Shutdown (3)
    engine.Run defer?Closed (4)

    对状态的改变都是通过

    atomic
    包下的函数进行更改的,保证了并发安全。

    优雅退出

    Hertz Graceful Shutdown 功能的核心方法如下,

    signalToNotify
    数组包含了所有会触发退出的信号,触发了的信号会传向
    signals
    这个 channel,并且 Hertz 会根据收到信号类型决定进行优雅退出还是强制退出。

    // Default implementation for signal waiter.
    // SIGTERM triggers immediately close.
    // SIGHUP|SIGINT triggers graceful shutdown.
    func waitSignal(errCh chan error) error {
        signalToNotify := []os.Signal{syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM}
        if signal.Ignored(syscall.SIGHUP) {
            signalToNotify = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
        }
    
        signals := make(chan os.Signal, 1)
        signal.Notify(signals, signalToNotify...)
    
        select {
        case sig := <-signals:
            switch sig {
            case syscall.SIGTERM:
                // force exit
                return errors.New(sig.String()) // nolint
            case syscall.SIGHUP, syscall.SIGINT:
                hlog.SystemLogger().Infof("Received signal: %s
    ", sig)
                // graceful shutdown
                return nil
            }
        case err := <-errCh:
            // error occurs, exit immediately
            return err
        }
    
        return nil
    }

    如果

    engine.Run
    方法返回了一个错误则会通过
    errCh
    传入
    waitSignal
    函数然后触发立刻退出。前面也提到
    h.Spin()
    是以异步的方式调用
    engine.Run
    waitSignal
    则由
    h.Spin()
    直接调用,所以运行后 Hertz 会阻塞在
    waitSignal
    函数的
    select
    这里等待信号。

    三个会触发 Shutdown 的信号区别如下:

      syscall.SIGINT
      表示中断信号,通常由用户在终端上按下 Ctrl+C 触发,用于请求程序停止运行;

      syscall.SIGHUP
      表示挂起信号,通常是由系统发送给进程,用于通知进程它的终端或控制台已经断开连接或终止,进程需要做一些清理工作;

      syscall.SIGTERM
      表示终止信号,通常也是由系统发送给进程,用于请求进程正常地终止运行,进程需要做一些清理工作;

    如果

    waitSignal
    的返回值为
    nil
    h.Spin()
    会进行优雅退出:

    func (h *Hertz) Spin() {
        errCh := make(chan error)
        h.initOnRunHooks(errCh)
        go func() {
            errCh <- h.Run()
        }()
    
        signalWaiter := waitSignal
        if h.signalWaiter != nil {
            signalWaiter = h.signalWaiter
        }
    
        if err := signalWaiter(errCh); err != nil {
            hlog.SystemLogger().Errorf("Receive close signal: error=%v", err)
            if err := h.Engine.Close(); err != nil {
                hlog.SystemLogger().Errorf("Close error=%v", err)
            }
            return
        }
    
        hlog.SystemLogger().Infof("Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)
    
        ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
        defer cancel()
    
        if err := h.Shutdown(ctx); err != nil {
            hlog.SystemLogger().Errorf("Shutdown error=%v", err)
        }
    }

    并且 Hertz 通过

    context.WithTimeout
    的方式设置了优雅退出的超时时长,默认为 5 秒,用户可以通过
    WithExitWaitTime
    方法配置 server 的优雅退出超时时长。将设置了超时时间的
    ctx
    传入
    Shutdown
    方法,如果 ShutdownHook 先执行完毕则
    ch
    channel 收到信号后返回退出,否则 Context 超时收到信号强制返回退出。

    func (engine *Engine) Shutdown(ctx context.Context) (err error) {
        ...
    
        ch := make(chan struct{})
        // trigger hooks if any
        go engine.executeOnShutdownHooks(ctx, ch)
    
        defer func() {
            // ensure that the hook is executed until wait timeout or finish
            select {
            case <-ctx.Done():
                hlog.SystemLogger().Infof("Execute OnShutdownHooks timeout: error=%v", ctx.Err())
                return
            case <-ch:
                hlog.SystemLogger().Info("Execute OnShutdownHooks finish")
                return
            }
        }()
    
        ...
        return
    }

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

    自己实现

    说是自己实现实际上也就是代码搬运工,把 Hertz 的 graceful shutdown 及其相关功能给 PIANO 进行适配罢了ww

    代码实现都差不多,一些小细节根据我个人的习惯做了修改,完整修改参考这个 commit,对 PIANO 感兴趣的话欢迎 Star !

    适配 Hook

    type Engine struct {
        ...
    
        // hook
        OnRun      []HookFuncWithErr
        OnShutdown []HookFunc
    
        ...
    }
    
    type (
        HookFunc        func(ctx context.Context)
        HookFuncWithErr func(ctx context.Context) error
    )
    
    func (e *Engine) executeOnRunHooks(ctx context.Context) error {
        for _, h := range e.OnRun {
            if err := h(ctx); err != nil {
                return err
            }
        }
        return nil
    }
    
    func (e *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) {
        wg := sync.WaitGroup{}
        for _, h := range e.OnShutdown {
            wg.Add(1)
            go func(hook HookFunc) {
                defer wg.Done()
                hook(ctx)
            }(h)
        }
        wg.Wait()
        ch <- struct{}{}
    }

    适配 Engine Status

    type Engine struct {
        ...
        
        // initialized | running | shutdown | closed
        status uint32
    
        ...
    }
    
    const (
        _ uint32 = iota
        statusInitialized
        statusRunning
        statusShutdown
        statusClosed
    )

    适配 Graceful Shutdown

    // Play the PIANO now
    func (p *Piano) Play() {
        errCh := make(chan error)
        go func() {
            errCh <- p.Run()
        }()
        waitSignal := func(errCh chan error) error {
            signalToNotify := []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM}
            if signal.Ignored(syscall.SIGHUP) {
                signalToNotify = signalToNotify[1:]
            }
            signalCh := make(chan os.Signal, 1)
            signal.Notify(signalCh, signalToNotify...)
            select {
            case sig := <-signalCh:
                switch sig {
                case syscall.SIGTERM:
                    // force exit
                    return errors.New(sig.String())
                case syscall.SIGHUP, syscall.SIGINT:
                    // graceful shutdown
                    log.Infof("---PIANO--- Receive signal: %v", sig)
                    return nil
                }
            case err := <-errCh:
                return err
            }
            return nil
        }
        if err := waitSignal(errCh); err != nil {
            log.Errorf("---PIANO--- Receive close signal error: %v", err)
            return
        }
        log.Infof("---PIANO--- Begin graceful shutdown, wait up to %d seconds", p.Options().ShutdownTimeout/time.Second)
        ctx, cancel := context.WithTimeout(context.Background(), p.Options().ShutdownTimeout)
        defer cancel()
        if err := p.Shutdown(ctx); err != nil {
            log.Errorf("---PIANO--- Shutdown err: %v", err)
        }
    }

    标签: golang

    热门推荐