以下文章来源于Go招聘 ,作者小土
Golang 相关求职和招聘,以及面试题、经验分享,Go 语言其他知识和职场也是值得分享的。
我是一只可爱的土拨鼠,专注于分享 Go 职场、招聘和求职,解 Gopher 之忧!欢迎关注我。 欢迎大家加入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)
}小土趁此机会也看了下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 这个方法也很微妙,其中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
}
相信很多同学在这里处理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())
}
}
那么怎么去处理这个在服务监听过程中出现的错误呢?小土这里解决方案如下:
ErrServerClosed的错误做过滤特殊处理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 语言必看书籍。
回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!
回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。
回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。
回复「后台」,获取后台开发必看 10 本书籍。
对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇
如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!