Go | http.Server 优雅退出方式简析

Go 1.8 为我们带来了 http 服务优雅退出的方式:Server.Shutdown。对于该方法,代码中的解释是:

Shutdown 方法会在不干扰任何活跃连接的情况下关闭服务器。首先,它会关闭所有开着的监听器,然后关闭所有空闲连接,接着无限等待所有连接变成空闲状态,最后关闭。

如果提供的 context.Context 对象在关闭完成之前过期了,那么,Shutdown 方法返回该 Context 对象的错误信息。否则,它会将正在关闭的服务器的底层监听器的错误返回(如果有的话)。

一旦调用了 Shutdown 方法,ServeListenAndServeListenAndServeTLS 会立即返回 ErrServerClosed。需要确保程序不退出,而是等待 Shutdown 返回。

Shutdown 并不会像 WebSockets 那样尝试关闭或者等待被劫持的连接。Shutdown 的调用者应该在需要的时候,挨个通知这些长期存在的连接关闭,并且等待它们关闭。

接下来我们来看下 Shutdown 方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (srv *Server) Shutdown(ctx context.Context) error {
atomic.AddInt32(&srv.inShutdown, 1)
defer atomic.AddInt32(&srv.inShutdown, -1)

srv.mu.Lock()
lnerr := srv.closeListenersLocked()
srv.closeDoneChanLocked()
for _, f := range srv.onShutdown {
go f()
}
srv.mu.Unlock()

ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
for {
if srv.closeIdleConns() {
return lnerr
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}

基于 go 1.9.2

上面的代码做了几件事:

  1. 将 Server 的字段 inShutdown int32 加一。这个字段用于 Server 非公开方法 shuttingDown 中,非零表示 Server 正在关闭。
  2. 调用 closeListenersLocked 方法,关闭所有打开的监听器。
  3. 调用 closeDoneChanLocked 方法,关闭 doneChan chan struct{}。从而通知 ServeListenAndServeListenAndServeTLS 退出并返回 ErrServerClosed 错误。
  4. 将所有使用 RegisterOnShutdown 方法注册的方法(保存在 onShutdown []func() 中)放在单独的 goroutine 中调用,并且不等待方法返回。这些方法不应该等待关闭完成。
  5. 创建一个定时器,定时时间由 shutdownPollInterval 指定,默认是 500ms。目前没有可以修改该值的方法。
  6. 每到步骤 5 创建的定时时间调用一次 closeIdleConns 方法关闭空闲连接。无限循环直到该方法返回 true(表示服务器已经处于静默模式),或者当 ctx 过期了。如果是前者,则返回步骤 2 的执行结果。后者则返回 ctx 的错误信息。

由此可见,Shutdown 方法主要是做了两件事:关闭监听器和关闭空闲连接。

前者直接调用监听器的 Close 方法。这里需要注意的是,一旦该方法调用失败,只会保存错误信息,并且继续调用下一个监听器的 Close 方法。如果存在多个监听器关闭错误,也只会返回其中一个错误。

我们来看看后者的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// closeIdleConns closes all idle connections and reports whether the
// server is quiescent.
func (s *Server) closeIdleConns() bool {
s.mu.Lock()
defer s.mu.Unlock()
quiescent := true
for c := range s.activeConn {
st, ok := c.curState.Load().(ConnState)
if !ok || st != StateIdle {
quiescent = false
continue
}
c.rwc.Close()
delete(s.activeConn, c)
}
return quiescent
}

上面的代码做了几件事:

  1. 检查 activeConn map[*conn]struct{} 中的每个连接状态
  2. 如果连接不是 idle 的,那么设置服务器为非静默模式,然后继续检查下一个连接
  3. 如果连接已经是 idle 的了,那么关闭该连接,并且将其从 activeConn 中删除
  4. 最后返回服务器是否处于静默状态。

和一般的 Close 方法进行对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Close immediately closes all active net.Listeners and any
// connections in state StateNew, StateActive, or StateIdle. For a
// graceful shutdown, use Shutdown.
//
// Close does not attempt to close (and does not even know about)
// any hijacked connections, such as WebSockets.
//
// Close returns any error returned from closing the Server's
// underlying Listener(s).
func (srv *Server) Close() error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.closeDoneChanLocked()
err := srv.closeListenersLocked()
for c := range srv.activeConn {
c.rwc.Close()
delete(srv.activeConn, c)
}
return err
}

区别如下:

  1. Shutdown 方法会调用注册的关闭时执行的方法,而 Close 方法没有。
  2. Shutdown 方法会等待所有活跃连接变成 idle 状态才关闭连接,而 Close 方法则直接关闭连接。