基于 net/http 抽象出 go 服务优雅停止的一般思路

和其他语言相比,Go 中有相同也有不同,相同的是实现思路上和其他语言没啥差异,不同在于 Go 采用的是 goroutine + channel 的并发模型,与传统的进程线程相比,实现细节上存在差异。

本文将从实际场景和它的一般实现方式展开,逐步讨论这个话题。

简介

什么是优雅停止?在谈优雅停止前,我们可以说说什么是优雅重启,或者说热重启。

简言之,优雅重启就是在服务升级、配置更新时,要重新启动服务,优雅重启就是在服务不中断或连接不丢失的情况下,重启服务。优雅重启的整个流程中,新的进程将在旧的进程停止前启动,旧进程会完成活动中的请求后优雅地关闭进程。

优雅重启是服务开发中一个非常重要的概念,它让我们在不中断服务的情况下,更新代码和修复问题。它在维持高可用性的生产环境中尤其关键。

从上面的这段可知,优雅重启是由两个部分组成,分别是优雅停止和启动。

本文重点介绍优雅停止,而优雅启动的整个流程要借助于外部工具控制,如 k8s 的容器编排。

优雅停止

优雅停止,即要在停止服务的同时,保证业务的完整性。从目标上看,优雅停止经历三个步骤:通知服务停止、服务启动清理,等待清理确认退出。

要停止一个服务,首先是通过一些机制告知服务要执行退出前的工作,最常见的就是基于操作系统信号,我们惯例监听的信号主要是两个,分别是由 kill PID 发出的 SIGTERM 和 CTRL+C 发出的 SIGINT。 其他信号还有,CTRL+/ 发出的 SIGQUIT。

当接收到指定信号,服务就要停止接受新的请求,且等待当前活动中的请求全部完成后再完全停止服务。

接下来,开始具体的代码实现部分吧。

从 HTTP 服务开始

谈优雅重启,最常被引用的案例就是 HTTP 服务,我将通过代码逐步演示这个过程。如下是一个常规 HTTP 服务:

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello World\n")
}

func main() {
  http.HandleFunc("/", hello)
  log.Println("Starting server on :8080")
  if err := http.ListenAndServe(":8080", nil); err != nil {
      log.Fatal("ListenAndServe: ", err)
  }
}

我们通过 time.Sleep 增加 hello 的耗时,以便于调试。

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello World\n")
  time.Sleep(10 * time.Second)
}

运行:

$ go run main.go

通过 curl 请求访问 http://localhost:8080/ ,它进入到 10 秒的处理阶段。假设这时,我们 CTRL+C 请求退出,HTTP 服务会直接退出,我们的 curl 请求被直接中断。

我们可以使用 Go 标准库提供的 http.Server 有一个 Shutdown 方法,可以安全地关闭服务器而不中断任何活动的连接。而我们要做的,只需在收到停止信号后,执行 Shutdown 即可。

信号方面,我们通过 Go 标准库 signal 实现,它提供了一个 Notify 函数,可与 chan nnel 配合传递信号消息。我们监听的目标信号是 SIGINTSIGTERM

重新修改 HTTP 服务入口,使用 http.ServerShutdown 函数关闭 Server

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", hello)

  server := http.Server{Addr: ":8080", Handler: mux}
  go server.ListenAndServe()
  
  quit := make(chan os.Signal, 1)
  // 注册接收信号的 channel
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 
  
  <-quit // 等待停止信号
  
  if err := server.Shutdown(context.Background()); err != nil {
    log.Fatal("Shutdown: ", err)
  }
}

我们将 server.ListenAndServe 运行于另一个 goroutine 中同时忽略了它的返回错误。

通过 signal.Notify 注册信号。当收到如 CTRL+C 或 kill PID 发出的中断信号,执行 serve.Shutdown,它会通知到 server 停止接收新的请求,并等待活动中的连接处理完成。

现在运行 go run main.go 启动服务,执行 curl 命令测试接口,在请求还没有返回之时,我们可以通过 CTRL+C 停止服务,它会有一段时间等待,我们可以在这个过程中尝试 curl 请求,看它是否还接收新的请求。

如果希望防止程序假死,或者其他问题导致服务长时间无法退出,可通过 context.WithTimeout 方法包装下传递给 Shutdown 方法的 ctx 变量。

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

if err := server.Shutdown(ctx); err != nil {
  log.Fatal("Shutdown: ", err)
}

