一文讲懂服务的优雅重启和更新

开发 架构
在重启过程中,会有一段时间不能给用户提供正常服务;同时粗鲁关闭服务,也可能会对业务依赖的数据库等状态服务造成污染。所以我们服务重启或者是重新发布过程中,要做到新旧服务无缝切换,同时可以保障变更服务 零宕机时间!

[[404467]]

本文转载自微信公众号「微服务实践」,作者hxl。转载本文请联系微服务实践公众号。   

在服务端程序更新或重启时,如果我们直接 kill -9 杀掉旧进程并启动新进程,会有以下几个问题:

  1. 旧的请求未处理完,如果服务端进程直接退出,会造成客户端链接中断(收到 RST)
  2. 新请求打过来,服务还没重启完毕,造成 connection refused
  3. 即使是要退出程序,直接 kill -9 仍然会让正在处理的请求中断

很直接的感受就是:在重启过程中,会有一段时间不能给用户提供正常服务;同时粗鲁关闭服务,也可能会对业务依赖的数据库等状态服务造成污染。

所以我们服务重启或者是重新发布过程中,要做到新旧服务无缝切换,同时可以保障变更服务 零宕机时间!

作为一个微服务框架,那 go-zero 是怎么帮开发者做到优雅退出的呢?下面我们一起看看。

优雅退出

在实现优雅重启之前首先需要解决的一个问题是 如何优雅退出:

对 http 服务来说,一般的思路就是关闭对 fd 的 listen , 确保不会有新的请求进来的情况下处理完已经进入的请求, 然后退出。

go 原生中 http 中提供了 server.ShutDown(),先来看看它是怎么实现的:

  1. 设置 inShutdown 标志
  2. 关闭 listeners 保证不会有新请求进来
  3. 等待所有活跃链接变成空闲状态
  4. 退出函数,结束

分别来解释一下这几个步骤的含义:

