1. 背景

  项目开发过程中,随着需求的迭代,代码的发布会频繁进行,在发布过程中,如何让程序做到优雅的退出?

为什么需要优雅的退出?

  • 你的 http 服务,监听端口没有关闭,客户的请求发过来了,但处理了一半,可能造成脏数据。
  • 你的协程 worker 的一个任务运行了一半,程序退出了,结果不符合预期。

如下我们以 http 服务,gRPC 服务,单独的 woker 协程为例子,一步步说明平滑关闭的写法。

2. 常见的几种平滑关闭

为了解决退出可能出现的潜在问题,平滑关闭一般做如下一些事情

  • 关闭对外的监听端口,拒绝新的连接
  • 关闭异步运行的协程
  • 关闭依赖的资源
  • 等待如上资源关闭
  • 然后平滑关闭

2.1 http server 平滑关闭

原来的写法

  1. // startHttpServer start http server
  2. func startHttpServer() {
  3. mux := http.NewServeMux()
  4. // mux.Handle("/metrics", promhttp.Handler())
  5. if err := http.ListenAndServe(":1608", mux); err != nil {
  6. log.Fatal("startHttpServer ListenAndServe error: " + err.Error())
  7. }
  8. }

  

带平滑关闭的写法

  1. // startHttpServer start http server
  2. func startHttpServer() {
  3. mux := http.NewServeMux()
  4. // mux.Handle("/metrics", promhttp.Handler())
  5. srv := &http.Server{
  6. Addr: ":1608",
  7. Handler: mux,
  8. }
  9. // 注册平滑关闭,退出时会调用 srv.Shutdown(ctx)
  10. quit.GetQuitEvent().RegisterQuitCloser(srv)
  11. if err := srv.ListenAndServe(); err != nil {
  12. log.Fatal("startHttpServer ListenAndServe error: " + err.Error())
  13. }
  14. }

把平滑关闭注册到http.Server的关闭函数中

  1. // startHttpServer start http server
  2. func startHttpServer() {
  3. mux := http.NewServeMux()
  4. // mux.Handle("/metrics", promhttp.Handler())
  5. srv := &http.Server{
  6. Addr: ":1608",
  7. Handler: mux,
  8. }
  9. // 把平滑退出注册到http.Server中
  10. srv.RegisterOnShutdown(quit.GetQuitEvent().GracefulStop)
  11. if err := srv.ListenAndServe(); err != nil {
  12. log.Fatal("startHttpServer ListenAndServe error: " + err.Error())
  13. }
  14. }

  

2.2 gRPC server 平滑关闭

原来的写法

  1. // startGrpcServer start grpc server
  2. func startGrpcServer() {
  3. listen, err := net.Listen("tcp", "0.0.0.0:9999")
  4. if err != nil {
  5. log.Fatalf("Failed to listen: %v", err)
  6. return
  7. }
  8. grpcServer := grpc.NewServer()
  9. // helloBoot.GrpcRegister(grpcServer)
  10. go grpcServer.Serve(listen)
  11. defer grpcServer.GracefulStop()
  12. // ...
  13. }

带平滑关闭的写法 

  1. // startGrpcServer start grpc server
  2. func startGrpcServer() {
  3. listen, err := net.Listen("tcp", "0.0.0.0:9999")
  4. if err != nil {
  5. log.Fatalf("Failed to listen: %v", err)
  6. return
  7. }
  8. grpcServer := grpc.NewServer()
  9. // helloBoot.GrpcRegister(grpcServer)
  10. go grpcServer.Serve(listen)
  11. // 把 grpc 的GracefulStop注册到退出事件中
  12. quit.GetQuitEvent().RegisterStopFunc(grpcServer.GracefulStop)
  13. quit.WaitSignal()
  14. }

  

2.3 worker 协程平滑关闭

