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

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

最简单的 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 指令的详细说明

这里我们可以看到一个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() 的返回值也是一个通道

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

上面我们说到的都是主协程等待 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

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

欢迎点赞,关注,收藏

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

好了,本次就到这里

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

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

相关文章
|
3月前
|
数据安全/隐私保护 Windows
远程重启停止响应的服务器
远程重启停止响应的服务器
28 2
|
Linux Go 数据库
原来服务端的退出姿势也可以这么优雅
相信写过 golang 的 xdm 都写过 http 服务器吧,用 golang 写服务器是不是很爽呀
|
SQL 关系型数据库 MySQL
开启关闭服务器以及登录退出客户端 | 学习笔记
快速学习开启关闭服务器以及登录退出客户端。
196 1
开启关闭服务器以及登录退出客户端 | 学习笔记
|
网络安全 数据安全/隐私保护
解决 SSH 无操作自动断开 | pychram 超时无响应
SSH 是用于与远程服务器建立加密通信通道的,因此配置涉及服务端和客户端
425 0
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
1664 0
开机显示被调用的对象已与其客户端断开连接,解决方案亲测有效
|
Shell Linux
linux配置超时不操作自动退出登录TMOUT
linux配置超时不操作自动退出登录TMOUT
1281 0
|
Web App开发 前端开发 API
你知道关闭页面时怎么向后台发送消息吗?
这两天碰到一个需求:在用户刷新页面或者关闭页面的时候,前端要给后台发一条请求,释放该页面的授权占用。
186 0
你知道关闭页面时怎么向后台发送消息吗?
|
网络协议
如何处理TCPSocket客户端与服务器端连接中断后的异常
如图,我通过TCP Socket将客户端与服务器建立起双向连接,一旦我关闭客户端,服务器端就会打印如下错误消息:
如何处理TCPSocket客户端与服务器端连接中断后的异常
|
SQL 关系型数据库 MySQL
开启关闭服务器以及登录退出客户端|学习笔记
快速学习开启关闭服务器以及登录退出客户端
开启关闭服务器以及登录退出客户端|学习笔记