Go标准库http server的优雅关闭深入理解

 更新时间:2024年01月15日 10:56:36   作者:凉凉的知识库  
这篇文章主要为大家介绍了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 := &lt;-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")
}

没有收到SIGINTSIGTERM信号前,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 &amp;&amp; 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关闭的资料请关注脚本之家其它相关文章!

相关文章

  • 详解Go语言中iota的应用

    详解Go语言中iota的应用

    在本文中,小编将带着大家深入探讨 iota 的神奇力量,包括 iota 的介绍和应用场景以及使用技巧和注意事项,准备好了吗,准备一杯你最喜欢的饮料或茶,随着本文一探究竟吧
    2023-07-07
  • Golang使用zlib压缩和解压缩字符串

    Golang使用zlib压缩和解压缩字符串

    本文给大家分享的是Golang使用zlib压缩和解压缩字符串的方法和示例,有需要的小伙伴可以参考下
    2017-02-02
  • Go语言并发模型的2种编程方案

    Go语言并发模型的2种编程方案

    这篇文章主要介绍了Go语言并发模型的2种编程方案,本文给出共享内存和通过通信的2种解决方案,并给出了实现代码,需要的朋友可以参考下
    2014-10-10
  • golang标准库time时间包的使用

    golang标准库time时间包的使用

    时间和日期是我们编程中经常会用到的,本文主要介绍了golang标准库time时间包的使用,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • Go高级特性探究之优先级队列详解

    Go高级特性探究之优先级队列详解

    Heap 是一种数据结构,这种数据结构常用于实现优先队列,这篇文章主要就是来和大家深入探讨一下GO语言中的优先级队列,感兴趣的可以了解一下
    2023-06-06
  • GO语言Context的作用及各种使用方法

    GO语言Context的作用及各种使用方法

    golang的Context包是专门用来处理多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作,下面这篇文章主要给大家介绍了关于GO语言Context的作用及各种使用方法的相关资料,需要的朋友可以参考下
    2024-01-01
  • GO使用socket和channel实现简单控制台聊天室

    GO使用socket和channel实现简单控制台聊天室

    今天小编给大家分享一个简单的聊天室功能,聊天室主要功能是用户可以加入离开聊天室,实现思路也很简单明了,下面小编给大家带来了完整代码,感兴趣的朋友跟随小编一起看看吧
    2021-12-12
  • go语言中sort包的实现方法与应用详解

    go语言中sort包的实现方法与应用详解

    golang中也实现了排序算法的包sort包,所以下面这篇文章主要给大家介绍了关于go语言中sort包的实现方法与应用的相关资料,文中通过示例代码介绍的非常详细,需要的朋友们可以参考借鉴,下面随着小编来一起学习学习吧。
    2017-11-11
  • Swaggo零基础入门教程

    Swaggo零基础入门教程

    swagger是一套基于OpenAPI规范构建的开源工具,使用RestApi。swagger-ui呈现出来的是一份可交互式的API文档,可以直接在文档页面尝试API的调用
    2023-01-01
  • Golang设计模式工厂模式实战写法示例详解

    Golang设计模式工厂模式实战写法示例详解

    这篇文章主要为大家介绍了Golang 工厂模式实战写法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08

最新评论