原来服务端的退出姿势也可以这么优雅

简介: 相信写过 golang 的 xdm 都写过 http 服务器吧,用 golang 写服务器是不是很爽呀

相信写过 golang 的 xdm 都写过 http 服务器吧,用 golang 写服务器是不是很爽呀

最简单的 http 服务端

咱们来写一个简单的 http 服务器

func main() {
  srvMux := http.NewServeMux()
  srvMux.HandleFunc("/getinfo", getinfo)
  http.ListenAndServe(":9090", srvMux)
}
func getinfo(w http.ResponseWriter, r *http.Request) {
  fmt.Println("i am xiaomotong!!!")
  w.Write([]byte("you are access right!!\n"))
}

这个功能非常简单,就是监听了本地的 9090 端口,并且其中有一个 url 是会处理请求的,/getinfo ,咱们可以通过如下指令来请求一下看看效果

# curl localhost:9090/getinfo
you are access right!!

明确是可以正常访问的,且也会拿到我们对应的信息,服务器的日志也是正常的

咱们思考一下,这个时候如果遇到了意外,程序崩溃了,panic 了,或者我们认为的 kill  掉了,我们如何判断服务端是如何退出的呢?

加入 信号的 服务端

我们写 C/C++ 的时候对于信号应该不陌生吧,在 golang 里面,我们也加入信号来识别是否是认为 kill 程序的

linux 里面可以通过 man kill 查看 kill 指令的详细说明

image.png

这里我们可以看到一个kill -9 是对应的 SIGKILL 信号 ,我们还知道 SIGINT 信号是 Ctrl-C 的时候会发出这个信号,也是一个中断信号,如果对于这点不清楚话,可以网络上搜索一下  linux 信号列表

func main() {
  sig := make(chan os.Signal)
  signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
  srvMux := http.NewServeMux()
  srvMux.HandleFunc("/getinfo", getinfo)
  go http.ListenAndServe(":9090", srvMux)
   fmt.Println(<-sig)
}

http.ListenAndServe 是阻塞的,则此处咱们监听 9090 的服务是开了一个单独处理

验证一下

# go run main.go
^Cinterrupt

这个时候,我们的 http 服务器,已经能够区分信号了,知道自己是如何退出的了

咱们的需求有慢慢的增加,实际工作中,肯定不能做的这么 cuo

优雅的退出

工作中,我们带有 http 的服务端,肯定还有别的处理逻辑,例如读写文件,GRPC 通信,或者是使用数据库,那么我们程序关闭情况,还是要根据情况来处理,要遵循原子性

有如下 2 种情况:

  • 对于数据没有严格的质量要求,程序 panic 也无所谓,那么这个时候不用优雅关闭也没有啥问题
  • 对于上述说到的会操作数据库,读写文件等等会修改数据的,这里可不期望操作数据的过程中被中断,我们要遵循原子性,咱们的程序需要提供一个缓冲的时间,来优雅的退出

正常工作中退出必须是优雅的

如何实现优雅退出呢?

例如上面的例子,当主协程收到了中断信号后,就会马上退出程序,子协程也会相应退出

如果需要主协程等待子协程处理完当前手里的活再退出,那么我们是不是需要让主协程和子协程相互通信,才有可能实现呢?

使用 2 个 channel 来实现优雅关闭

这个方法比较容易想到

实现大体分为 2 步走:

  • 主协程收到中断信号后,通知子协程优雅关闭 ,这里命名为  stopCh
  • 子协程收到通知后,处理完手头的通知主协程关闭程序,这里命名为 closeCh
func main() {
  sig := make(chan os.Signal)
  signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
  stopCh := make(chan int)
  closeCh := make(chan int)
  srvMux := http.NewServeMux()
  srvMux.HandleFunc("/getinfo", getinfo)
  go http.ListenAndServe(":19999", srvMux)
  go func(stopCh, closeCh chan int) {
    for {
      select {
      case <-stopCh:
        fmt.Println(" processing tasks")
                // 模拟正在处理数据
        time.Sleep(time.Second*time.Duration(2))
        fmt.Println("process over ")
        closeCh <- 1
      }
    }
  }(stopCh, closeCh)
  <-sig
  stopCh <- 1
  <-closeCh
  fmt.Println("close server ")
}

此处我们可以看出使用了 2 个通道来让主协程和子协程相互通信

开辟一个协程,执行匿名函数来监听 stopCh  通道是否有数据,若有数据,说明主协程收到了信号,并且通知子协程要优雅关闭了

这个时候,子协程做完自己的事情,就在  closeCh 写入数据,通知主协程可以正常关闭程序了

使用嵌套的 channel 来实现

使用 嵌套的 channel 来实现优雅关闭,可能一下子还想不到,不过官网有给我们一些方向

实现思路是:

  • 使用一个通道 stopCh,通道 stopCh 里面的元素是另外一个通道 tmpCh
  • 当主协程收到退出信号时,在 stopCh 中写入数据 tmpCh,并开始监听 tmpCh 是否有数据
  • 子协程从 stopCh  读取到数据 tmpCh 时,便知道自己需要优雅关闭了,处理完自己的事情之后,子协程往 tmpCh  写入数据
  • 主协程监听到 tmpCh  有数据,则退出程序