单独的协程启停,可以通过计数的方式注册到退出事件处理器中。

  • 启动协程 增加计数

    • quit.GetQuitEvent().AddGoroutine()
  • 停止协程 减计数 
    • quit.GetQuitEvent().DoneGoroutine()
  • 常驻后台运行的协程退出的条件改成退出事件是否结束的条件 
    • !quit.GetQuitEvent().HasFired()
  • 常驻后台运行的协程若通过 select 处理 chan,同时增加退出事件的chan
    • case <-quit.GetQuitEvent().Done()
  1. // myWorker my worker
  2. type myWorker struct {
  3. }
  4.  
  5. // RunWorkerWithChan run Goroutine worker
  6. func (m *myWorker) RunWorkerWithChan() {
  7. // 启动一个Goroutine时,增加Goroutine数
  8. quit.GetQuitEvent().AddGoroutine()
  9. defer func() {
  10. // 一个Goroutine退出时,减少Goroutine数
  11. quit.GetQuitEvent().DoneGoroutine()
  12. }()
  13. // 退出时,此次退出
  14. for !quit.GetQuitEvent().HasFired() {
  15. select {
  16. // 退出时,收到退出信号
  17. case <-quit.GetQuitEvent().Done():
  18. break
  19. //case msg := <- m.YouChan:
  20. // handle msg
  21. }
  22. }
  23. }
  24.  
  25. // RunWorker run Goroutine worker
  26. func (m *myWorker) RunWorker() {
  27. // 启动一个Goroutine时,增加Goroutine数
  28. quit.GetQuitEvent().AddGoroutine()
  29. defer func() {
  30. // 一个Goroutine退出时,减少Goroutine数
  31. quit.GetQuitEvent().DoneGoroutine()
  32. }()
  33.  
  34. // 退出时,此次退出
  35. for !quit.GetQuitEvent().HasFired() {
  36. // ...
  37. }
  38. }

  

2.4 实现 io.Closer 接口的自定义服务平滑关闭

实现 io.Closer 接口的结构体,增加到退出事件处理器中 

  1. // startMyService start my service
  2. func startMyService() {
  3. srv := NewMyService()
  4. // 注册平滑关闭,退出时会调用 srv.Close()
  5. quit.GetQuitEvent().RegisterCloser(srv)
  6. srv.Run()
  7. }
  8.  
  9. // myService my service
  10. type myService struct {
  11. isStop bool
  12. }
  13.  
  14. // NewMyService new
  15. func NewMyService() *myService {
  16. return &myService{}
  17. }
  18.  
  19. // Close my service
  20. func (m *myService) Close() error {
  21. m.isStop = true
  22. return nil
  23. }
  24.  
  25. // Run my service
  26. func (m *myService) Run() {
  27. for !m.isStop {
  28. // ....
  29. }
  30. }

  

2.5 集成其他框架怎么做

退出信号处理由某一框架接管,寻找框架如何注册退出函数,优秀的框架一般都会实现安全实现退出的机制。

如下将退出事件注册到某一框架的平滑关闭函数中

  1. func startMyServer() {
  2. // ...
  3. // xxx框架退出函数注册退出事件
  4. xxx.RegisterQuitter(func() {
  5. quit.GetQuitEvent().GracefulStop()
  6. })
  7. }

参考:

https://github.com/mygityf/go-library/blob/main/quit/quit.go

完。

祝玩的开心~

