使用Go实现优雅重启服务功能
怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《使用Go实现优雅重启服务功能》,涉及到服务、go重启,有需要的可以收藏一下
暴力的重启服务方案
一般服务器重启可以直接通过 kill 命令杀死进程,然后重新启动一个新的进程即可。但这种方法比较粗暴,有可能导致某些正在处理中的客户端请求失败,如果请求正在写数据,那么还有可能导致数据丢失或者数据不一致等。
那么有什么方式可以优雅的重启服务呢?
优雅的重启服务方案
优雅的重启方式流程如下:
从上面的流程可以看出,旧进程必须等待所有的请求连接完成后才会退出,请求不会被强制关闭,所以是个优雅的重启方式。
使用Go实现优雅重启
下面我们使用Go语言来演示怎么实现优雅启动功能,我们先来看看原理图:
从原理图可以知道,重启时首先通过发送 SIGHUP信号 给服务进程,服务进程收到 SIGHUP信号 后会 fork 一个新进程来处理新的请求,然后新进程会发送 SIGTERM信号 给旧服务进程(父进程),旧服务进程接收到 SIGTERM信号 后会关闭监听的 socket句柄 (停止接收新请求),并且等待未处理完成的请求完成后再退出进程。
下面通过代码来说明这个流程,代码主要参考 endless 这个库,有兴趣可以查看其源码。
首先我们定义一个名为 endlessServer 的结构并且继承 http.Server 结构:
type endlessServer struct {
http.Server
EndlessListener net.Listener
wg sync.WaitGroup
sigChan chan os.Signal
isChild bool
state uint8
lock *sync.RWMutex
}
Go的继承很简单,就是在定义结构时把要继承的结构嵌入到里面就可以了。
这里说明一下 endlessServer 各个成员的作用吧:
- Server:用于继承 http.Server 结构
- EndlessListener:监听客户端请求的 Listener
- wg:用于记录还有多少客户端请求没有完成
- sigChan:用于接收信号的管道
- isChild:用于重启时标志本进程是否是为一个新进程
- state:当前进程的状态
- lock:用于锁定一些资源
定义一个创建 endlessServer 结构的函数:
func NewServer(addr string, handler http.Handler) (srv *endlessServer) {
isChild := os.Getenv("ENDLESS_CONTINUE") != ""
srv = &endlessServer{
wg: sync.WaitGroup{},
sigChan: make(chan os.Signal),
isChild: isChild,
state: STATE_INIT,
lock: &sync.RWMutex{},
}
srv.Server.Addr = addr
srv.Server.ReadTimeout = 0
srv.Server.WriteTimeout = 0
srv.Server.MaxHeaderBytes = 0
srv.Server.Handler = handler
return
}
NewServer() 函数的实现比较简单,就是创建一个 endlessServer 结构,然后初始化其各个成员。要注意的是,是否为新进程是通过读取环境变量 ENDLESS_CONTINUE 来判断的,如果定义了 ENDLESS_CONTINUE 环境变量,就是说当前进程是新的服务进程。
用过Go语言的HTTP包的同学应该知道,要进行监听客户端请求的话必须调用其 ListenAndServe() 函数,所以我们要定义这个函数:
func ListenAndServe(addr string, handler http.Handler) error {
server := NewServer(addr, handler)
return server.ListenAndServe()
}
函数的实现很简单,就是先调用 NewServer() 函数创建一个 endlessServer 结构,然后调用其 ListenAndServe() 方法。所以我们要为 endlessServer 结构定义一个 ListenAndServe() 方法:
func (srv *endlessServer) ListenAndServe() (err error) {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
go srv.handleSignals()
l, err := srv.getListener(addr)
if err != nil {
log.Println(err)
return
}
srv.EndlessListener = newEndlessListener(l, srv)
if srv.isChild {
syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
}
return srv.Serve()
}
ListenAndServe() 方法首先会创建一个协程处理 handleSignals() 方法,这个方法主要是处理信号,下面会介绍。然后调用 getListener() 方法获取一个类型为 net.Listener 的对象,然后调用 newEndlessListener() 函数创建一个类型为 endlessListener 的对象。再通过判断当前进程是否为新的处理进程,如果是就调用 syscall.Kill() 方法发送一个 SIGTERM信号 给父进程(旧的服务处理进程),最后调用 Serve() 方法开始处理客户端连接。
我们先来看看处理信号的 handleSignal() 方法:
func (srv *endlessServer) handleSignals() {
var sig os.Signal
signal.Notify(
srv.sigChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
)
pid := syscall.Getpid()
for {
sig =
handleSignal() 方法主要监听3种信号, syscall.SIGHUP 、 syscall.SIGINT 和 syscall.SIGTERM 。 syscall.SIGHUP 信号为重启信号,而 syscall.SIGINT 信号为关闭服务信号,而 syscall.SIGTERM 信号主要是新的服务进程发送给旧的服务进程,告诉其关闭监听处理客户端的socket。当收到 syscall.SIGHUP 信号时,需要调用 fork() 方法来创建一个新的服务进程,而收到 syscall.SIGINT 和 syscall.SIGTERM 信号主要调用 shutdown() 方法来关闭当前进程。
再来看看创建新服务进程的 fork() 方法:
func (srv *endlessServer) fork() (err error) {
files := []*os.File{
srv.EndlessListener.(*endlessListener).File(),
}
env := append(
os.Environ(),
"ENDLESS_CONTINUE=1",
)
path := os.Args[0]
var args []string
if len(os.Args) > 1 {
args = os.Args[1:]
}
cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = files
cmd.Env = env
err = cmd.Start()
if err != nil {
log.Fatalf("Restart: Failed to launch, error: %v", err)
}
return
}
fork() 方法也比较简单,主要是使用 exec 包的 Command() 方法来创建一个 Cmd 对象,然后调用其 Start() 方法来启动一个新进。要注意的是,创建新进程前需要设置环境变量 ENDLESS_CONTINUE ,这是告诉新进程需要发送 syscall.SIGTERM 信号给父进程。还有就是通过 Cmd 对象的 ExtraFiles 成员把监听客户端连接的socket句柄传递给新服务处理进程了。
再来看看关闭服务进程的 shutdown() 方法:
func (srv *endlessServer) shutdown() {
err := srv.EndlessListener.Close()
}
这个方法很简单,就是调用 net.Listener 对象的 Close() 方法来关闭监听客户端请求的socket。关闭监听客户端请求的socket后,主循环会退出处理,然后会退出进程。
接着我们来看看接收客户端请求的 endlessListener.Accept() 方法:
func (el *endlessListener) Accept() (c net.Conn, err error) {
tc, err := el.Listener.(*net.TCPListener).AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true) // see http.tcpKeepAliveListener
tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener
c = endlessConn{
Conn: tc,
server: el.server,
}
el.server.wg.Add(1)
return
}
主要要注意的是,函数最后会调用 el.server.wg.Add(1) 这行代码来增加客户端请求的计数器,这是优雅重启的关键。因为在 endlessServer.Serve() 方法中会等待所有客户端请求处理完毕才会退出,我们来看看 endlessServer.Serve() 方法的实现:
func (srv *endlessServer) Serve() (err error) {
err = srv.Server.Serve(srv.EndlessListener)
srv.wg.Wait()
return
}
可以看到, endlessServer.Serve() 方法最后会调用 srv.wg.Wait() 这行代码来等待所有客户端请求完成。那么客户端连接计数器什么时候会减少呢?在 endlessConn.Close() 方法中可以看到计数器减少的操作:
func (w endlessConn) Close() error {
err := w.Conn.Close()
if err == nil {
w.server.wg.Done()
}
return err
}
可以看到, endlessConn.Close() 方法最后会调用 w.server.wg.Done() 这 行代码来减少客户端请求计数器。 至此,优雅重启服务的实现就完成。
当然,本篇文章主要介绍的是优雅重启的原理,完成的源码实现还是要查看 endless 这个库。
总结
以上所述是小编给大家介绍的使用Go实现优雅重启服务功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对golang学习网网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
今天关于《使用Go实现优雅重启服务功能》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!
Golang实现拓扑排序(DFS算法版)
- 上一篇
- Golang实现拓扑排序(DFS算法版)
- 下一篇
- 使用 Go 管理版本的方法示例
-
- 热情的吐司
- 这篇文章出现的刚刚好,太细致了,太给力了,码起来,关注老哥了!希望老哥能多写Golang相关的文章。
- 2023-02-16 02:44:57
-
- 香蕉汉堡
- 真优秀,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢作者大大分享文章!
- 2023-02-10 08:03:20
-
- Golang · Go教程 | 4分钟前 | go · database/sql · Rows.Scan ·
- Go 扫描未知列数结果时怎么动态分配接收变量
- 328浏览 收藏
-
- Golang · Go教程 | 17分钟前 | Go教程 · database/sql · 数据库元数据 · SQL NULL · Go database/sql rows ColumnTypes ColumnType.Length
- Go RowsColumnTypes 返回长度为 nil 时怎么解释
- 128浏览 收藏
-
- Golang · Go教程 | 27分钟前 |
- Go database/sql 怎么读取查询结果的列类型信息
- 319浏览 收藏
-
- Golang · Go教程 | 41分钟前 |
- Go 证书主机名校验失败时应该查 SAN 还是 CN
- 272浏览 收藏
-
- Golang · Go教程 | 49分钟前 | https · Go教程 · 证书 · tls crypto/x509 CertPool SystemCertPool RootCAs
- Go x509.SystemCertPool 返回空池时怎么兼容不同系统
- 327浏览 收藏
-
- Golang · Go教程 | 54分钟前 |
- Go TLS 客户端连接内网证书时怎么追加自定义 CA
- 255浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go CheckRedirect 返回错误时怎么读取最后一次响应
- 285浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go 跟随重定向后 Authorization 为什么消失
- 212浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go HTTP Client 禁止自动重定向时怎么保留原始响应
- 458浏览 收藏
-
- Golang · Go教程 | 2小时前 | Go教程 · 结构体标签 · encoding/xml · XML 序列化 · encoding/xml XMLName Go Marshal xml.Name XML 属性
- Go Marshal 输出 XML 时怎么控制根节点和属性
- 278浏览 收藏
-
- Golang · Go教程 | 2小时前 | go · xml · encoding/xml ·
- Go XML 中同名嵌套元素怎么映射成切片
- 380浏览 收藏
-
- Golang · Go教程 | 2小时前 | go · 文本处理 · XML解析 · encoding/xml · Go 命名空间 encoding/xml CharData XMLName
- Go encoding/xml 怎么用 CharData 读取混合文本而不丢空白
- 168浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 25次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 178次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 114次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 40次使用
-
- Generrated
- Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
- 21次使用
-
- go-micro开发RPC服务以及运行原理介绍
- 2022-12-31 420浏览
-
- Go快速开发一个RESTfulAPI服务
- 2023-01-01 493浏览
-
- Go语言程序开发gRPC服务
- 2022-12-31 319浏览
-
- Golang使用Consul详解
- 2023-01-19 277浏览
-
- go-micro使用Consul做服务发现的方法和原理解析
- 2022-12-31 229浏览

