头图

最简单的 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

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

欢迎点赞,关注,收藏

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

好了,本次就到这里

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

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


阿兵云原生
192 声望37 粉丝