06 重启-优雅关闭

文章讲述了在服务集群环境下,如何优雅地关闭服务以避免数据丢失和用户体验影响。重点讨论了通过信号控制进程关闭,如在Go中捕获SIGINT和SIGTERM信号,以及使用server.Shutdown方法等待所有逻辑处理结束,确保服务无损关闭。
摘要由CSDN通过智能技术生成

重启服务,指的是一个关闭、启动进程的完成过程。

目前所有服务基本都无单点问题,都是集群化部署。对一个服务的关闭、启动进程来说,启动的流程基本上问题不大,可以由集群的统一管理器,比如 Kubernetes,来进行服务的启动,启动之后慢慢将流量引入到新启动的节点,整个服务是无损的。

但是在关闭服务的过程中,要考虑的情况就比较复杂了,比如说有服务已经在连接请求中怎么办?如果关闭服务的操作超时了怎么办?我们考虑的就是如何优雅关闭的问题。

如何优雅关闭

什么叫优雅关闭?你可以对比着想,不优雅的关闭比较简单,就是什么都不管,强制关闭进程,这明显会导致有些连接被迫中断

或许你并没有意识到这个问题的严重性,不妨试想下,当一个用户在购买产品的时候,由于不优雅关闭,请求进程中断,导致用户的钱包已经扣费了,但是商品还未进入用户的已购清单中。这就会给用户带来实质性的损失(就是说某些连续的操作只完成了一半)。

所以,优雅关闭服务,其实说的就是,关闭进程的时候,不能暴力关闭进程,而是要等进程中的所有请求都逻辑处理结束后,才关闭进程。按照这个思路,需要研究两个问题“如何控制关闭进程的操作” 和 “如何等待所有逻辑都处理结束”。

当我们了解了如何控制进程关闭操作,就可以延迟关闭进程行为,设置为等连接的逻辑都处理结束后,再关闭进程(核心:不理解结束进程,等所有连接的操作都完成,再结束)。

如何控制关闭进程的操作

平时关闭一个进程的方法:

  • Ctrl + C: 向进程发送SIGINT信号,信号可被阻塞和处理
  • Ctrl + : 向进程发送SIGQUIT信号,也可被阻塞和处理
  • kill命令: kill pid 会向进程发送 SIGTERM 信号,而 kill -9 会向进程发送 SIGKILL 信号。这两个信号都用于立刻结束进程,但是 SIGTERM 是可以被阻塞和处理的,而 SIGKILL 信号是不能被阻塞和处理的。

除了 SIGKILL 信号无法被捕获之外,其他的信号都能捕获,所以,只要在程序中捕获住这些信号,就能实现控制关闭进程操作了。

在go中如何捕获信号:

// 忽略某个信号
func Ignore(sig ...os.Signal){}
// 判断某个信号是否被忽略了
func Ignored(sig os.Signal) bool{}
// 关注某个/某些/全部 信号
func Notify(c chan<- os.Signal, sig ...os.Signal){}
// 取消使用 notify 对信号产生的效果
func Reset(sig ...os.Signal){}
// 停止所有向 channel 发送的效果
func Stop(c chan<- os.Signal){}
  • 订阅信号Notify:关注某些信号,一旦信号到来就会放入到chan中
  • 忽略信号Ignode:忽略某个信号
  • 停止所有订阅Stop
  • 重置Reset:

那么在主业务中监测信号可以这么写:

func main() {
	go func() {
		server.ListenAndServe()
	}()
	
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, system.SIGQUIT)
	<- quit
}

一般将监听signal的处理放在主goroutine中,便于处理。

如何等待所有逻辑都处理结束

go 1.8 版本之后,net/http 引入了 server.Shutdown 来进行优雅重启。

server.Shutdown 方法是个阻塞方法,一旦执行之后,它会阻塞当前 Goroutine,并且在所有连接请求都结束之后,才继续往后执行。其主要逻辑如下:

在这里插入图片描述

  1. 设置标记位:原子操作,设置后后续不再接受新请求
  2. 调用钩子函数:onShutdonw是一个钩子函数列表
  3. for循环不断判断所有连接是否都已结束:使用ticker计时,真正执行的是closeIdleConns 方法。
  4. closeIdleConns 判断所有连接中的请求是否已完成,是返回true,否返回false

// closeIdleConns 关闭所有的连接并且记录是否服务器的连接已经全部关闭
func (s *Server) closeIdleConns() bool {
  s.mu.Lock()
  defer s.mu.Unlock()
  quiescent := true
  for c := range s.activeConn {
    st, unixSec := c.getState()
    // Issue 22682: 这里预留5s以防止在第一次读取连接头部信息的时候超过5s
    if st == StateNew && unixSec < time.Now().Unix()-5 {
      st = StateIdle
    }
    if st != StateIdle || unixSec == 0 {
      // unixSec == 0 代表这个连接是非常新的连接,则标记位需要标记false
      quiescent = false
      continue
    }
    c.rwc.Close()
    delete(s.activeConn, c)
  }
  return quiescent
}

梳理之后,我们的代码可以这么写:


func main() {
  ...

  // 当前的 Goroutine 等待信号量
  quit := make(chan os.Signal)
  // 监控信号:SIGINT, SIGTERM, SIGQUIT
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
  // 这里会阻塞当前 Goroutine 等待信号
  <-quit

  // 调用Server.Shutdown graceful结束
  if err := server.Shutdown(context.Background()); err != nil {
    log.Fatal("Server Shutdown:", err)
  }
}

【小结】:

  1. 监测信号使用os.signal包
  2. net/http包内部的server.Shutdown阻塞等待左右连接关闭
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值