关于平滑发版,紫月写过一篇博客《软件工程实践之平滑发版》,其中讲解了为什么需要平滑发版以及服务中如何实现。总结下来,平滑发版中会经历的几个步骤:
- 启动新进程,旧进程停止监听请求,新请求由新进程监听
- 旧进程等待已连接的请求结束
- 待请求全部执行完毕或超过超时时间,关闭进程
而进程什么时候停止监听,什么时候关闭进程,则可以通过Signal来实现。
以k8s为例,在执行kubectl rollout restart
后,
- 旧pod会接收到SIGTERM信号,pod status变为Terminating
- 旧pod等待处理的中的请求结束
- 待处理中的请求结束或超过预设的
terminationGracePeriodSeconds
时间
- 旧pod接收到SIGKILL信号后被移除
一般来说,通过守护进程进行部署的方式,只需要正确处理Signal,即可实现服务业务平滑发版,例如最近整的项目中,由四部分组成:
其中machinery本身就实现了gracefully shutdown,调用api即可,后文将依次介绍其他三个部分如何实现平滑发版。
gin
Gin官方提供了gracefully shutdown的例子,代码如下
1 2 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来处理连接中的请求
1 2 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等待超时,待当前所有定时任务结束后,进程结束。
1 2 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全部结束。有兴趣可以查看各个类库中如何等待处理进行中的连接。