最简单的 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
以上就是从一个不会优雅关闭到学会常用优雅关闭方法的简单路径,希望对你有用哦
欢迎点赞,关注,收藏
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~