处理 Go 语言服务端在 Accept 时的临时错误
最近在看 gRPC 的源代码,看到了 gRPC 服务器在 net.Listener.Accept()
时的异常处理逻辑,发现我目前不管是在个人项目还是在公司级项目都没有处理 Accept 的错误,直接 panic 或退出循环了。
有点自惭形秽,是时候学习并改进一波代码了。
我直接把 gRPC 这段逻辑的代码贴出来吧(简化并去掉与本文章不相关的代码,并加上了注释):
// Accept 临时失败时的等待时长,连续出错将累加到最大允许值
var tempDelay time.Duration // how long to sleep on accept failure
for {
rawConn, err := lis.Accept()
if err != nil {
// 这里判断是不是临时错误
// 临时错误可能有很多原因,比如等待的过程中遇到了中断需要处理
// 那么类 Linux 操作系统可能返回 EAGAIN 错误,这个错我们就应该重试
// Temporary() 来源于:https://github.com/golang/go/blob/c4fed25553ee266ed9cb3a98e7a33a82af110ed4/src/net/net.go#L396-L400
if ne, ok := err.(interface {
Temporary() bool
}); ok && ne.Temporary() {
if tempDelay == 0 {
// 设置初始重试等待时间为 5ms
tempDelay = 5 * time.Millisecond
} else {
// 以后连续的错误重试等待时间是之前的 2 倍
tempDelay *= 2
}
// 最大等待时长为 1s
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
// 等待超时或退出信号
timer := time.NewTimer(tempDelay)
select {
case <-timer.C:
case <-s.quit.Done():
timer.Stop()
return nil
}
// 然后进行重试
continue
}
// 如果不是临时错误,则作其它处理
// 比如,直接退出循环,返回错误
if s.quit.HasFired() {
return nil
}
return err
}
// 如果 Accept 是成功的,则重置等待时间为 0
tempDelay = 0
// s.handleRawConn(rawConn)
}
这段代码和我写的代码的最大不同就是增加了 Accept 出错可能是因为临时错误的处理逻辑,临时错误属于可能重试可能成功的错误,所以有必要在有限度的条件下进行重试。 这应该是编写更加稳健的程序的良好习惯。
可惜我用 C 写 Linux 的程序不多,还没有养成这个习惯。 而在 Windows 上,我记得是没有这个错误的,操作系统会自动处理因为中断程序的原因导致的重试。
参考文章: