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