grpc-go源码剖析八十四之深入源码,分析重试机制withRetry的原理?

已发表的技术专栏
0  grpc-go、protobuf、multus-cni 技术专栏 总入口

1  grpc-go 源码剖析与实战  文章目录

2  Protobuf介绍与实战 图文专栏  文章目录

3  multus-cni   文章目录(k8s多网络实现方案)

4  grpc、oauth2、openssl、双向认证、单向认证等专栏文章目录)

本篇文章我们从源码的视角来分析重试机制的原理。

1、源码分析入口

进入grpc-go/stream.go文件中的withRetry方法里

1func (cs *clientStream) withRetry(op func(a *csAttempt) error, onSuccess func()) error {
2.	cs.mu.Lock()
3for {
4if cs.committed {
5.			cs.mu.Unlock()
6return op(cs.attempt)
7}
8.		a := cs.attempt

9.		cs.mu.Unlock()
10.		err := op(a)
11.		cs.mu.Lock()

12if a != cs.attempt {
13continue
14}
15if err == io.EOF {
16<-a.s.Done()
17}
18if err == nil || (err == io.EOF && a.s.Status().Code() == codes.OK) {
19onSuccess()
20.			cs.mu.Unlock()
21return err
22}
23if err := cs.retryLocked(err); err != nil {
24.			cs.mu.Unlock()
25return err
26}
27}
28}

withRetry主要流程说明

  • 第10行:执行具体的业务函数,如创建流阶段的函数op,发送阶段的函数op等
  • 第15-26行:具体执行业务函数后的逻辑处理
    • 第18-22行:针对的是,执行结果成功的处理逻辑
      • 第19行:成功后,执行传进来的函数onSuccess函数
    • 第23-26行:针对的是,执行结果失败的从逻辑逻辑,底层校验是否允许重试等操作

其实,代码实现主体思路是:

  • 先执行业务逻辑,
  • 再根据业务逻辑的执行结果来判断是否进入重试机制流程

只要执行op成功后,就会执行onSuccess()函数,(切面操作)

如果传递的onSuccess函数,是bufferForRetryLocked函数,就具备了重试功能

如果传递的onSuccess函数,是commitAttemptLocked函数,就不具备重试功能

如果传递的onSuccess函数,是其他函数的,可能就会有其他功能了

注意:

withRetry属于clientStream,属于客户端的;
并没有发现服务器端也有类似的重试机制功能;
服务器端将执行结果反馈给客户端时,并没有使用重试机制。

2、重试机制withRetry实现方式的特点

该方法withRetry的特点:

  • 将重试机制流程跟具体的业务逻辑隔离开,
  • 具体的业务是通过函数参数op,以及onSuccess 传进来的;
  • 这样的话,该重试机制流程可以支持不同的场景;如创建流场景,传输数据的场景等等

3、当业务执行失败时,重试机制retryLocked如何处理此种情况?

进入retryLocked方法里:

1func (cs *clientStream) retryLocked(lastErr error) error {
2for {
3.		cs.attempt.finish(lastErr)

4if err := cs.shouldRetry(lastErr); err != nil {
5.			cs.commitAttemptLocked()
6return err
7}

8.		cs.firstAttempt = false
9if err := cs.newAttemptLocked(nil, nil); err != nil {
10return err
11}

12if lastErr = cs.replayBufferLocked(); lastErr == nil {
13return nil
14}
15}
16}

主流程说明:

  • 第4行:根据执行结果的错误信息,判断是否允许重试
    • 若不允许重试:调用commitAttemptLocked
      • 将cs.committed = true
      • 将存储执行函数的缓存cs.buffer重置为nil
    • 若允许重试:
      • 第9行:创建csAttempt,并且重新选择状态为Ready的链接
      • 第12行:从缓存cs.buffer里,获取已经成功执行过的函数,重新执行一次;注意,这里并没有执行本次执行失败的方法,执行的是前面运行正确的方法

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.
3func (cs *clientStream) shouldRetry(err error) error {
4.	unprocessed := false
5if cs.attempt.s == nil {
6.		pioErr, ok := err.(transport.PerformedIOError)
7if ok {
8// Unwrap error.
9.			err = toRPCErr(pioErr.Err)
10} else {
11.			unprocessed = true
12}
13if !ok && !cs.callInfo.failFast {
14return nil
15}
16}

17if cs.finished || cs.committed {
18return err
19}

20if cs.attempt.s != nil {
21<-cs.attempt.s.Done()
22.		unprocessed = cs.attempt.s.Unprocessed()
23}
24if cs.firstAttempt && unprocessed {
25return nil
26}

27if cs.cc.dopts.disableRetry {
28return err
29}

30.	pushback := 0
31.	hasPushback := false
32if cs.attempt.s != nil {
33//------省略掉跟Trailer相关的代码,暂不分析
34}

35var code codes.Code
36if 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
42if rp == nil || !rp.retryableStatusCodes[code] {
43return err
44}

45if cs.retryThrottler.throttle() {
46return err
47}

48if cs.numRetries+1 >= rp.maxAttempts {
49return err
50}
51var dur time.Duration
52if 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
58if 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)
65select {
66case <-t.C:
67.		cs.numRetries++
68return nil
69case <-cs.ctx.Done():
70.		t.Stop()
71return 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方法里:

1func (cs *clientStream) replayBufferLocked() error {
2.	a := cs.attempt

3for _, f := range cs.buffer {
4if err := f(a); err != nil {
5return err
6}
7}
8return 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依旧是发送数据函数,也就是重新尝试执行发送数据函数;



当执行某个操作成功时,将当前操作缓存起来,如依次存储到切片里;

等执行某个操作失败时,进行重试:

先对缓存里的函数依次执行一次,

最后再执行刚才执行失败的操作

下一篇文章

  元数据相关介绍?

你可能感兴趣的:(golang,grpc-go,grpc-g0源码,rpc,微服务)