到这里,我们就介绍完了 Go 标准库 net/http 的优雅停止的使用方案。

抽象出一个常规方案

如果开发一个非 HTTP 的服务,如何让它支持优雅停止呢?毕竟不是所有项目都是 HTTP 服务,不是所有项目都有现成的框架。

本文开头提到的的三步骤,net/http 包的 Shutdown 把最核心的服务停止前的清理和等待都已经在内部实现了。我们可解读下它的实现。

进入到 Shutdown 的源码中,重点是开头的第一句代码,如下所示:

// future calls to methods such as Serve will return ErrServerClosed.
func (srv *Server) Shutdown(ctx context.Context) error {
  srv.inShutdown.Store(true)
  // ...其他清理代码
  // ...等待活动请求完成并将其关闭
}

inShutdown 是一个标志位,用于标识程序是否已停止。为了解决并发数据竞争,它的底层类型是 atomic.bool,。

server.go 中的 Server.Serve 方法中,通过判断 inShutdown 决定是否继续接受新的请求。

func (srv *Server) Serve(l net.Listener)  error {
  // ...
  for {
    rw, err := l.Accept()
    if err != nil {
      if srv.shuttingDown() {
        return ErrServerClosed
      }
  // ...
}

我们可以从如上的分析中得知,要让 HTTP 服务支持优雅停止要启动两个 goroutine,Shutdown 运行与 main goroutine 中,当接收中停止信号,通过 inShutdown 标志位通知运行中的 goroutine。

用简化的代码表示这个一般模式。

var inShutdown bool

func Start() {
  for !inShutdown {
    // running
    time.Sleep(10 * time.Second)
  }
}

func Shutdown() {
  inShutdown = true
}

func main() {
  go Start()

  quit = make(chan os.Signal, 1)
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
  <- quit

  Shutdown()
}

大概看起来是那么回事,但这里的代码少了一个步骤,即 Shutdown 没有等待 Start 完成。

标准库 net/http 是通过 for 循环不断检查是否有活动中的连接,如果连接没有进行中请求会将其关闭,直到将所有连接关闭,便会退出 Shutdown

核心代码如下:

func (srv *Server) Shutdown(ctx context.Context) {
  // ...之前的代码

  timer := time.NewTimer(nextPollInterval())
  defer timer.Stop()
  for {
    if srv.closeIdleConns() {
      return lnerr
    }
    select {
    case <-ctx.Done():
      return ctx.Err()
    case <-timer.C:
      timer.Reset(nextPollInterval())
    }
  }
}

重点就是那句 closeIdleConns,它负责检查是否还有执行中的请求。我就不把这部分的源代码贴出来了。而检查频率是通过 timer 控制的。

现在让简化版等待 Start 完成后才退出。我们引入一个名为 isStop 的标志位以监控停止状态。

var inShutdown bool
var isStop bool

func Start() {
  for !inShutdown {
    // running
    time.Sleep(10 * time.Second)
  }
  isStop = true
}

func Shutdown() {
  inShutdown = true

  timer := time.NewTimer(time.Millisecond)
  defer timer.Stop()
  for {
    if isStop {
      return
    }
    <- timer.C
    timer.Reset(time.Millisecond))
  }
}

如上的代码中,Start 函数退出时会执行 isStop = true 表明已退出,在 Shutdown 中,通过定期检查 isStop 等待 Start 退出完成。

此外,net/httpShutdown 方法还接收了一个 context.Context 参数,允许实现超时控制,从而防止程序假死或强制关闭。

需要特别指出的是,示例中用的 isStop 和 inShutdown 标志位为非原子类型,在正式场景中,为避免数据竞争,要使用原子操作或其他同步机制。

除了用共享内存标志位在不同进程间传递状态,也可以通过 channel 实现,或你看到过类似如下的形式。

var inShutdown bool

func Start(stop chan struct{}) {
	for !inShutdown {
		// running
		time.Sleep(10 * time.Second)
	}
	stop <- struct{}{}
}

func Shutdown() {
	inShutdown = true
}

func main() {
	stop := make(chan struct{})
	defer close(stop)

	go Start(stop)

	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
		<-quit
		Shutdown()
	}()

	<-stop
}

如上的代码中,Start 通过 channel 通知主 goroutine,当触发停止信号,isShutdown 通知 Start 要停止退出,它成功退出后,通过 stop <- struct{} 通知主函数,结束等待。

