如何完美实现 Go 服务的平滑升级

2024-08-27 20:12

本文主要是介绍如何完美实现 Go 服务的平滑升级,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        Go 服务作为常驻进程,如何进行服务升级呢?你可能会觉得这还不简单,先将现有服务停止,再启动新的服务不就可以了。可是将现有服务停止时,如果它还在处理请求,那么这些请求该如何处理?另外,在现有服务已经退出但是新服务还没有启动期间,新的请求到达了又该如何处理? Go 服务升级并没有那么简单,我们需要实现一套平滑升级方案来保证升级过程是无损的。

1. 服务升级导致 502 状态码

        Go 服务升级会导致出现大量的 502 状态码,这一结论可以通过模拟服务升级流程来验证。假设 HTTP 请求的访问链路是客户端--网关--Go服务,即我们还需要搭建网关。基于 Go 语言实现的 HTTP 服务示例程序如下所示:

func mian(){server := &http.Server{Addr: "0.0.0.0:8080",}http.HandleFunc("/ping",func(w http.ResponseWriter,r *http.Request){duration := rand.Intn(1000)//模拟请求耗时time.Sleep(time.Millisecond * time.Durtation(duration))w.Write([]byte(r.URL.Path +">ping response"))})_= server.ListenAndServe()
}

        参考上面的代码,每一个请求都会随机休眠 0~1000ms。我们通过这种方式模拟了请求的正常响应时间。

         接下来使用 ab 压测工具模拟发起请求并升级 Go 服务。如何升级呢?我们可以通过简单的重启(升级和重启类似,只不过升级会替换可执行程序)来模拟。

//模拟并发请求
$ab -n 10000 -c 100 http://127.0.0.1/ping
//重启服务
$supervisorctl restart main

        在上面的命令中,我们通过 supervisorctl 命令重启了 Go 服务。补充一下,Go 服务是部署在物理机上的,为了避免 Go 服务异常退出,我们通常会使用成熟的进程管理工具,比如 supervisor。其中,supervisorctl 命令是 supervisor 提供的客户端命令。

        如何验证是否会出现瞬时的 502 错误呢? 可以查看 Nginx 的错误日志。这时候,你应该可以看到不少错误日志,这些错误日志可以分为两种,如下所示:

upstream prematurely closed connection while reading response header from upstream
connect( ) failed (111:Connection refused) while connecting to upstream

 

2. Go 语言信号处理框架

        为什么要先介绍信号呢?因为当我们需要将现有 Go 服务停止时,是通过给 Go 服务发送信号实现的,比如 Crtl+C 组合按键、supervisor 进程管理工具等。我们可以通过 kill 命令查看系统支持的所有信号,如下所示:

[root@pass ~]# kill -l1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

        需要注意的是,SIGKILL 信号是不能被捕获的,所以称该信号为强制退出信号。那么在 Go 语言中我们如何使用信号呢?可以参考下面的测试程序:

 

package mainimport ("fmt""os""os/signal""sync""syscall"
)func main() {c := make(chan os.Signal, 1)//相当于捕获信号signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)wg := sync.WaitGroup{}wg.Add(1)go func() {<-c //接收到信号fmt.Println("quit signal receive,quit")wg.Done()}()wg.Wait()
}

 

3. Go 服务平滑退出

        在第1节中提到,我们将 Go 服务升级引起的 502 问题拆解为两个独立的子问题。其中,第一个问题是:如何实现 Go 服务的平滑退出? 平滑退出的含义是在处理完所有正在处理的请求之后再退出。下面我们将以 HTTP 服务为例,介绍如何实现 Go 服务的平滑退出。

         其实,Go 语言本身就提供了平滑结束 HTTP 服务的方法。所以我们只需要监听退出信号,比如 SIGINT、SIGTERM 等信号,并且在接收到这些信号时调用对应的方法就可以了。参考下面的示例程序:

func main(){server := &http.Server {Addr:"0.0.0.0:8080",}exit := make(chan interface{},0)sig := make(chan os.Signal,2)//监听退出信号signal.Notify(sig,syscall.SIGINT,syscall.SIGTRM)//子协程,退出时阻塞式等待 HTTP 服务结束go func(){<-sigfmt.Println(time.Now(),"recv quit signal")_= server.Shutdown(context.Background())// 通知主协程,HTTP 服务已停止close(exit)}()//注册请求处理方法(方法阻塞 10 秒才返回响应结果),省略//启动 HTTP 服务err := server.ListenAndServe()if err != nil {fmt.Println(time.Now(),err)}//只有HTTP服务结束后,主协程才能退出<-exitfmt.Println(time.Now(),"main coroutine exit")
}

        在上面的代码中,方法 server.Shutdown 用于停止 HTTP 服务,该方法会一直阻塞直到所有监听的套接字都已经关闭,以及所有的 TCP 连接都已经关闭(当HTTP服务正在退出时,Go服务处理完 HTTP 请求后会立即关闭连接)。也就是说,当方法 server.Shutdown 返回时,说明 Go 服务已经处理完所有正在处理的请求了,这时候 Go 服务也就可以退出了。需要注意的是,当我们调用方法 server.Shutdown 停止 HTTP 服务时,方法 server.ListenAndServe 基本上会立即返回错误(错误信息 http:Server closed,这是因为监听的套接字被关闭了)。所以,为了避免主协程退出导致 Go 进程退出,我们使用了一个管道 exit,子协程可以通过管道 exit 通知主协程 HTTP 服务已经平滑结束。

 

4. 基于 gracehttp 的 Go 服务平滑升级

        3节已经实现了 Go 服务的平滑退出,想要实现 Go 服务平滑升级,还有一个问题需要解决:如何实现 Go 服务的无缝启动? 也就是说,在现有的 Go 服务退出之前,新的 Go 服务就需要启动,并且这时候新的 HTTP 请求应该由新的 Go 服务处理。下面将基于开源框架 gracehttp 讲解如何实现 Go 服务平滑升级。

        首先,这里其实有一个非常典型的问题需要解决:现有的 Go 进程已经绑定了 8080 端口,并且监听了套接字,这样一来当新的 Go 进程再次绑定 8080 端口并监听套接字时,就会产生错误 bind: address already in use。

        如何解决这一问题呢?我们可以让现有 Go 进程作为父进程来启动新的 Go 进程。难道父子进程就能同时绑定同一个端口号吗?当然不是,那为什么要这样做呢?这就需要了解一下系统调用 exec 了,该系统调用用于创建新的进程,所以,子进程并不需要再执行绑定端口号并监听套接字的操作了,只要获取到父进程套接字的文件描述符就可以了。如何获取呢?这方法就比较多了,比如父进程可以通过环境变量将套接字的文件描述符传递给子进程。

        这里推荐一个开源框架 gracehttp,其封装了平滑升级的相关逻辑,使用起来非常简单,可以参考官方示例程序,代码如下所示:

package main
import (......"github.com/facebookgo/grace/gracehttp"
)var now = time.Now()
func main(){gracehttp.Serve(	// 包装Go原生的HTTP服务&http.Server{Addr: ":8080",Handler:newHandler("Zero ")},)
}func newHandler(nae string) http.Handler {mux := http.NewServeMux()// HTTP 请求处理方法,可以根据请求参数休眠指定时间mux.HandleFunc("/sleep",func(w http.ResponseWriter,r *http.Request) {duration,_:=time.ParseDuration(r.FormValue("duration"))time.Sleep(duration)fmt.Fprintf(w,"%s started at %s slept for %d nanoseconds from pid %d.\n",name,now,duration.Nanoseconds(),os.Getpid(),)	})return mux
}

        在上面的代码中,我们只需要使用 gracehttp.Serve 将 Go 语言原生的 HTTP 服务包装一下,就能实现 Go 服务的平滑升级。需要说明的是,gracehttp 监听的是 SIGUSR2 信号,当接收到该信号之后, gracehttp 就会创建新的进程,等到新的进程启动后再平滑停止现有进程。编译并运行上面的程序,通过 curl 命令手动发起 HTTP 请求并重启 Go 服务,结果如下所示:

        由上面的输出结果可千,我们首先查询了 Go 服务的进程 ID 是 31057 ,随后通过 curl 命令发起了 HTTP 请求,之后再向 Go 服务发送了 SIGUSR2 信号。结果表明,该请求由进程 31057 处理了,最后再次查询了 Go 服务的进程 ID 是 31095,说明 Go 服务确实重启了。

        看到这里有些读者可能会有疑问,仅仅发起一个 HTTP 请求,就认为重启过程是平滑的吗?当然不是,严格的验证方案可以参考第1小节。我们在升级的过程中同时使用 ab 压测工具模拟并发请求,验证结果如下所示:

        由上面的输出结果可知,我们首先查询了 Go 服务的进程 ID 是 31185,随后通过 ab 压测工具发起了大量请求并向 Go 服务发送了 SIGUSR2 信号。再次查询 Go 服务的进程 ID,你会发现存在两个 Go 进程,这是因为新的 Go 进程已经启动了,但是老的 Go 进程还在处理请求没有退出。最后,稍等片刻再次查询 Go 服务的进程 ID,你会发现这时候只有一个 Go 进程了。

        那么在 Go 服务重启过程中,有没有引起一些 502 请求呢?可以查看网关 Nginx 的访问日志或错误日志,你会发现所有请求都正常返回了状态码 200,也就是说 gracehttp 确写着可以帮助我们实现 Go 服务的平滑升级。

        最后,简单看一下 gracehttp 框架的实现原理。首先,gracehttp 在启动 Go 服务的时候,需要判断是否应该绑定端口并监听套接字,其次,当 Go 服务作为子进程启动之后,还需要给父进程发送一个退出信号,而父进程退出也必须是平滑的。我们先简单看一下 gracehttp 启动 Go 服务的核心逻辑,代码如下所示;

func (a *app)run() error {//创建监听套接字if err := a.listen(); err != nil {return err}//启动Go服务a.serve()//给父进程发送退出信号if didInherit && ppid != 1 {if err := syscall.Kill(ppid,syscall.SIGTERM);err != nil {}}......
}

        在上面的代码中,方法 a.listen 用于创建并监听套接字,当然如果 Go 服务作为子进程启动,那么该 Go 服务不会再创建套接字,而是直接继承父进程的套接字。另外可以看到,Go 服务作为子进程启动后,通过系统调用 kill 给父进程发送一个退出信号。 

        当然,实现平滑升级的前提是能够接收并处理指定信号,gracehttp 自定义的信号处理函数如下所示:

func (a *app) signalHandler (wg *sync.WaitGroup){ch := make(chan os.Signal,10)signal.Notify(ch,syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)for {sig := <-chanswitch sig {case syscall.SIGINT,syscall.SIGTERM:// 平滑退出 Go 服务returncase syscall.SIGUSR2://创建新的进程,底层通过环境变量传递了其监听的套接字文件描述符if _,err := a.net.StartProcess();err != nil {a.errors <- err}}}
}

        参考上面的代码,gracehttp 总共监听了 3 个信号。其中,信号 syscall.SIGINT 和 syscall.SIGTERM 用于平滑退出 Go 服务,信号 syscall.SIGUSR2 用于启动新的 Go 服务,这 3 个信号的组合实现了 Go 服务的平滑升级。方法 a.net.StartProcess 用于创建新的进程,并通过环境变量将 Go 父进程监听的套接字文件描述符传递给子进程。

这篇关于如何完美实现 Go 服务的平滑升级的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1112658

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、