详解Golang如何优雅的终止一个服务


Posted in Golang onMarch 21, 2022

前言

采用常规方式启动一个 Golang http 服务时,若服务被意外终止或中断,即未等待服务对现有请求连接处理并正常返回且亦未对服务停止前作一些必要的处理工作,这样即会造成服务硬终止。这种方式不是很优雅。

参看如下代码,该 http 服务请求路径为根路径,请求该路径,其会在 2s 后返回 hello。

var addr = flag.String("server addr", ":8080", "server address")

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second)
        fmt.Fprintln(w, "hello")
    })
    http.ListenAndServe(*addr, nil)
}

若服务启动后,请求http://localhost:8080/,然后使用 Ctrl+C 立即中断服务,服务即会立即退出(exit status 2),请求未正常返回(ERR_CONNECTION_REFUSED),连接即马上断了。

接下来介绍使用 http.Server 的 Shutdown 方法结合 signal.Notify 来优雅的终止服务。

1 Shutdown 方法

Golang http.Server 结构体有一个终止服务的方法 Shutdown,其 go doc 如下。

func (srv *Server) Shutdown(ctx context.Context) error
    Shutdown gracefully shuts down the server without interrupting any active
    connections. Shutdown works by first closing all open listeners, then
    closing all idle connections, and then waiting indefinitely for connections
    to return to idle and then shut down. If the provided context expires before
    the shutdown is complete, Shutdown returns the context's error, otherwise it
    returns any error returned from closing the Server's underlying Listener(s).

    When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
    immediately return ErrServerClosed. Make sure the program doesn't exit and
    waits instead for Shutdown to return.

    Shutdown does not attempt to close nor wait for hijacked connections such as
    WebSockets. The caller of Shutdown should separately notify such long-lived
    connections of shutdown and wait for them to close, if desired. See
    RegisterOnShutdown for a way to register shutdown notification functions.

    Once Shutdown has been called on a server, it may not be reused; future
    calls to methods such as Serve will return ErrServerClosed.

由文档可知:

使用 Shutdown 可以优雅的终止服务,其不会中断活跃连接。

其工作过程为:首先关闭所有开启的监听器,然后关闭所有闲置连接,最后等待活跃的连接均闲置了才终止服务。

若传入的 context 在服务完成终止前已超时,则 Shutdown 方法返回 context 的错误,否则返回任何由关闭服务监听器所引起的错误。

当 Shutdown 方法被调用时,Serve、ListenAndServe 及 ListenAndServeTLS 方法会立刻返回 ErrServerClosed 错误。请确保 Shutdown 未返回时,勿退出程序。

对诸如 WebSocket 等的长连接,Shutdown 不会尝试关闭也不会等待这些连接。若需要,需调用者分开额外处理(诸如通知诸长连接或等待它们关闭,使用 RegisterOnShutdown 注册终止通知函数)。

一旦对 server 调用了 Shutdown,其即不可再使用了(会报 ErrServerClosed 错误)。

有了 Shutdown 方法,我们知道在服务终止前,调用该方法即可等待活跃连接正常返回,然后优雅的关闭。

但服务启动后的某一时刻,程序如何知道服务被中断了呢?服务被中断时如何通知程序,然后调用 Shutdown 作处理呢?接下来看一下系统信号通知函数的作用。

2 signal.Notify 函数

signal 包的 Notify 函数提供系统信号通知的能力,其 go doc 如下。

func Notify(c chan<- os.Signal, sig ...os.Signal)
    Notify causes package signal to relay incoming signals to c. If no signals
    are provided, all incoming signals will be relayed to c. Otherwise, just the
    provided signals will.

    Package signal will not block sending to c: the caller must ensure that c
    has sufficient buffer space to keep up with the expected signal rate. For a
    channel used for notification of just one signal value, a buffer of size 1
    is sufficient.

    It is allowed to call Notify multiple times with the same channel: each call
    expands the set of signals sent to that channel. The only way to remove
    signals from the set is to call Stop.

    It is allowed to call Notify multiple times with different channels and the
    same signals: each channel receives copies of incoming signals
    independently.

由文档可知:

参数 c 是调用者的信号接收通道,Notify 可将进入的信号转到 c。sig 参数为需要转发的信号类型,若不指定,所有进入的信号都将会转到 c。

信号不会阻塞式的发给 c:调用者需确保 c 有足够的缓冲空间,以应对指定信号的高频发送。对于用于通知仅一个信号值的通道,缓冲大小为 1 即可。

同一个通道可以调用 Notify 多次:每个调用扩展了发送至该通道的信号集合。仅可调用 Stop 来从信号集合移除信号。

允许不同的通道使用同样的信号参数调用 Notify 多次:每个通道独立的接收进入信号的副本。

综上,有了 signal.Notify,传入一个 chan 并指定中断参数,这样当系统中断时,即可接收到信号。

参看如下代码,当使用 Ctrl+C 时,c 会接收到中断信号,程序会在打印“program interrupted”语句后退出。

func main() {
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    <-c
    log.Fatal("program interrupted")
}
$ go run main.go

Ctrl+C

2019/06/11 17:59:11 program interrupted
exit status 1

3 Server 优雅的终止

接下来我们使用如上 signal.Notify 结合 http.Server 的 Shutdown 方法实现服务优雅的终止。

如下代码,Handler 与文章开始时的处理逻辑一样,其会在2s后返回 hello。

创建一个 http.Server 实例,指定端口与 Handler。

声明一个 processed chan,其用来保证服务优雅的终止后再退出主 goroutine。