总的来说,channel 的优势很明显,避免了单独管理一个 isStop 标志位来标识服务状态,并且免去了基于定时器的定期轮询检查的过程,还更加实时和高效。当然,net/http 使用轮询检查机制,是它的场景所决定,和我们这里不完全一样。

一点思考

Go 语言支持多种方式在 Goroutine 间传递信息,这催生了多样的优雅停止实现方式。如果是在涉及多个嵌套 Goroutine 的场景中,我们可以引入 context 来实现多层级的状态和信息传递,确保操作的连贯性和安全性。

然尽管实现方式众多,但其核心思路是一致的,而底层目标始终是我们要保证处理逻辑的完整性。

另外,通过将优雅停止与容器编排技术结合,并为服务添加健康检查,我们能够确保总有服务处于可用状态,实现真正意义上的优雅重启。这不仅提高了服务的可靠性,也优化了资源的利用效率。

总结

本文探索了 Go 语言中优雅重启的实现方法,展示了如何通过 http.Server 的 Shutdown 方法安全地重启服务,以及使用 context 控制超时。基于此,我们抽象出了一般服务优雅停止的核心思路。

最后,希望本文对你有所帮助,感谢关注我的公众号:微信搜索 “码途漫漫”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/543008.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯物联网竞赛_STM32L071KBU6_全部工程及国赛省赛真题及代码

包含stm32L071kbu6全部实验工程、源码、原理图、官方提供参考代码及国、省赛真题及代码 链接&#xff1a;https://pan.baidu.com/s/1pXnsMHE0t4RLCeluFhFpAg?pwdq497 提取码&#xff1a;q497

3D室内装潢设计 Sweet Home 3D for Mac 中文直装版

Sweet Home 3D 是一款非常棒的家装辅助设计软件&#xff0c;支持包括中文在内的16中语言&#xff0c;它能帮您通过二维的家居平面图来设计和布置您的家具,还可以用三维的视角浏览整个装修布局的全貌。是一款操作起来简单方便&#xff0c;使用起来快捷、迅速&#xff0c;拥有超高…

在Mac主机上连接Linux虚拟机

前言 最近醉心于研究Linux&#xff0c;于是在PD上安装了一个Debian Linux虚拟机&#xff0c;用来练练手。但是每次在mac和Linux之间切换很是麻烦&#xff0c;有没有一种方法&#xff0c;可以在mac终端直接连接我的虚拟机&#xff0c;这样在mac终端上就可以直接操控我的Linux虚…

Redis之路系列(1)千里之行始于足下

01 千里之行始于足下 文章内容基于redis6 安装与运行 无论你一名极客还是一名工程师&#xff0c;Redis安装我都推荐源码安装&#xff0c;请前往官方下载地址&#xff1a;http://redis.io/download 进行源码下载&#xff0c;偶数为稳定版 奇数为不稳定版。 如果你是类linux系统…

传统图机器学习的特征工程-全图

将整张图表示成为一个低维向量&#xff0c;反映全图的特征 key idea&#xff1a;Bag-of-Words&#xff08;BOW&#xff09;把图看作文章&#xff0c;把节点看作单词 Kernel mothods

【python】python汽车之家数据抓取分析可视化(代码+报告+数据)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

盲人独立出行的新里程:“盲人软件”赋能无障碍生活

作为一名资深记者&#xff0c;我始终致力于探索并分享那些以科技之力提升特殊群体生活质量的故事。最近&#xff0c;一款名为蝙蝠避障的盲人软件进入了我的视野&#xff0c;其强大的避障导航功能正悄然改变着视障人士的出行方式&#xff0c;赋予他们前所未有的独立生活能力。 …

小贴士:Windows下docker挂载目录填坑记录

为便于代码调试和维护&#xff0c;一般在生成 Docker 容器时&#xff0c;会将宿主机的目录挂载到容器里。在macOS里使用终端运行 Shell 脚本&#xff0c;调试一切正常&#xff0c;但是在 Windows 里使用 Git Bash 终端运行同样的脚本时&#xff0c;发现挂载失败。 1 问题描述 …

【产品经理修炼之道】- 融资租赁相关业务介绍

一、什么是融资租赁&#xff1f; 根据《民法典》第735条的规定&#xff0c;融资租赁合同是出租人根据承租人对出卖人、租赁物的选择&#xff0c;向出卖人购买租赁物&#xff0c;提供给承租人使用&#xff0c;承租人支付租金的合同。 例如&#xff0c;A工厂因业务发展需要欲购置…

