返回顶部
首页 > 资讯 > 后端开发 > GO >Golang使用channel实现一个优雅退出功能
  • 510
分享到

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

摘要

目录前言实现思路读源码HookEngine Status优雅退出自己实现适配 Hook适配 Engine Status适配 Graceful Shutdown总结前言 最近补 gol

前言

最近补 golang channel 方面八股的时候发现用 channel 实现一个优雅退出功能好像不是很难,之前写的 Http 框架刚好也不支持优雅退出功能,于是就参考了 Hertz 优雅退出方面的代码,为我的 PIANO 补足了这个 feature。

Hertz

字节跳动开源社区 CloudWeGo 开源的一款高性能 HTTP 框架,具有高易用性、高性能、高扩展性等特点。

PIANO

笔者自己实现的轻量级 HTTP 框架,具有中间件,三种不同的路由(静态,通配,参数)方式,路由分组,优雅退出等功能,迭代发展中。

实现思路

通过一个 os.Signal 类型的 chan 接收退出信号,收到信号后进行对应的退出收尾工作,利用 context.WithTimeouttime.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\n", sig)
			// graceful shutdown
			return nil
		}
	case err := <-errCh:
		// error occurs, exit immediately
		return err
	}

	return nil
}

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

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

  • syscall.SIGINT 表示中断信号,通常由用户在终端上按下 Ctrl+C 触发,用于请求程序停止运行;
  • syscall.SIGHUP 表示挂起信号,通常是由系统发送给进程,用于通知进程它的终端或控制台已经断开连接或终止,进程需要做一些清理工作;
  • syscall.SIGTERM 表示终止信号,通常也是由系统发送给进程,用于请求进程正常地终止运行,进程需要做一些清理工作;

如果 waitSignal 的返回值为 nilh.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)
	}
}

总结

本文通过对 Hertz 优雅退出功能的实现做了源码分析并对自己的 HTTP 框架进行了适配,希望可以帮助读者利用 channel 实现一个优雅退出功能提供参考和思路,如果哪里有问题或者错误欢迎评论或者私信,以上。

以上就是Golang使用channel实现一个优雅退出功能的详细内容,更多关于Golang channel退出的资料请关注编程网其它相关文章!

您可能感兴趣的文档:

--结束END--

本文标题: Golang使用channel实现一个优雅退出功能

本文链接: https://lsjlt.com/news/199134.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Golang使用channel实现一个优雅退出功能
    目录前言实现思路读源码HookEngine Status优雅退出自己实现适配 Hook适配 Engine Status适配 Graceful Shutdown总结前言 最近补 Gol...
    99+
    2023-03-09
    Golang channel实现退出功能 Golang channel退出 Golang channel
  • Golang怎么使用channel实现一个优雅退出功能
    这篇文章主要介绍了Golang怎么使用channel实现一个优雅退出功能的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Golang怎么使用channel实现一个优雅退出功能文章都会有所收获,下面我们一起来看看吧...
    99+
    2023-07-05
  • Golang实现程序优雅退出的方法详解
    目录1. 背景2. 常见的几种平滑关闭2.1 http server 平滑关闭2.2 gRPC server 平滑关闭2.3 worker 协程平滑关闭2.4 实现 io.Close...
    99+
    2024-04-02
  • 怎么实现一个按Home键退出应用的功能
    怎么实现一个按Home键退出应用的功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。实例如下:func exitApplication() { let app = ...
    99+
    2023-05-31
    home键
  • 怎么在Android中实现一个滑动退出Activity功能
    怎么在Android中实现一个滑动退出Activity功能?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。实现Android 滑动退出Activity的功能and...
    99+
    2023-05-30
    android activity roi
  • 使用golang怎么实现一个京东支付功能
    这篇文章主要介绍了使用golang怎么实现一个京东支付功能,此处通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考价值,需要的朋友可以参考下:什么是golanggolang 是Google开发的一种静态强类型、编译型、并发型...
    99+
    2023-06-06
  • 使用golang怎么实现一个登录验证码功能
    这篇文章将为大家详细讲解有关使用golang怎么实现一个登录验证码功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。golang适合做什么golang可以做服务器端开发,但golang很适合...
    99+
    2023-06-06
  • 使用golang怎么实现一个比特币交易功能
    使用golang怎么实现一个比特币交易功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。比特币交易交易(transaction)是比特币的核心所在,而区块链唯一的目的,也正...
    99+
    2023-06-15
  • 如何在Golang中使用WebSocket实现一个通信功能
    本篇文章给大家分享的是有关如何在Golang中使用WebSocket实现一个通信功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。什么是golanggolang 是Google...
    99+
    2023-06-06
  • 怎么使用Git实现优雅的版本回退
    这篇文章主要介绍“怎么使用Git实现优雅的版本回退”,在日常操作中,相信很多人在怎么使用Git实现优雅的版本回退问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么使用Git实现优雅的版本回退”的疑惑有所帮助!...
    99+
    2023-06-04
  • 使用Angular CDK实现一个Service弹出Toast组件功能
    目录1.环境安装2.创建Toast组件和ToastService2.1编写Toast组件和样式在Angular中,官方团队在开发Material组件库的同时,顺手做了一套Compon...
    99+
    2024-04-02
  • 在Android中使用PopupWindow实现一个弹出分享功能
    在Android中使用PopupWindow实现一个弹出分享功能?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。代码package com.duanlian.po...
    99+
    2023-05-31
    android popupwindow roi
  • Android应用中怎么实现一个双击返回键退出程序功能
    Android应用中怎么实现一个双击返回键退出程序功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。具体如下:  第一种就是根据用户点击俩次的时间间隔去判断是否退出程...
    99+
    2023-05-31
    android roi
  • Android应用中怎么实现一个双击返回键退出应用的功能
    Android应用中怎么实现一个双击返回键退出应用的功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。在MyAppliction中(继承Application) ...
    99+
    2023-05-31
    android roi
  • Spring Boot项目如何优雅实现Excel导入与导出功能
    目录背景EasyExcel 问题分析与解决Spring Boot Excel 导入与导出依赖引入Excel 导入基本导入功能进阶导入功能Excel 导出Excel 导入参数校验开启校...
    99+
    2024-04-02
  • 利用golang怎么实现一个微信支付功能
    本文章向大家介绍利用golang怎么实现一个微信支付功能的基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。golang的优点有哪些golang是一种编译语言,可以将代码编译为机器代码,编译后的二进制文件可以直接部署...
    99+
    2023-06-06
  • 使用java怎么实现一个ATM功能
    使用java怎么实现一个ATM功能?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java的特点有哪些Java的特点有哪些1.Java语言作为静态面向对象编程语言...
    99+
    2023-06-14
  • 使用servlet实现一个用户登录功能
    这篇文章给大家介绍使用servlet实现一个用户登录功能,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。用servlet来简单实现一个用户登录的小程序。 首先,servlet也是一个JAVA类,新建一个JAVA类,它直接...
    99+
    2023-05-31
    servlet 用户登录
  • 如何使用Angular CDK实现一个Service弹出Toast组件功能
    这篇文章将为大家详细讲解有关如何使用Angular CDK实现一个Service弹出Toast组件功能,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.环境安装cdk不是angular的默认模块,需要手动...
    99+
    2023-06-20
  • golang实现一个简单的websocket聊天室功能
    基本原理: 1.引入了 golang.org/x/net/websocket 包。 2.监听端口。 3.客户端连接时,发送结构体: {"type":"login","uid":"我是...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作