Go HTTP服务用了优雅关闭,为什么还是报错?

Go 写的 HTTP Server 怎么优雅进行关闭,相信大家都知道了吧,如果还不知道的,建议你们看下我的出道文章:深入学习用Go编写HTTP服务器 ae83cec5032a192d93a2a44ce669f85a.png

不过道理懂了,实操的时候总是有课可能会有点翻车小插曲,这里给大家分享一个Go HTTP Server 优雅关闭了,还是panic 的案例,其实就是一些没注意到的使用小细节导致的问题。文章转载自「Go招聘」


问题排查

最近小土在修改一个服务,升级过程中在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排查快速定位。另外源码中很多优秀的代码和设计理念很值得我们在日常编码中借鉴与应用。


欢迎关注Go招聘公众号,获取Go专题大厂内推面经简历股文等相关资料可回复和点击导航查阅。

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 👆

网管整理了一本《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】领取!还有一本《k8s 入门实践》讲解了常用软件在K8s上的部署过程,公众号回复【k8s】即可领取!

觉得有用就点个在看  👇👇👇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值