赋能Web3用户:增强在线隐私

随着数字化时代的发展&#xff0c;人们越来越依赖互联网来进行各种活动&#xff0c;从社交互动到金融交易&#xff0c;几乎所有的日常生活都离不开网络。然而&#xff0c;随之而来的是个人隐私安全面临的挑战。在传统的互联网架构下&#xff0c;用户的个人数据往往被中心化的平…

971: 统计利用先序遍历创建的二叉树的深度

解法&#xff1a; 1.先序遍历创建二叉树链表形式 2.求二叉树的深度 用后序遍历实现&#xff1a; 1.后序遍历求节点A左右子树高度 2.对节点A&#xff1a; 1.取左右子树较大高度 2.返回高度1&#xff08;即以节点A为根节点的子树的最大深度&#xff09; 例如 #include <ios…

fast_bev 学习笔记

目录 一. 简述二. 输入输出三. github资源四. 复现推理过程4.1 cuda tensorrt 版 训练修改图像数 一. 简述 原文:Fast-BEV: A Fast and Strong Bird’s-Eye View Perception Baseline FAST BEV是一种高性能、快速推理和部署友好的解决方案&#xff0c;专为自动驾驶车载芯片设计…

python画神经网络图

代码1(画神经网络连接图&#xff09; from math import cos, sin, atan import matplotlib.pyplot as plt # 注意这里并没有用到这个networkx这个库&#xff0c;完全是根据matploblib这个库来画的。 class Neuron():def __init__(self, x, y,radius,nameNone):self.x xself.y …

为什么要部署IP SSL证书?怎么申请?

我们需要知道什么是IP SSL证书。SSL&#xff0c;全称为Secure Sockets Layer&#xff0c;即安全套接层&#xff0c;是为网络通信提供安全及数据完整性的一种安全协议。而IP SSL证书就是基于SSL协议的一种证书&#xff0c;它能够为网站和用户的数据传输提供加密处理&#xff0c;…

《深入浅出Spring Boot 3.x》正式出版了一周

各位&#xff0c;我编写的《深入浅出Spring Boot 3.x》已经正式发版了。 目前在京东已经开始销售了&#xff0c;希望有需要的朋友多多支持哦。 尽量采用Java 8后的语法编写&#xff0c;采用JDK 17去使用Jarkata EE 9。 相关内容如下&#xff1a;

“木偶猴帝国”渐起,BRC-20生态Meme币PUPS成为新星

比特币生态中基于BRC20协议的木偶猴代币PUPS在最近一周内价格暴涨1101%&#xff0c;达到了44.56美元&#xff0c;而其他一些BRC20代币&#xff0c;如WZRD、W☮、ZBIT、$π等也经历了显著的价格上涨&#xff0c;带动交易量攀升。 PUPS正在形成一种图币新玩法&#xff0c;与木偶…

最优二叉搜索树的设计与分析

最优二叉搜索树的设计与分析 引言最优二叉搜索树的定义构建最优二叉搜索树的算法算法步骤伪代码C代码示例总结 引言 在计算机科学中&#xff0c;二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是一种非常重要的数据结构&#xff0c;它允许我们高效…

使用python编写网页自动答题-仿真考试

自动化实践经验分享 监听数据包地址&#xff1a;通过监听数据包地址&#xff0c;可以获得实时的答案信息&#xff0c;确保答题的准确性和实效性。提取答案内容&#xff1a;使用正则表达式和json模块&#xff0c;可以快速提取和处理答案信息。答题操作&#xff1a;根据答案内容…

SHARE 203S PRO:倾斜摄影相机在地灾救援中的应用

在地质灾害的紧急关头&#xff0c;救援队伍面临的首要任务是迅速而准确地掌握灾区的地理信息。这时&#xff0c;倾斜摄影相机成为了救援测绘的利器。SHARE 203S PRO&#xff0c;这款由深圳赛尔智控科技有限公司研发的五镜头倾斜摄影相机&#xff0c;以其卓越的性能和功能&#…

Docker部署WebRTC-Streamer

文章目录 WebRTC-Streamer概述Docker部署WebRTC-StreamerVue使用WebRTC-Streamer一些问题 WebRTC-Streamer概述 WebRTC-Streamer是一个基于WebRTC技术的流媒体传输工具&#xff0c;它可以通过Web浏览器实现实时音视频流的传输和播放。它提供了一种简单而强大的方式&#xff0c…