Go标准库http server的优雅关闭深入理解
作者:凉凉的知识库
引言
本篇为【深入理解Go标准库】系列第三篇
第一篇:http server的启动
第二篇:ServeMux的使用与模式匹配
第三篇:http server的优雅关闭👈
本系列将持续更新,欢迎关注 👏 获取实时通知
还记得怎么启动一个HTTP Server么?
package main import ( "net" "net/http" ) func main() { // 方式1 err := http.ListenAndServe(":8080", nil) if err != nil { panic(err) } // 方式2 // server := &http.Server{Addr: ":8080"} // err := server.ListenAndServe() // if err != nil { // panic(err) // } }
ListenAndServe
在不出错的情况下,会一直阻塞在这个位置,如何停止这样的一个HTTP Server呢?
CTRL+C
是结束一个进程常用的方式,它和kill pid
或者kill -l 15 pid
命令本质上没有任何区别,他们都是向进程发送了SIGTERM
信号。因为程序没有设置对SIGTERM
信号的处理程序,所以系统默认的信号处理程序结束了我们的进程
这会带来什么问题?
在服务器的进程被杀死时,我们的服务器可能正在处理请求并未完成。因此对于客户端产生了一个预期外的错误
curl -v --max-time 4 127.0.0.1:8009/foo * Connection #0 to host 127.0.0.1 left intact * Trying 127.0.0.1:8009... * Connected to 127.0.0.1 (127.0.0.1) port 8009 (#0) > GET /foo HTTP/1.1 > Host: 127.0.0.1:8009 > User-Agent: curl/7.86.0 > Accept: */* > * Empty reply from server * Closing connection 0 curl: (52) Empty reply from server
如果有nginx代理,因为upstream的中断,nginx会产生502的响应
curl -v --max-time 11 127.0.0.1:8010/foo * Trying 127.0.0.1:8010... * Connected to 127.0.0.1 (127.0.0.1) port 8010 (#0) > GET /foo HTTP/1.1 > Host: 127.0.0.1:8010 > User-Agent: curl/7.86.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 502 Bad Gateway < Server: nginx/1.25.3 < Date: Sat, 02 Dec 2023 10:14:33 GMT < Content-Type: text/html < Content-Length: 497 < Connection: keep-alive < ETag: "6537cac7-1f1"
优雅关闭的初步实现
优雅关闭(graceful shutdown)指的是我们的HTTP Server关闭前既拒绝新来的请求,又正确的处理完正在进行中的请求,随后进程退出。如何实现?
🌲 异步启动HTTP server
因为ListenAndServe
会阻塞goroutine,如果还需要让代码继续执行,我们需要把它放到一个异步的goroutine中
go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }()
🌲 第二步:设置SIGTERM信号处理程序
操作系统默认的信号处理程序是直接结束进程,因此要实现graceful shutdown,要设置程序自己的信号处理程序。
Go中可以使用如下的方式来处理信号
signal.Notify
来设置我们要监听的信号,一旦有程序设定的信号发生时,信号会被写入channel中signalCh chan os.Signal
我们定义的是一个带缓冲的channel,当channel中没有数据时读操作会阻塞
signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) sig := <-signalCh log.Printf("Received signal: %v\n", sig)
🌲 第三步:平滑的关闭HTTP Server
在自定义的信号处理程序中处理什么呢?
1、首先需要关闭端口的监听,此时新的请求就无法建立连接
2、对空闲的连接进行关闭
3、对进行中的连接等待处理完成,变成空闲连接后进行关闭
在Go 1.8以前实现上述操作需要编写大量的代码,也有一些第三方的库(tylerstillwate/graceful、facebookarchive/grace等)可供使用。但Go1.8之后标准库提供了 Shutdown()
方法
🌲 实现:综合上面三步有如下实现
func main() { mx := http.NewServeMux() mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Duration(rand.Intn(10)) * time.Second) w.Write([]byte("Receive path foo\n")) }) srv := http.Server{ Addr: ":8009", Handler: mx, } go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }() signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) sig := <-signalCh log.Printf("Received signal: %v\n", sig) if err := srv.Shutdown(context.Background()); err != nil { log.Fatalf("Server shutdown failed: %v\n", err) } log.Println("Server shutdown gracefully") }
没有收到SIGINT
、SIGTERM
信号前,main goroutine被signalCh
的读阻塞
一旦收到信号,signalCh
的阻塞被解除会往下执行server的Shutdown()
,Shutdown()
函数会处理好活跃和非活跃的连接,并返回结果
上述代码有什么问题么?
优雅关闭实现的细节
🌲 当Shutdown
被调用时ListenAndServe
会立刻返回http.ErrServerClosed
的错误
go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }()
对于上文的代码,Shutdown()
刚被调用,ListenAndServe
所在的goroutine就抛出了panic,因而也导致main goroutine被退出,并没有达到运行Shutdown()
预期的效果
如果依旧想对ListenAndServe
的错误抛出painc,需要忽略http.ErrServerClosed
的错误
go func() { err := srv.ListenAndServe() if err != nil && err != http.ErrServerClosed { panic(err) } }()
🌲 在有限的时间内关闭服务器
优雅关闭过程中会等待进行中的请求完成。但请求处理的过程可能非常耗时,或者请求本身已经陷入了无法结束的状态,我们不可能无限的等待下去,因此设定一个关闭的上限时间会更稳妥。
Shutdown()
接受一个context.Context
类型的参数,我们可以用来设定超时时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server shutdown failed: %v\n", err) } log.Println("Server shutdown gracefully")
通过ctx.Done()
可以区分是否因为超时导致的服务器关闭,因而可以对不同的退出原因进行区分
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { select { case <-ctx.Done(): // 由于达到超时时间服务器关闭,未完成优雅关闭 log.Println("timeout of 5 seconds.") default: // 其他原因导致的服务关闭异常,未完成优雅关闭 log.Fatalf("Server shutdown failed: %v\n", err) } return } // 正确执行优雅关闭服务器 log.Println("Server shutdown gracefully")
🌲 释放其他资源
除了显式的释放资源,main goroutine也有必要通知其他goroutine进程即将退出,做必要的处理
例如,我们的服务在启动后会向服务中心进行注册,之后异步定时上报自身状态。
为了让注册中心第一时间感知到服务已下线,需要主动注销服务。在注销服务前,需要先暂停异步的定时上报
context.Context
让我们可以很轻松的做到这件事
ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() }() // 需要在服务启动后才在注册中心注册 go func() { tc := time.NewTicker(5 * time.Second) for { select { case <-tc.C: // 上报状态 log.Println("status update success") case <-ctx.Done(): // server closed, return tc.Stop() log.Println("stop update success") return } } }()
示例仓库中还有一个更复杂的利用context.Context
退出子goroutine的例子
🌲 全貌
结合上面的所有的细节,一个优雅关闭的http server代码如下
func registerService(ctx context.Context) { tc := time.NewTicker(5 * time.Second) for { select { case <-tc.C: // 上报状态 log.Println("status update success") case <-ctx.Done(): tc.Stop() log.Println("stop update success") return } } } func destroyService() { log.Println("destroy success") } func gracefulShutdown() { mainCtx, mainCancel := context.WithCancel(context.Background()) // 用ctx初始化资源,mysql,redis等 // ... defer func() { mainCancel() // 主动注销服务 destroyService() // 清理资源,mysql,redis等 // ... }() mx := http.NewServeMux() mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Duration(rand.Intn(10)) * time.Second) w.Write([]byte("Receive path foo\n")) }) srv := http.Server{ Addr: ":8009", Handler: mx, } // ListenAndServe也会阻塞,需要把它放到一个goroutine中 go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { panic(err) } }() // 需要在服务启动后才在注册中心注册 go registerService(mainCtx) signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) // 等待信号 sig := <-signalCh log.Printf("Received signal: %v\n", sig) ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second) defer cancelTimeout() if err := srv.Shutdown(ctxTimeout); err != nil { select { case <-ctxTimeout.Done(): // 由于达到超时时间服务器关闭,未完成优雅关闭 log.Println("timeout of 5 seconds.") default: // 其他原因导致的服务关闭异常,未完成优雅关闭 log.Fatalf("Server shutdown failed: %v\n", err) } return } // 正确执行优雅关闭服务器 log.Println("Server shutdown gracefully") }
以上就是Go标准库http server的优雅关闭深入理解的详细内容,更多关于Go标准库http server关闭的资料请关注脚本之家其它相关文章!