【Golang】程序如何优雅的退出?的更多相关文章

  1. 如何优雅地退出python程序

    如何优雅地退出python程序 一个单模的python程序,启动之后要能够优雅地关闭.即当用户按Ctrl+C或者kill pid的时候,程序都能从容关闭.实现起来非常简单. is_running = ...

  2. C# Note11:如何优雅地退出WPF应用程序

    前言 I should know how I am supposed to exit my application when the user clicks on the Exit menu item ...

  3. 从nsq中学习如何优雅的退出go 网络程序

    退出运行中的程序,可以粗暴的kill -9 $PID,但这样会破坏业务的完整性,有可能一个正在在执行的逻辑半途而费,从而产生不正常的垃圾数据. 本文总结在go语言中,如何能优雅的退出网络应用,涉及的知 ...

  4. Android程序的隐藏与退出

    转自Android程序的隐藏与退出 Android的程序无需刻意的去退出,当你一按下手机的back键的时候,系统会默认调用程序栈中最上层Activity的Destroy()方法来销毁当前Activit ...

  5. 情景linux--如何优雅地退出telnet

    情景linux--在脚本中如何优雅地退出telnet 情景 telnet命令是TELNET协议的用户接口,它支持两种模式:命令模式和会话模式.虽然telnet支持许多命令,但大部分情况下,我们只是使用 ...

  6. python 如何优雅地退出子进程

    python 如何优雅地退出子进程 主进程产生子进程,子进程进入永久循环模式.当主进程要求子进程退出时,如何能安全地退出子进程呢? 参考一些代码,我写了这个例子.运行之后,用kill pid试试.pi ...

  7. 在MacOS上使用gdb(cgdb)调试Golang程序

    如果你在MacOS上使用GDB工具载入Golang程序时无法载入,这篇文章可以解决.本文不具体介绍调试的方法,网上的文章太多了就不赘述了. cgdb使用的是gdb的内核,方法和原理试用本文. 问题分析 ...

  8. 如何优雅的退出/关闭/重启gunicorn进程

    在工作中,会发现gunicorn启动的web服务,无论怎么使用kill -9 进程号都是无法杀死gunicorn,经过我一番百度和谷歌,发现想要删除gunicorn进程其实很简单. 1. 寻找mast ...

  9. CentOS7中_带sqlite3_CGO的golang程序_交叉编译到arm中

    CentOS7中_带sqlite3_CGO的golang程序_交叉编译到arm中 转载注明来源: 本文链接 来自osnosn的博客,写于 2019-10-28. 编写了个golang程序,用到了这个C ...

随机推荐

  1. Dockerfile入门

    1.Dockerfile介绍 在之前Docker的使用中,我们直接从仓库下载需要的镜像到本地,然后稍加配置就可以应用了,通常从仓库下载下来的镜像都是通用的,无任何私有化的东西,我们拿过来就需要加很多的 ...

  2. Mybatis插入数据

    对上文->Mybatis快速入门-<进行代码修改 1.在UserMapper.xml中添加插入操作 <!-- 插入操作--> <insert id="save& ...

  3. Struts2封装获取表单数据方式

    一.属性封装 1.创建User实体类` package cn.entity; public class User { private String username; private String p ...

  4. node.js -- 身份认证

    请问昨天结束的早是对堆积在了今天吗,今天还来加个班更博,看在这个毅力的份上能否给亿点点推荐. 有个好消息有个坏消息,先说坏消息吧,就是在这么学下去我急需急支糖浆,来回顾回顾前面的知识,这几天学的太急了 ...

  5. 攻防世界-MISC:stegano

    这是攻防世界新手练习区的第五题,题目如下: 点击附件1下载,得到一个pdf文件,打开后内容如下: 把pdf文件里的内容复制到记事本上,发现一串A和B的字符串,不知道是什么(真让人头大) 参考一下WP, ...

  6. XCTF练习题---MISC---a_good_idea

    XCTF练习题---MISC---a_good_idea flag:NCTF{m1sc_1s_very_funny!!!} 解题步骤: 1.观察题目,下载附件 2.到手以后发现是一张图片,尝试修改文件 ...

  7. 下载并配置pycharm

    1.下载(推荐下载社区版) https://www.jetbrains.com/pycharm/download/#section=windows 2.配置代码编写前注释 得到这种效果: 3.设置字体 ...

  8. 虚拟机(Vmvare)与配置,得到一台学习机

    准备: 1.Vmvare 2.CentOS7.4镜像 安装与配置操作系统: 1.配置虚拟机上网 2.配置静态ip地址 开始安装 1. 2.直接下一步选择我们准备好的镜像,然后下一步 3.修改虚拟机的名 ...

  9. MongoDB 常用运维实践总结

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 一.MongoDB 集群简介 MongoDB是一个基于分布式文件存储的数据库,其目的在于为WE ...

  10. BootstrapBlazor实战 Markdown 编辑器使用

    基础工程使用工程: B08. BootstrapBlazor实战 Menu 导航菜单使用 实战BootstrapBlazorMenu Markdown 编辑器使用, 以及整合Freesql orm快速 ...