Go HTTP服务优雅关闭中出现的小插曲
2022-4-8 18:12:0 Author: mp.weixin.qq.com(查看原文) 阅读量:31 收藏

问题排查

最近小土在修改一个服务,升级过程中在stderr.log文件中意外发现一个panic错误http: Server closed。通过堆栈信息找到服务在HTTP监听时的一段代码。

go func() {
  if err := h.server.ListenAndServe(); nil != err {
     h.logger.Panicf("[HTTPServer] http server start fail or has been close, cause:[%v]", err)
 }
}()       

 httpServer.Start

首先小土这里查看了下源码,如下:

Go版本

go version go1.16.13 darwin/arm64

源代码

net/http/server.go | ListenAndServe

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
 if srv.shuttingDown() {
  return ErrServerClosed
 }
 addr := srv.Addr
 if addr == "" {
  addr = ":http"
 }
 ln, err := net.Listen("tcp", addr)
 if err != nil {
  return err
 }
 return srv.Serve(ln)
}

其中关于ListenAndServe对于错误返回的解释是:

// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
// ListenAndServe 总是返回一个非空错误,在调用Shutdown或者Close方法后,返回的错误是ErrServerClosed。

ErrServerClosed的错误描述正是 http: Server closed

// ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
var ErrServerClosed = errors.New("http: Server closed")

这里就破案了。正是在处理优雅关闭HTTP服务中调用了Shutdown方法,所以导致服务在关闭中抛出了异常错误。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

defer cancel()
if err := h.server.Shutdown(ctx); nil != err {
  h.logger.Errorf("[HTTPServer] http server shutdown cause:[%v]",err)
}

httpServer.Stop

Shutdown源码解析

小土趁此机会也看了下Shutdown的源码,发现了两个比较有意思的方法。唉,平时还是看源码少。

net/http/server.go | Shutdown

// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, then closing all idle connections, and then waiting
// indefinitely for connections to return to idle and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener(s).
//
// When Shutdown is called, Serve, ListenAndServe, and
// ListenAndServeTLS immediately return ErrServerClosed. Make sure the
// program doesn't exit and waits instead for Shutdown to return.
//
// Shutdown does not attempt to close nor wait for hijacked
// connections such as WebSockets. The caller of Shutdown should
// separately notify such long-lived connections of shutdown and wait
// for them to close, if desired. See RegisterOnShutdown for a way to
// register shutdown notification functions.
//
// Once Shutdown has been called on a server, it may not be reused;
// future calls to methods such as Serve will return ErrServerClosed.
func (srv *Server) Shutdown(ctx context.Context) error {
  
  // 这里主要通过原子操作将inShutdown标志位设为1
 srv.inShutdown.setTrue()

 srv.mu.Lock()
  // 调用listenrs中的close方法
 lnerr := srv.closeListenersLocked()
  // 关闭doneChan
 srv.closeDoneChanLocked()
  // 调用注册的Shutdowns方法
 for _, f := range srv.onShutdown {
  go f()
 }
 srv.mu.Unlock()

 pollIntervalBase := time.Millisecond
 nextPollInterval := func() time.Duration {
  // Add 10% jitter
  interval := pollIntervalBase + time.Duration(rand.Intn(int(pollIntervalBase/10)))
  // Double and clamp for next time
  pollIntervalBase *= 2
  if pollIntervalBase > shutdownPollIntervalMax {
   pollIntervalBase = shutdownPollIntervalMax
  }
  return interval
 }

 timer := time.NewTimer(nextPollInterval())
 defer timer.Stop()
 for {
  if srv.closeIdleConns() && srv.numListeners() == 0 {
   return lnerr
  }
  select {
  case <-ctx.Done():
   return ctx.Err()
  case <-timer.C:
   timer.Reset(nextPollInterval())
  }
 }
}

closeListenersLocked 小解

closeListenersLocked 这个方法也很微妙,其中s.listeners是个的map, 遍历执行方法中的键对象的Close()方法。如果调用Close()出错且返回值err不为空,err才等于cerr,这里也没有立马返回err,而是继续遍历执行后续元素的方法。

type Server Struct {
  ...
  listeners  map[*net.Listener]struct{}
  ...
}

func (s *Server) closeListenersLocked() error {
 var err error
 for ln := range s.listeners {
  if cerr := (*ln).Close(); cerr != nil && err == nil {
   err = cerr
  }
 }
 return err
}

for轮询的优雅处理

相信很多同学在这里处理ctx.Done()都会直接for...select操作。Shutdown 而是采用了渐进成倍式的增长轮询时间来控制执行。

pollInterval的增长

最开始默认是1s 间隔,然后10%左右增长,继而成倍增长、最后最大控制在600ms以内。

// 默认轮询间隔1ms
pollIntervalBase := time.Millisecond
// 轮询间隔
nextPollInterval := func() time.Duration {
  // 添加10%的时基抖动增长
  interval := pollIntervalBase + time.Duration(rand.Intn(int(pollIntervalBase/10)))
  // 成倍增长使用,shutdownPollIntervalMax=静默时的最大轮询间隔500ms
  pollIntervalBase *= 2

  if pollIntervalBase > shutdownPollIntervalMax {
    pollIntervalBase = shutdownPollIntervalMax
  }
  return interval
}

// 超时轮训处理 
timer := time.NewTimer(nextPollInterval())
defer timer.Stop()
for {
  if srv.closeIdleConns() && srv.numListeners() == 0 {
    return lnerr
  }
  select {
   case <-ctx.Done():
    return ctx.Err()
   case <-timer.C:
    timer.Reset(nextPollInterval())
  }
}

如何处理?

那么怎么去处理这个在服务监听过程中出现的错误呢?小土这里解决方案如下:

  1. 针对ErrServerClosed的错误做过滤特殊处理
  2. 对于监听错误日志级别从Panicf 改为Fatalf,服务抛出异常日志并以非0正常退出程序。
go func() {
  if err := h.server.ListenAndServe(); nil != err {
   if err == http.ErrServerClosed {
    h.logger.Infof("[HTTPServer] http server has been close, cause:[%v]", err)
   }else {
    h.logger.Fatalf("[HTTPServer] http server start fail, cause:[%v]", err)
   }
  }
}()

小结

经此一小役,小结一下。“源码面前,了无秘密”。建议大家在编码中不能只是简单使用,还是需要去明其意,知其然知其所以然,才能运用自如,BUG排查快速定位。另外源码中很多优秀的代码和设计理念很值得我们在日常编码中借鉴与应用。


往期推荐

我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。

坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxNzY0NDE3NA==&mid=2247489800&idx=1&sn=dc34ac16a805a48127700d6a6f55bcaf&chksm=9be336e9ac94bfff2ba1263cc0c0808502f4fed29ccbbdc84ec62afbbe19487722372196b99d#rd
如有侵权请联系:admin#unsafe.sh