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

news2024/11/20 9:18:56

和其他语言相比,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 控制超时。基于此,我们抽象出了一般服务优雅停止的核心思路。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode: 705. 设计哈希集合 + 数据结构设计】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【数据分析】嫡权法EWM

总结&#xff1a;基于熵值信息来计算出权重&#xff0c;数据具有客观性。 目录 简介 计算步骤 案例 简介 熵值法原理 熵值法的基本思路是根据指标变异性的大小来确定客观权重信息熵:信息量的期望。可以理解成不确定性的大小&#xff0c;不确定性越大&#xff0c;信息熵也就…

【Liunx】什么是vim?五大模式及转换方法详解

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

基于SSM的在线学习系统的设计与实现(论文+源码)_kaic

基于SSM的在线学习系统的设计与实现 摘要 随着信息互联网购物的飞速发展&#xff0c;一般企业都去创建属于自己的管理系统。本文介绍了在线学习系统的开发全过程。通过分析企业对于在线学习系统的需求&#xff0c;创建了一个计算机管理在线学习系统的方案。文章介绍了在线学习系…

程序员会营销,好比虎生双翅,不是牛叉,是牛叉大发了。

Hi&#xff0c;我是贝格前端工场&#xff0c;一般来讲程序员在语言表达和营销上都是弱项&#xff0c;你看头条上那些程序员XXX&#xff0c;嘚啵嘚的能说的&#xff0c;其实都是伪程序&#xff0c;都是大商务。 不过&#xff0c;如果程序员如果能够提升自己的营销能力&#xff0…

教你将配置好的conda环境迁移到其它设备

文章目录 问题分析存在的方法环境要求方法步骤1. 下载conda pack2. 打包原环境3. 新设备还原环境4. 查看环境 问题分析 好不容易配置好的conda环境&#xff0c;要在另一个设备上运行&#xff0c;还要重新配置&#xff0c;好麻烦。 存在的方法 pip install -r requirement.txt …

CMD 汉字乱码处理

windows 11 cmd汉字乱码问题处理 一 查看CMD编码 win R 输入 cmd 输入 chcp 查看回显信息 “936”代表的意思就是 GBK (汉字内码扩展规范)&#xff0c;通常情况下GBK也是cmd的默认编码。 解决乱码需要把编码改为 utf-8 二 临时修改 在 终端中输入 chcp 65001 三 永久修改…

踩了一堆坑,终于掌握了postgreSQL主从流的精髓

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

大模型化身数据魔法师,降低NLP高置信误判

关注公众号【AI论文解读】回复: 论文解读 获取本文论文 引言&#xff1a;NLP模型的高置信错误与脆弱性问题 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;模型的预测性能优化往往伴随着高置信错误&#xff08;high confidence errors&#xff09;的产生&#x…

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

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

2D AI交互数字人:赋能文旅、金融、政务、教育行业数字化转型

AI交互数字人结合了语音合成、语音识别、语义理解、图像处理、机器翻译、虚拟形象驱动等多项AI核心技术&#xff0c;可以提供服务导览、业务咨询、语音互动交流、信息播报等智能服务。 其中&#xff0c;2D AI交互数字人是采集真人视频&#xff0c;通过AI训练&#xff0c;生成逼…

C语言——字符函数与字符串函数

正文开始&#xff1a;在编程过程中&#xff0c;我们经常要处理字符和字符串&#xff0c;为了方便操作字符和字符串&#xff0c;C语⾔标准库中提供了 一系列库函数&#xff0c;接下来我们就学习⼀下这些函数。 1. 字符分类函数 C语⾔中有⼀系列的函数是专门做字符分类的&#…

基于ssm的智慧餐厅点餐管理系统设计与实现(java项目+文档+元)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的智慧餐厅点餐管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 智慧餐厅点餐管理系统设计…

在Mac主机上连接Linux虚拟机

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

资本涌向AI,AI规模将达2205亿美元?

随着科技的飞速发展&#xff0c;全球科技巨头瑞银&#xff08;UBS&#xff09;在其最新报告中预测&#xff0c;科技产业正迎来一个前所未有的增长浪潮。特别是在人工智能&#xff08;AI&#xff09;领域&#xff0c;预计到2027年&#xff0c;AI模型和应用程序的市场规模将达到惊…

Docker部署WebRTC-Streamer

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

21.5k Star , AI 智能体项目OpenDevin:少写代码,多创造(附部署教程)

Aitrainee | 公众号&#xff1a;AI进修生 这是一个旨在复制 Devin 的开源项目&#xff0c;Devin 是一位自主人工智能软件工程师&#xff0c;能够执行复杂的工程任务并在软件开发项目上与用户积极协作。该项目致力于通过开源社区的力量复制、增强和创新 Devin。 Devin 代表了一…

汇舟问卷:国外问卷调查适合哪些人?

在这个快节奏的时代&#xff0c;朝九晚五的工作模式似乎已经成为许多人的固定生活模式。然而&#xff0c;这种日复一日的工作方式往往让人感到疲惫和厌倦&#xff0c;我们渴望找到一种既能赚钱又能兼顾生活的方式。 海外问卷调查作为一种适合在家做的赚钱方式&#xff0c;这两…

前端知识学习笔记-六(vue)

简介 Vue是前端优秀框架是一套用于构建用户界面的渐进式框架 Vue优点 Vue是目前前端最火的框架之一 Vue是目前企业技术栈中要求的知识点 vue可以提升开发体验 Vue学习难度较低 Vue开发前准备 一、nodejs环境 Nodejs简介 Nodejs诞生于2009年&#xff0c;主攻服务器方向&#x…

Github Coplit的认证及其在JetBrains中的使用

原文地址&#xff1a;Github Coplit的认证及其在JetBrains中的使用 - Pleasure的博客 下面是正文内容&#xff1a; 前言 今天分享一个可有可无的小技巧&#xff0c;水一篇文。 如标题所述&#xff0c;Github Coplit的认证及其在JetBrains中的使用 正文 介绍JetBrains JetBrain…