func main() {
  sig := make(chan os.Signal)
  signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
  stopCh := make(chan chan struct{})
  srvMux := http.NewServeMux()
  srvMux.HandleFunc("/getinfo", getinfo)
  go http.ListenAndServe(":19999", srvMux)
  go func(stopCh chan chan struct{}) {
    for {
      select {
      case tmpCh:=<-stopCh:
        fmt.Println(" processing tasks")
        time.Sleep(time.Second*time.Duration(2))
        fmt.Println("process over ")
        tmpCh <- struct{}{}
      }
    }
  }(stopCh)
  tmpCh := make(chan struct{})
  <-sig
  stopCh <- tmpCh
  <-tmpCh
  fmt.Println("close server ")
}

上面 2 种方法都比较类似,都是使用通道来实现优雅关闭的功能,通道是 golang 天生的数据结构,咱们要用起来

使用 golang 标准解法 context

使用 golang 的 context ,能够更好的实现优雅关闭的问题

别以为 context 只会拿来传递数据,context 也是可以控制 子协程的生命周期的

func main() {
  sig := make(chan os.Signal)
  signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
  stopCh := make(chan struct{})
    // 创建一个上下文
  ctx,cancle := context.WithCancel(context.Background())
  srvMux := http.NewServeMux()
  srvMux.HandleFunc("/getinfo", getinfo)
  go http.ListenAndServe(":19999", srvMux)
  go func(ctx context.Context,stopCh chan struct{}) {
    for {
      select {
      case <-ctx.Done():
        fmt.Println(" processing tasks")
        time.Sleep(time.Second*time.Duration(2))
        fmt.Println("process over ")
        stopCh <- struct{}{}
      }
    }
  }(ctx,stopCh)
  <-sig
  cancle()
  <-stopCh
  fmt.Println("close server ")
}

此处我们使用 context 的方式,当主协程关闭上下文的时候,子协程就会从通道到读取到数据,进而进行优雅关闭,我们可以看到源码,ctx.Done()  的返回值也是一个通道

image.png

主协程等待所有子协程优雅关闭实现方法

上面我们说到的都是主协程等待 1 个子协程优雅关闭后,自己关闭程序

那么实际工作中肯定是不止一个协程的,咱们要做的优雅,那就优雅到底 ,此处我们的处理方式是 golang 中 context + sync.WaitGroup 的方式来实现

func main() {
  sig := make(chan os.Signal)
  signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
  ctx, cancle := context.WithCancel(context.Background())
  mywg := sync.WaitGroup{}
  // 控制 5 个子协程的声明周期
  mywg.Add(5)
  for i := 0; i < 5; i++ {
    go func(ctx context.Context) {
      defer mywg.Done()
      for {
        select {
        case <-ctx.Done():
          fmt.Println(" processing tasks")
          time.Sleep(time.Second * time.Duration(1))
          fmt.Println("process over ")
          return
        default:
          time.Sleep(time.Second * time.Duration(1))
        }
      }
    }(ctx)
  }
  <-sig
  cancle()
  // 等待所有的子协程都优雅关闭
  mywg.Wait()
  fmt.Println("close server ")
}

上述代码中,我们使用  sync.WaitGroup 控制 5 个子协程的生命周期,当主协程收到中断信号后,cancle() 掉 ctx

每一个子协程都能从  ctx.Done() 读取到数据,自行处理完毕手中事情后

最终 defer mywg.Done() ,主协程  mywg.Wait() 等待所有协程都优雅关闭后,自己也关闭了自己的程序

验证效果

# go run main.go
^C processing tasks
processing tasks
processing tasks
processing tasks
processing tasks
process over
process over
process over
process over
process over
close server

以上就是从一个不会优雅关闭到学会常用优雅关闭方法的简单路径,希望对你有用哦

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

image.png

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~


相关文章
|
Cloud Native Linux Go
原来服务端的退出姿势也可以这么优雅
原来服务端的退出姿势也可以这么优雅
|
7月前
|
数据安全/隐私保护 Windows
远程重启停止响应的服务器
远程重启停止响应的服务器
46 2
|
Kubernetes Cloud Native Shell
k8s pod 中的程序为啥服务优雅关闭不生效?收不到 sigterm 信号?
咱们工作的环境在不断的变好,我们也会思考去提升程序运行的环境,让我们的服务更加容易部署,极简维护,现在很多公司都在向着 devpos 发展,殊不知已经被某些大企玩剩下了
160 0
k8s pod 中的程序为啥服务优雅关闭不生效?收不到 sigterm 信号?
|
数据安全/隐私保护
阿里云 RPA 在与服务器连接断开时会显示这个警告
阿里云 RPA 在与服务器连接断开时会显示这个警告
184 3
|
SQL 关系型数据库 MySQL
开启关闭服务器以及登录退出客户端 | 学习笔记
快速学习开启关闭服务器以及登录退出客户端。
214 1
开启关闭服务器以及登录退出客户端 | 学习笔记
|
网络安全 数据安全/隐私保护
解决 SSH 无操作自动断开 | pychram 超时无响应
SSH 是用于与远程服务器建立加密通信通道的,因此配置涉及服务端和客户端
508 0
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
1818 0
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
通过nc命令模拟客户端或服务器端程序
通过nc命令模拟客户端或服务器端程序
663 0
|
SQL 关系型数据库 MySQL
开启关闭服务器以及登录退出客户端|学习笔记
快速学习开启关闭服务器以及登录退出客户端
开启关闭服务器以及登录退出客户端|学习笔记