关于平滑发版,紫月写过一篇博客《软件工程实践之平滑发版》,其中讲解了为什么需要平滑发版以及服务中如何实现。总结下来,平滑发版中会经历的几个步骤:
- 启动新进程,旧进程停止监听请求,新请求由新进程监听
- 旧进程等待已连接的请求结束
- 待请求全部执行完毕或超过超时时间,关闭进程
而进程什么时候停止监听,什么时候关闭进程,则可以通过Signal来实现。
以k8s为例,在执行kubectl rollout restart后,
- 旧pod会接收到SIGTERM信号,pod status变为Terminating
- 旧pod等待处理的中的请求结束
- 待处理中的请求结束或超过预设的terminationGracePeriodSeconds时间
- 旧pod接收到SIGKILL信号后被移除
一般来说,通过守护进程进行部署的方式,只需要正确处理Signal,即可实现服务业务平滑发版,例如最近整的项目中,由四部分组成:
其中machinery本身就实现了gracefully shutdown,调用api即可,后文将依次介绍其他三个部分如何实现平滑发版。
gin
Gin官方提供了gracefully shutdown的例子,代码如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 
 | package main
 import (
 "context"
 "log"
 "net/http"
 "os"
 "os/signal"
 "syscall"
 "time"
 
 "github.com/gin-gonic/gin"
 )
 
 func main() {
 router := gin.Default()
 router.GET("/", func(c *gin.Context) {
 time.Sleep(5 * time.Second)
 c.String(http.StatusOK, "Hello World")
 })
 
 srv := &http.Server{
 Addr:    ":8080",
 Handler: router,
 }
 go func() {
 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 log.Fatalf("listen: %s\n", err)
 }
 }()
 
 quit := make(chan os.Signal)
 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 <-quit
 log.Println("Shutting down server...")
 
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()
 if err := srv.Shutdown(ctx); err != nil {
 log.Fatal("Server forced to shutdown:", err)
 }
 log.Println("Server exiting")
 }
 
 | 
例子中通过主goroutine通过signal.Notify()监听, <-quit进行阻塞,另外一个goroutine启动http server,当接收到SIGINT或SIGTERM信号后,http.ListenAndServe()返回http.ErrServerClosed,此时httpserver不再接受新请求,并创建一个超时时间为5s的context,通过http.Server的Shutdown,并阻塞等待连接释放或context超时。
grpc-go
grpc-go中提供了GracefulStop,我们只需要监听Signal,接受到后,由GracefulStop来处理连接中的请求
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | package main
 import (
 "log"
 "net"
 "os"
 "os/signal"
 "syscall"
 
 "google.golang.org/grpc"
 )
 
 func main() {
 listen, err := net.Listen("tcp", ":8080")
 if err != nil {
 panic(err)
 }
 server := grpc.NewServer()
 
 
 go func() {
 if err = server.Serve(listen); err != nil {
 panic(err)
 }
 }()
 quit := make(chan os.Signal, 1)
 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 <-quit
 log.Println("Shutting down gRPC server...")
 server.GracefulStop()
 log.Println("gRPC server exiting...")
 }
 
 | 
cron
robfig/cron包中虽然没提供关于gracefully shutdown的内容,至少提供了Start以及Stop方法,按照前几个实现的套路,我们可以自己动手实现一下gracefully shutdown。因为cron.Start本身会新起一个的goroutine启动server,所以我们在主goroutine中监听signal,在收到Signal后,执行cron.Stop(),而cron.Stop()会返回一个context,后续通过select监听context结束或者通过time.After等待超时,待当前所有定时任务结束后,进程结束。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
 | package main
 import (
 "log"
 "os"
 "os/signal"
 "syscall"
 "time"
 )
 
 func main() {
 cronjob := cron.New()
 cronjob.AddFunc("@every 5s", func() { time.Sleep(10 * time.Second) })
 cronjob.Start()
 
 quit := make(chan os.Signal, 1)
 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 <-quit
 ctx := cronjob.Stop()
 log.Println("Shutting down cron...")
 select {
 case <-time.After(10 * time.Second):
 log.Fatal("Cron forced to shutdown...")
 case <-ctx.Done():
 log.Println("Cron exiting...")
 }
 }
 
 | 
小结
本文只介绍了golang中如何实现平滑发版,一般框架都会给出关于gracefully shutdown的api,没有也没关系,套路就是正常启动server后,通过监听Signal并阻塞主goroutine,待接收到SIGNTERM等信号后,通过time.After或context.WithTimeout,作为超时处理,并等待进行中的goroutine全部结束。有兴趣可以查看各个类库中如何等待处理进行中的连接。