V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
nanmu42
V2EX  ›  Go 编程语言

谈 Golang http.Server 安全退出:容易被误用的 Shutdown()方法

  •  1
     
  •   nanmu42 ·
    nanmu42 · 2021-09-23 12:55:22 +08:00 · 3290 次点击
    这是一个创建于 1164 天前的主题,其中的信息可能已经有所发展或是发生改变。

    各位好。

    Go HTTP server 安全退出是一个比较常见的需求,妥善使用可以降低发版时的服务抖动。

    我在最近才发现两年多以来,我的实现一直有问题,原因是我没好好读文档┑( ̄Д  ̄)┍,另外Shutdown()这个方法的 API 设计略微有些毛刺,望文生义容易翻车。

    我把我的经历写了下来,希望能抛砖引玉,欢迎各位交流拍砖。

    谢谢。

    25 条回复    2022-01-15 12:50:02 +08:00
    SorcererXW
        1
    SorcererXW  
       2021-09-23 13:28:14 +08:00   ❤️ 1
    我的理解是不是将退出操作放在主协程,其实 server 放在另外一个协程,就能避免立即退出?

    func main() {
    go server.Server()
    <- signal
    server.Shutdown(ctx)
    }
    nanmu42
        2
    nanmu42  
    OP
       2021-09-23 13:32:03 +08:00
    @SorcererXW 这里就见仁见智了,ListenAndServe()在 goroutine 中的话,错误处理大概率是 log.Fatal(err)这样的操作,如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的。我这里用了一些额外的复杂度让安全退出的逻辑更圆满了一些。
    v2Geeker
        3
    v2Geeker  
       2021-09-23 14:23:09 +08:00   ❤️ 1
    见识了。

    我一般都是 ListenAndServe 和 Shutdown 都放在 2 个不同的 gorountine 中,用 sync.WaitGroup 的 Wait 来等待结束,于是我好像从来没意识到 Shutdown 有这样的问题。
    whitedroa
        4
    whitedroa  
       2021-09-23 14:44:59 +08:00   ❤️ 1
    @nanmu42 没太看懂:“错误处理大概率是 log.Fatal(err)这样的操作” 这句话是什么意思呢

    “如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的”
    这里 main 中的 defer 为什么不会执行呢,是因为其他协程 panic 导致程序直接退出吗?
    nanmu42
        5
    nanmu42  
    OP
       2021-09-23 15:30:09 +08:00
    @v2Geeker 我是错了将近两年,涉及好几个服务,直到线上日志观察到了问题才醒悟的。
    FrankAdler
        6
    FrankAdler  
       2021-09-23 15:30:31 +08:00   ❤️ 1
    @nanmu42 #2 想让 main 的 defe 能执行,就需要让 main 正常退出,那在 goroutine 里出错错误的时候,发送 sign 到 main 里等等的 chan 就行了,比如

    defer func() {
    log.Println("defer")
    }()

    server := http.Server{
    Addr: fmt.Sprintf(":%d", *port),
    Handler: downright.SlowHandler(*sleepSeconds),
    }

    quit := make(chan os.Signal, 1)

    go func() {
    err := server.ListenAndServe()
    if err != http.ErrServerClosed {
    log.Printf("ListenAndServe err: %v", err)
    quit <- syscall.SIGTERM
    }
    }()

    signal.Notify(quit, os.Interrupt)
    <-quit
    log.Println("waiting for shutdown finishing...")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
    log.Fatalf("shutdown err: %v", err)
    }
    log.Println("shutdown finished")

    ListenAndServe 不管出现什么级别的错误都可以处理(只要不调用 os.Exit ),毕竟 main 一直在等待新号阻塞着
    FrankAdler
        7
    FrankAdler  
       2021-09-23 15:30:59 +08:00
    有几个错别字,见谅。。
    nanmu42
        8
    nanmu42  
    OP
       2021-09-23 15:34:16 +08:00
    @whitedroa 在另一个 goroutine 里做 ListenAndServe(),它的返回值一般是用 log.Fatal()来接的,要不然就不晓得 HTTP 服务启停状态了。
    log.Fatal()调用的是 os.Exit(),这个方法会造成 go 程序直接退出,main()里的 defer 函数不运行(博文里链了 godoc 链接)。
    当然也可以不用 log.Fatal(),自己搞定同步,但是那样复杂度上来了。
    nanmu42
        9
    nanmu42  
    OP
       2021-09-23 15:36:30 +08:00
    @FrankAdler 是呢,这样可以让 main()执行完,我们的思路挺类似呢。
    zouzou0208
        10
    zouzou0208  
       2021-09-23 15:38:06 +08:00   ❤️ 1
    写的好,学到了,感谢感谢。之前没主要到过这个。还给出了代码,真贴心。
    hhaobao
        11
    hhaobao  
       2021-09-23 16:08:23 +08:00   ❤️ 1
    go 1.16 后, Notify 可以改成 NotifyContext
    Yoock
        12
    Yoock  
       2021-09-23 16:21:41 +08:00   ❤️ 1
    学到了
    index90
        13
    index90  
       2021-09-23 17:30:06 +08:00   ❤️ 1
    你这样子改,如果一个程序要启动多个 http 服务就不行了。
    如果你是担心 goroutine 启动 server,server 意外退出的问题,用 errgroup 好了。
    比较完善的多 routine 和退出处理,用 rungourp 一把梭
    hu8245
        14
    hu8245  
       2021-09-23 18:14:05 +08:00
    with context 啊
    nanmu42
        15
    nanmu42  
    OP
       2021-09-24 10:04:10 +08:00
    @index90 兄台说得对,这个方案不适合一个程序要启动多个 http 服务,只覆盖了部分用例。
    感谢你的分享。
    lesismal
        16
    lesismal  
       2021-09-24 12:34:28 +08:00
    Shutdown 只是减少了停服的短暂过程的抖动数量,对于当时 qps/tps 非常高的服务效果好点。但仍可能存在在途请求(网络链路、尚未被读取的内核缓冲区中的数据)被放弃、请求方失败、超时的情况。

    所以虽然冠以了 graceful 之名,只是 part of graceful,仍然需要业务层来保证需求的实现,以及集群架构层面的高可用性部署、调度等相关支持,业务逻辑相关的重试、幂等保证是必需品。

    即使不是程序本身的导致的抖动,也存在其他网络链路抖动的影响比如 ISP 线路故障,仍然是需要集群架构层面的高可用性部署、调度等做相关的强支持,而这些支持能够同时从更高层面照顾到程序引起的抖动造成的影响。( ISP 、程序抖抖可能造成请求方重试、累积踩踏雪崩之类的,都是需要网络、运维、高可用部署相关的这些保障)

    有了业务和运维层面的保证,对于绝大多数业务量级而言,程序引起的短暂抖动其实影响很小。而对于中小厂的流量,抖那么一下,受影响的请求数也是极小的。

    所以其实 graceful Shutdown,虽然照样用,但实际发挥的用处不大。

    顺便蹭蹭,欢迎关注我的两个框架,高性能、海量并发相关:
    https://www.v2ex.com/t/794435#reply3
    zoharSoul
        17
    zoharSoul  
       2021-09-24 21:01:37 +08:00   ❤️ 1
    博客是用什么搭的啊 挺好看的
    Goat121
        18
    Goat121  
       2021-09-25 16:06:43 +08:00   ❤️ 1
    @index90 请教下这里的 rungroup 是指什么呢
    index90
        19
    index90  
       2021-09-26 09:27:52 +08:00   ❤️ 1
    Goat121
        20
    Goat121  
       2021-09-27 12:05:38 +08:00
    @index90 谢谢 去看看先
    nanmu42
        21
    nanmu42  
    OP
       2021-10-03 10:24:08 +08:00
    @zoharSoul 谢谢夸奖。Hugo 和 Zzo 主题,稍微自己改了一点点。
    Lihanx9
        22
    Lihanx9  
       2022-01-05 17:13:51 +08:00   ❤️ 1
    https://gin-gonic.com/docs/examples/graceful-restart-or-stop/

    是不是和 Gin 给的这个示例异曲同工?如果直接用 ctx.Done() 的 channel ,是不是就可以不用自己创建一个 s.shutdownFinished 这个 channel 了呢?😳
    nanmu42
        23
    nanmu42  
    OP
       2022-01-10 13:13:18 +08:00
    @lihanx9 啊,我怀疑 Gin 的这个例子是错的…
    Lihanx9
        24
    Lihanx9  
       2022-01-14 11:31:04 +08:00
    @nanmu42 好吧,那我再理解理解😂😂😂 谢谢
    nanmu42
        25
    nanmu42  
    OP
       2022-01-15 12:50:02 +08:00 via iPhone
    @lihanx9 Shutdown 这个 API 挺容易踩雷的,不过服务退出和重启并不经常发生,实际影响还是有限。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2795 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 09:32 · PVG 17:32 · LAX 01:32 · JFK 04:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.