inShutdown

  1. func (srv *Server) ListenAndServe() error { 
  2.     if srv.shuttingDown() { 
  3.         return ErrServerClosed 
  4.     } 
  5.     .... 
  6.     // 实际监听端口;生成一个 listener 
  7.     ln, err := net.Listen("tcp", addr) 
  8.     if err != nil { 
  9.         return err 
  10.     } 
  11.     // 进行实际逻辑处理,并将该 listener 注入 
  12.     return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) 
  13.  
  14. func (s *Server) shuttingDown() bool { 
  15.   return atomic.LoadInt32(&s.inShutdown) != 0 

ListenAndServe 是http启动服务器的必经函数,里面的第一句就是判断 Server 是否被关闭了。

inShutdown 就是一个原子变量,非0表示被关闭。

listeners

  1. func (srv *Server) Serve(l net.Listener) error { 
  2.     ... 
  3.     // 将注入的 listener 加入内部的 map 中 
  4.     // 方便后续控制从该 listener 链接到的请求 
  5.     if !srv.trackListener(&l, true) { 
  6.         return ErrServerClosed 
  7.     } 
  8.     defer srv.trackListener(&l, false
  9.    ... 

Serve 中注册到内部 listeners map 中 listener,在 ShutDown 中就可以直接从 listeners 中获取到,然后执行 listener.Close(),TCP四次挥手后,新的请求就不会进入了。

closeIdleConns

简单来说就是:将目前 Server 中记录的活跃链接变成变成空闲状态,返回。

关闭

  1. func (srv *Server) Serve(l net.Listener) error { 
  2.   ... 
  3.   for { 
  4.     rw, err := l.Accept() 
  5.     // 此时 accept 会发生错误,因为前面已经将 listener close了 
  6.     if err != nil { 
  7.       select { 
  8.       // 又是一个标志:doneChan 
  9.       case <-srv.getDoneChan(): 
  10.         return ErrServerClosed 
  11.       default
  12.       } 
  13.     } 
  14.   } 

其中 getDoneChan 中已经在前面关闭 listener 时,对 doneChan 这个channel中push。

总结一下:Shutdown 可以优雅的终止服务,期间不会中断已经活跃的链接。

但服务启动后的某一时刻,程序如何知道服务被中断了呢?服务被中断时如何通知程序,然后调用Shutdown作处理呢?接下来看一下系统信号通知函数的作用

服务中断

这个时候就要依赖 OS 本身提供的 signal。对应 go 原生来说,signal 的 Notify 提供系统信号通知的能力。

https://github.com/tal-tech/go-zero/blob/master/core/proc/signals.go

  1. func init() { 
  2.   go func() { 
  3.     var profiler Stopper 
  4.      
  5.     signals := make(chan os.Signal, 1) 
  6.     signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM) 
  7.  
  8.     for { 
  9.       v := <-signals 
  10.       switch v { 
  11.       case syscall.SIGUSR1: 
  12.         dumpGoroutines() 
  13.       case syscall.SIGUSR2: 
  14.         if profiler == nil { 
  15.           profiler = StartProfile() 
  16.         } else { 
  17.           profiler.Stop() 
  18.           profiler = nil 
  19.         } 
  20.       case syscall.SIGTERM: 
  21.         // 正在执行优雅关闭的地方 
  22.         gracefulStop(signals) 
  23.       default
  24.         logx.Error("Got unregistered signal:", v) 
  25.       } 
  26.     } 
  27.   }() 
  • SIGUSR1 -> 将 goroutine 状况,dump下来,这个在做错误分析时还挺有用的
  • SIGUSR2 -> 开启/关闭所有指标监控,自行控制 profiling 时长
  • SIGTERM -> 真正开启 gracefulStop,优雅关闭

而 gracefulStop 的流程如下:

  1. 取消监听信号,毕竟要退出了,不需要重复监听了
  2. wrap up,关闭目前服务请求,以及资源
  3. time.Sleep() ,等待资源处理完成,以后关闭完成
  4. shutdown ,通知退出
  5. 如果主goroutine还没有退出,则主动发送 SIGKILL 退出进程

这样,服务不再接受新的请求,服务活跃的请求等待处理完成,同时也等待资源关闭(数据库连接等),如有超时,强制退出。

整体流程

我们目前 go 程序都是在 docker 容器中运行,所以在服务发布过程中,k8s 会向容器发送一个 SIGTERM 信号,然后容器中程序接收到信号,开始执行 ShutDown:

到这里,整个优雅关闭的流程就梳理完毕了。

但是还有平滑重启,这个就依赖 k8s 了,基本流程如下:

  • old pod 未退出之前,先启动 new pod
  • old pod 继续处理完已经接受的请求,并且不再接受新请求
  • new pod接受并处理新请求的方式
  • old pod 退出

这样整个服务重启就算是成功了,如果 new pod 没有启动成功,old pod 也可以提供服务,不会对目前线上的服务造成影响。

项目地址

https://github.com/tal-tech/go-zero

欢迎使用 go-zero 并 star 支持我们!

 

责任编辑:武晓燕 来源: 微服务实践
相关推荐

2021-09-03 05:03:58

模块命令项目

2020-03-26 09:18:54

高薪本质因素

2019-10-12 08:59:36

软件DevOps技术

2019-09-23 10:51:14

JavaJava虚拟机Linux

2024-08-13 17:09:00

架构分库分表开发

2020-07-16 09:02:45

aPaaS云计算aPaaS平台

2020-08-04 10:56:09

进程线程协程

2020-12-03 08:23:23

函数柯里化代码

2022-02-15 08:38:04

错误逻辑异常编程程序

2023-11-09 08:41:25

DevOpsAIOps软件

2020-05-20 09:55:42

Git底层数据

2020-01-02 09:06:23

微服务数据框架

2021-01-18 13:05:52

Serverless Serverfull FaaS

2020-12-01 11:34:14

Elasticsear

2019-03-14 15:59:44

前端开发编程

2019-04-22 15:09:24

云计算KVMXEN

2018-05-10 10:53:47

分布式架构负载均衡Web

2019-09-12 10:25:39

系统WindowsLinux

2023-05-04 08:24:52

ChatGPT产品经理工业革命

2023-10-16 08:16:31

Bean接口类型
点赞
收藏

51CTO技术栈公众号