前言
前不久介绍了如何比较 优雅的退出golang服务 ,虽然能够优雅的退出,但是只是简单的监听了系统的信号,后续服务模块得到资源释放又牵扯到更多的逻辑。对于服务启动的子协程的声明周期显然没有考虑到。经过一些时间的实践和阅读大佬们的代码,对如何优雅的退出golang服务再做一个简单的记录。
1.业务处理服务
1.1 必须的接口
一个供外部调用的释放资源的函数,比如:Close
1.2 必须的参数
chExit
:是一个缓冲区为1的channal。用来接收Close接口函数的指令,控制模块的业务逻辑退出。
wg
:是sync.WaitGroup类型的对象,当模块收到退出信号后,需要等待已经加载到数据处理缓存的数据处理完之后才能退出模块,防止数据丢失的问题。
1.3 示例
这个示例中用到了生产-消费的典型模型,消费者和生产者之间使用channel胶合。当主程序收到退信信号时,主服务调用模块service
的Close
函数,在Close函数中按逻辑释放service
的资源。
1.3.1 service
的定义
在servicec
中定义了一个chExit的channel,并且初始化缓冲区为1。chVal
是生产者和消费者的胶合channel,wg是保证消费者和生产者的数据能够同步,保证在退出时不会丢数据。
go s.fetch()
模拟生产者,在这个函数中会循环往chVal
中写入数据,为了模拟退出时的等待,在每次写数据前会sleep(10s)的时间,这样在后面观察时比较明显。
go s.run()
模拟消费者,在这个函数中会从chVal中一致取数据,并且将取出的数据打印
type service struct {
chExit chan interface{}
wg *sync.WaitGroup
chVal chan int64
}
func New() *service {
s := &service{
chExit: make(chan interface{}, 1),
wg: new(sync.WaitGroup),
chVal: make(chan int64, 100),
}
s.wg.Add(1)
go s.fetch()
s.wg.Add(1)
go s.run()
return s
}
1.3.2 必须的接口
service
需要实现Close
函数,用于在主程序检测到退出的signal
时调用,释放service
的资源。
func (s *service) Close() error {
logger.Printf("srv close ")
close(s.chExit)
s.wg.Wait()
return nil
}
1.3.3 其他的函数
fetch
函数
在往chVal中写数据时,用select实现了非阻塞的方式,当chVal缓冲区占满时,不会阻塞写入的过程。
检测了退出的chExit的值,生产者退出时关闭了chVal。
func (s *service) fetch() {
logger.Printf("fetch start ")
defer func() {
logger.Printf("fetch exit")
s.wg.Done()
close(s.chVal)
}()
for {
select {
case <-s.chExit:
return
default:
time.Sleep(_fetch)
select {
case s.chVal <- _count:
_count++
default:
}
}
}
}
run
函数
从chVal中取数据时,直接遍历了chVal,当胶合的channel关闭时,这个for循环自动退出。
func (s *service) run() {
logger.Printf("run started ")
defer func() {
logger.Printf("run exit")
s.wg.Done()
}()
for val := range s.chVal {
logger.Printf("catch value=[%d]", val)
}
}
- main函数
检测signal的退出信号,收到退出信号时,调用srv的Close函数,释放资源。
func main() {
logger = log.New(os.Stdout, "", log.Lshortfile|log.Ldate|log.Ltime)
srv := New()
utils.HandlerExit(func(s os.Signal) int {
logger.Printf("receive get a signal %s", s.String())
if err := srv.Close(); err != nil {
logger.Printf(" service exit, error: %s", err)
return 1
}
logger.Printf(" service exit")
return 0
})
}
1.3.4 运行的结果
程序启动后,生产者和消费者都开始工作。收到退出的信号后,服务一直等到模拟生产的sleep结束后才退出,保证生产的数据和消费的数据都能够正常的完成处理。
2.http-service服务
2.1 存在的问题
先简单写一个基于gin的http服务,在这个服务中给请求返回一个hello world
的字符串,并且为了后面方便查看问题,在返回响应之前sleep(10s)的时间。
当页面或者rest客户端请求该uri时,rest的service收到了退出的信号,service开始逐步的释放资源。此时对http的service执行了硬关闭,对于未处理完的任务会出现不可预见得到错误,我们希望在退出时能够对未处理完的任务正常返回且做一些服务中断前的必要的操作,防止资源泄露。
2.2 错误的示范
2.2.1 代码示例
在这个示例中,定义了gin默认的router后直接开始在地址4400上监听。
func main() {
logger = log.New(os.Stdout, "", log.Lshortfile|log.Ldate|log.Ltime)
router := gin.Default()
router.GET("/", handleRoot())
add := ":4400"
go func() {
if err := router.Run(add); err != nil {
logger.Fatalf("listen addr=[]failed,%v", err)
return
}
}()
utils.HandlerExit(func(s os.Signal) int {
logger.Printf("receive get a signal %s", s.String())
logger.Printf(" service exit")
return 0
})
}
func handleRoot() gin.HandlerFunc {
return func(context *gin.Context) {
logger.Printf("rcv handle root")
time.Sleep(time.Second * 10)
context.String(
http.StatusOK,
"hello world",
)
}
}
2.2.2 异常的问题
启动后台服务,正常的情况下,在浏览器中请求http://localhost:4400
,等待10s后会打印hello world
的字样。但是现在在页面请求之后,后台程序退出的情况下,并不是预期的,正常的返回数据。页面返回的结果,如下图:
2.2.3 正确的方法
利用go的net/http包来监听地址,并且保存监听后返回的srv句柄。在收到signal的退出信号后,再调用srv的关闭函数,正常关闭服务。
- 获取service的httpservice句柄
srvHttp := &http.Server{
Addr: add,
Handler: router,
}
go func() {
if err := srvHttp.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("listen addr=[]failed,%v", err)
}
}()
- 退出时调用srv的shutdown函数
defer cancle()
if err := srvHttp.Shutdown(ctx); err != nil {
logger.Printf(" http service shutdown failed,%v", err)
return 0
}
logger.Printf(" service exit")
2.3.5 运行的结果
当页面正在请求数据时,后台收到了停止指令,srv会等待所有的请求数据返回后再退出。
3.具体的业务场景
在多个模块的情况下,将上述两种柔和到一起就可以完全实现程序的优雅的退出。
3.1 消费者1和生产者1的场景下
在第一节业务处理服务以一个生产者和一个消费者的形式做了示例,在一个生产者和消费者的情况下这么写是没有问题的。但是当存在多个生产者时在退出服务时会发生异常。报错:panic: send on closed channel,panic: close of closed channel。这是因为在生产者多个生产者时,生产者1退出时关闭了chVal,生产者2在等待结束后写数据时向已经关闭的channel上写了数据,导致错发生。
究其原因,我们是在没有等生产者全部退出后再关闭chVal,导致崩溃的发生。下面分别就这个核心问题提出几个解决方案。
3.2 消费者n和生产者n的场景下
3.2.1 多个waitGroup
利用waitGroup的wait特性,给生产者和消费者定义各自的变量,等待生产者全部退出后再关闭channel,这样就不会有上述的报错了。
分别定义多个waitGroup
type service struct {
chExit chan interface{}
cwg *sync.WaitGroup // 消费者的waitGroup
pwg *sync.WaitGroup // 生产者的waitGroup
chVal chan int64
}
启动多个生产者(消费者同理)
var i int64 = 0
for i = 0; i < 2; i++ {
s.cwg.Add(1)
go s.fetch(i)
}
s.pwg.Add(1)
go s.run()
Close函数中等待生产者全部退出后再关闭chVal的channel
func (s *service) Close() error {
logger.Printf("srv close ")
close(s.chExit)
s.cwg.Wait()
close(s.chVal)
s.pwg.Wait()
return nil
}
这样是可以解决这个问题的,运行结果如下:
3.2.2 还有啥办法啊