新启一个 goroutine,其会监听 os.Interrupt 信号,一旦服务被中断即调用服务的 Shutdown 方法,确保活跃连接的正常返回(本代码使用的 Context 超时时间为 3s,大于服务 Handler 的处理时间,所以不会超时)。

处理完成后,关闭 processed 通道,最后主 goroutine 退出。

代码同时托管在 GitHub,欢迎关注(github.com/olzhy/go-excercises)。

var addr = flag.String("server addr", ":8080", "server address")

func main() {
    // handler
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second)
        fmt.Fprintln(w, "hello")
    })

    // server
    srv := http.Server{
        Addr:    *addr,
        Handler: handler,
    }

    // make sure idle connections returned
    processed := make(chan struct{})
    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, os.Interrupt)
        <-c

        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()
        if err := srv.Shutdown(ctx); nil != err {
            log.Fatalf("server shutdown failed, err: %v\n", err)
        }
        log.Println("server gracefully shutdown")

        close(processed)
    }()

    // serve
    err := srv.ListenAndServe()
    if http.ErrServerClosed != err {
        log.Fatalf("server not gracefully shutdown, err :%v\n", err)
    }

    // waiting for goroutine above processed
    <-processed
}

总结

到此这篇关于Golang如何优雅的终止一个服务的文章就介绍到这了,更多相关Golang终止服务内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
golang正则之命名分组方式
Apr 25 Golang
解决Golang中ResponseWriter的一个坑
Apr 27 Golang
golang 实现两个结构体复制字段
Apr 28 Golang
golang DNS服务器的简单实现操作
Apr 30 Golang
解决golang post文件时Content-Type出现的问题
May 02 Golang
go语言中http超时引发的事故解决
Jun 02 Golang
如何利用golang运用mysql数据库
Mar 13 Golang
浅谈GO中的Channel以及死锁的造成
Mar 18 Golang
golang为什么要统一错误处理
Apr 03 Golang
Golang原生rpc(rpc服务端源码解读)
Apr 07 Golang
Golang 并发编程 SingleFlight模式
Apr 26 Golang
Golang并发工具Singleflight
May 06 Golang
Go语言实现一个简单的并发聊天室的项目实战
Mar 18 #Golang
浅谈GO中的Channel以及死锁的造成
Mar 18 #Golang
Golang 并发下的问题定位及解决方案
Mar 16 #Golang
如何利用golang运用mysql数据库
深入理解go缓存库freecache的使用
Feb 15 #Golang
Go语言读取txt文档的操作方法
Jan 22 #Golang
一文搞懂Golang 时间和日期相关函数
You might like
基于php socket(fsockopen)的应用实例分析
2013/06/02 PHP
C#静态方法与非静态方法实例分析
2014/09/22 PHP
浅析php工厂模式
2014/11/25 PHP
利用php实现一周之内自动登录存储机制(cookie、session、localStorage)
2016/10/31 PHP
MooTools 1.2介绍
2009/09/14 Javascript
页面只有一个text的时候,回车自动submit的解决方法
2010/08/12 Javascript
js相册效果代码(点击创建即可)
2013/04/16 Javascript
jquery 跳到顶部和底部动画2句代码简单实现
2013/07/18 Javascript
jquery实现页面百叶窗走马灯式翻滚显示效果的方法
2015/03/12 Javascript
浅谈javascript中关于日期和时间的基础知识
2016/07/13 Javascript
JS中判断null的方法分析
2016/11/21 Javascript
Vue.js实现一个漂亮、灵活、可复用的提示组件示例
2017/03/17 Javascript
vue动态设置img的src路径实例
2018/09/18 Javascript
Vue 递归多级菜单的实例代码
2019/05/05 Javascript
微信小程序之下拉列表实现方法解析(附完整源码)
2019/08/23 Javascript
selenium 反爬虫之跳过淘宝滑块验证功能的实现代码
2020/08/27 Javascript
[02:03]《现实生活中的DOTA2》—林书豪&DOTA2职业选手出演短片
2015/08/18 DOTA
[01:24:16]2018DOTA2亚洲邀请赛 4.6 全明星赛
2018/04/10 DOTA
Django 后台获取文件列表 InMemoryUploadedFile的例子
2019/08/07 Python
关于ZeroMQ 三种模式python3实现方式
2019/12/23 Python
Django 博客实现简单的全文搜索的示例代码
2020/02/17 Python
解决Python 异常TypeError: cannot concatenate 'str' and 'int' objects
2020/04/08 Python
Pycharm生成可执行文件.exe的实现方法
2020/06/02 Python
HTML5 拖拽批量上传文件的示例代码
2018/03/28 HTML / CSS
html5桌面通知(Web Notifications)实例解析
2014/07/07 HTML / CSS
Foreo国际站:Foreo International
2018/10/29 全球购物
介绍一下Python中webbrowser的用法
2013/05/07 面试题
美发店5.1活动方案
2014/01/24 职场文书
银行爱岗敬业演讲稿
2014/05/05 职场文书
学校政风行风自查自纠报告
2014/10/21 职场文书
专业技术职务聘任证明
2015/03/02 职场文书
酒店前台接待岗位职责
2015/04/02 职场文书
环保守法证明
2015/06/24 职场文书
Nginx配置80端口访问8080及项目名地址方法解析
2021/03/31 Servers
MySQL中你可能忽略的COLLATION实例详解
2021/05/12 MySQL
Python预测分词的实现
2021/06/18 Python