处理 Go 语言服务端在 Accept 时的临时错误

陪她去流浪 桃子 2020年07月29日 编辑 阅读次数:2263

最近在看 gRPC 的源代码,看到了 gRPC 服务器在 net.Listener.Accept() 时的异常处理逻辑,发现我目前不管是在个人项目还是在公司级项目都没有处理 Accept 的错误,直接 panic 或退出循环了。 有点自惭形秽,是时候学习并改进一波代码了。

我直接把 gRPC 这段逻辑的代码贴出来吧(简化并去掉与本文章不相关的代码,并加上了注释):

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 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 上,我记得是没有这个错误的,操作系统会自动处理因为中断程序的原因导致的重试。

参考文章:

标签:Go · gRPC