已发表的技术专栏
0 grpc-go、protobuf、multus-cni 技术专栏 总入口
4 grpc、oauth2、openssl、双向认证、单向认证等专栏文章目录)
本篇文章我们从源码的视角来分析重试机制的原理。
1、源码分析入口 |
进入grpc-go/stream.go文件中的withRetry方法里
1.func (cs *clientStream) withRetry(op func(a *csAttempt) error, onSuccess func()) error {
2. cs.mu.Lock()
3. for {
4. if cs.committed {
5. cs.mu.Unlock()
6. return op(cs.attempt)
7. }
8. a := cs.attempt
9. cs.mu.Unlock()
10. err := op(a)
11. cs.mu.Lock()
12. if a != cs.attempt {
13. continue
14. }
15. if err == io.EOF {
16. <-a.s.Done()
17. }
18. if err == nil || (err == io.EOF && a.s.Status().Code() == codes.OK) {
19. onSuccess()
20. cs.mu.Unlock()
21. return err
22. }
23. if err := cs.retryLocked(err); err != nil {
24. cs.mu.Unlock()
25. return err
26. }
27. }
28.}
withRetry主要流程说明
- 第10行:执行具体的业务函数,如创建流阶段的函数op,发送阶段的函数op等
- 第15-26行:具体执行业务函数后的逻辑处理
- 第18-22行:针对的是,执行结果成功的处理逻辑
- 第19行:成功后,执行传进来的函数onSuccess函数
- 第23-26行:针对的是,执行结果失败的从逻辑逻辑,底层校验是否允许重试等操作
- 第18-22行:针对的是,执行结果成功的处理逻辑
其实,代码实现主体思路是:
- 先执行业务逻辑,
- 再根据业务逻辑的执行结果来判断是否进入重试机制流程
只要执行op成功后,就会执行onSuccess()函数,(切面操作)
如果传递的onSuccess函数,是bufferForRetryLocked函数,就具备了重试功能
如果传递的onSuccess函数,是commitAttemptLocked函数,就不具备重试功能
如果传递的onSuccess函数,是其他函数的,可能就会有其他功能了
注意:
withRetry属于clientStream,属于客户端的;
并没有发现服务器端也有类似的重试机制功能;
服务器端将执行结果反馈给客户端时,并没有使用重试机制。
2、重试机制withRetry实现方式的特点 |
该方法withRetry的特点:
- 将重试机制流程跟具体的业务逻辑隔离开,
- 具体的业务是通过函数参数op,以及onSuccess 传进来的;
- 这样的话,该重试机制流程可以支持不同的场景;如创建流场景,传输数据的场景等等
3、当业务执行失败时,重试机制retryLocked如何处理此种情况? |
进入retryLocked方法里:
1.func (cs *clientStream) retryLocked(lastErr error) error {
2. for {
3. cs.attempt.finish(lastErr)
4. if err := cs.shouldRetry(lastErr); err != nil {
5. cs.commitAttemptLocked()
6. return err
7. }
8. cs.firstAttempt = false
9. if err := cs.newAttemptLocked(nil, nil); err != nil {
10. return err
11. }
12. if lastErr = cs.replayBufferLocked(); lastErr == nil {
13. return nil
14. }
15. }
16.}
主流程说明:
- 第4行:根据执行结果的错误信息,判断是否允许重试
- 若不允许重试:调用commitAttemptLocked
- 将cs.committed = true
- 将存储执行函数的缓存cs.buffer重置为nil
- 若允许重试:
- 第9行:创建csAttempt,并且重新选择状态为Ready的链接
- 第12行:从缓存cs.buffer里,获取已经成功执行过的函数,重新执行一次;注意,这里并没有执行本次执行失败的方法,执行的是前面运行正确的方法
- 若不允许重试:调用commitAttemptLocked
3.1、假设某一操作失败了,客户端是如何判断是不是允许重试呢? |
进入grpc-go/stream.go文件中的shouldRetry方法里:
1.// shouldRetry returns nil if the RPC should be retried; otherwise it returns
2.// the error that should be returned by the operation.
3.func (cs *clientStream) shouldRetry(err error) error {
4. unprocessed := false
5. if cs.attempt.s == nil {
6. pioErr, ok := err.(transport.PerformedIOError)
7. if ok {
8. // Unwrap error.
9. err = toRPCErr(pioErr.Err)
10. } else {
11. unprocessed = true
12. }
13. if !ok && !cs.callInfo.failFast {
14. return nil
15. }
16. }
17. if cs.finished || cs.committed {
18. return err
19. }
20. if cs.attempt.s != nil {
21. <-cs.attempt.s.Done()
22. unprocessed = cs.attempt.s.Unprocessed()
23. }
24. if cs.firstAttempt && unprocessed {
25. return nil
26. }
27. if cs.cc.dopts.disableRetry {
28. return err
29. }
30. pushback := 0
31. hasPushback := false
32. if cs.attempt.s != nil {
33. //------省略掉跟Trailer相关的代码,暂不分析
34. }
35. var code codes.Code
36. if cs.attempt.s != nil {
37. code = cs.attempt.s.Status().Code()
38. } else {
39. code = status.Convert(err).Code()
40. }
41. rp := cs.methodConfig.retryPolicy
42. if rp == nil || !rp.retryableStatusCodes[code] {
43. return err
44. }
45. if cs.retryThrottler.throttle() {
46. return err
47. }
48. if cs.numRetries+1 >= rp.maxAttempts {
49. return err
50. }
51. var dur time.Duration
52. if hasPushback {
53. dur = time.Millisecond * time.Duration(pushback)
54. cs.numRetriesSincePushback = 0
55. } else {
56. fact := math.Pow(rp.backoffMultiplier, float64(cs.numRetriesSincePushback))
57. cur := float64(rp.initialBackoff) * fact
58. if max := float64(rp.maxBackoff); cur > max {
59. cur = max
60. }
61. dur = time.Duration(grpcrand.Int63n(int64(cur)))
62. cs.numRetriesSincePushback++
63. }
64. t := time.NewTimer(dur)
65. select {
66. case <-t.C:
67. cs.numRetries++
68. return nil
69. case <-cs.ctx.Done():
70. t.Stop()
71. return status.FromContextError(cs.ctx.Err()).Err()
72. }
73.}
shouldRetry方法:
- 返还的是err时,不允许重试,
- 返还的是nil时,允许重试
主要流程说明:
- 第5-16行:针对的是cs.attempt.s为空的情况,即创建流失败的情况;
- 第17-19行:若客户端流结束,或者cs.committed为true时,不允许重试
- 第20-26行:若是第一次失败,并且创建流还没有处理的话,就可以允许重试
- 第27-29行:disableRetry 默认值为true,不允许重试的;需要通过环境变量的设置来更新此值
- 第30-72行:从用户定义的重试策略角度,来分析是否允许重试
- 第35-40行:获取状态码,如OK,Unknown,Unimplemented,FailedPrecondition,Unavailable等
- 第41-44行:用户定义了一些状态码存储在RetryableStatusCodes里;当code状态码存在于用户的定义状态码里时,才允许重试
- 第45-47行:默认不会创建retryThrottler,即不会执行这里
- 第48-50行:判断重试次数,是否超过了用户规定的最大重试次数,超过时不允许重试;
- 第52-54行:可以先不用关心
- 第56-63行:根据重试参数,定义了一些规则,最终计算出dur;
- 第64-72行:创建定时器,定时时长设置为dur,定时结束后,累加重试次数;
- 也就是说,需要等待dur时长,才能进行重试;
- 而且,当cs.numRetriesSincePushback值,不断增加时,导致下次计算fact的值也不断变化,因此每次等待的时长会变化;
本方法就做了一件事,就是从不同的角度来分析,允不允许进行重试
3.2、当本次操作失败了,客户端如何重试前几步的操作呢? |
进入grpc-go/stream.go文件中的replayBufferLocked方法里:
1.func (cs *clientStream) replayBufferLocked() error {
2. a := cs.attempt
3. for _, f := range cs.buffer {
4. if err := f(a); err != nil {
5. return err
6. }
7. }
8. return nil
9.}
第3-7行:依次执行存储在cs.buffer里的函数,就是将以前执行成功的业务逻辑,再重新执行一次
4、假设客户端发送数据阶段失败时,整体模拟一遍,看看重试机制是如何处理的? |
看完前面的分析后,可能不是很理解整个流程,现在我们从头模拟一次:
假设发送数据失败了,发送数据函数为op代码如下:
op := func(a *csAttempt) error {
err := a.sendMsg(m, hdr, payload, data)
m, data = nil, nil
return err
}
err = cs.withRetry(op, func() { cs.bufferForRetryLocked(len(hdr)+len(payload), op) })
模拟流程如下:
-
客户端执行流的创建,创建流的函数为op, 在withRetry方法里的第10行执行op, 执行成功后,通过withRetry方法里的onSuccess()函数,即bufferForRetryLocked方法,将创建流的函数op存储到切片cs.buffer里
-
接下来,客户端开始调用传输数据的函数op,
-
在withRetry方法里的第10行执行op, 假设执行失败后,开始执行第23行retryLocked方法
-
进入shouldRetry方法里,判断是否允许重试
- 若最终返还的是err,又重新回到retryLocked里,执行结果是err,继续退出到withRetry方法里,最终退出循环,没有重试
- 若最终返还的是nil,又重新回到retryLocked里,说明允许重试:
- 调用newAttemptLocked,底层重新选择新的Ready状态的链接;
- 最终进入replayBufferLocked方法里:
- 执行缓存cs.buffer里的函数,如创建流的函数
- 若执行失败,退出replayBufferLocked方法,进入retryLocked方法里,继续从retryLocked的for循环里执行,就是继续判断是否允许重试等
- 若执行成功,退出replayBufferLocked方法,返还的是nil,退出retryLocked方法,进入withRetry方法里,继续进入for循环的下一次轮询,注意,此时op依旧是发送数据函数,也就是重新尝试执行发送数据函数;
- 执行缓存cs.buffer里的函数,如创建流的函数
当执行某个操作成功时,将当前操作缓存起来,如依次存储到切片里;
等执行某个操作失败时,进行重试:
先对缓存里的函数依次执行一次,
最后再执行刚才执行失败的操作
下一篇文章