Melhore a confiabilidade do sistema - nova tentativa automática

introdução

A repetição é um meio importante para melhorar a disponibilidade do sistema. Muitas vezes vemos muita lógica de repetição no código comercial. Se existe uma maneira de implementar a repetição sem intrusão, o código comercial é completamente inconsciente.

nova tentativa de negócios

Códigos comerciais comuns

func ExampleRPCSend(ctx context.Context, msg string) error {

   fmt.Printf("\nsend msg %v", msg)

   return errors.New("rpc err")

}



func RetrySend(ctx context.Context, msg string) error {

   var err error

   for i := 0; i < 3; i++ {

      err = ExampleRPCSend(ctx, msg)

      if err == nil {

         break

      } else {

         time.Sleep(10 * time.Millisecond)

      }

   }

   return err

}

Exemplo de uso

err := RetrySend(ctx, "example")

Obviamente, essa é uma lógica comum que pode ser reutilizada, então ela não pode ser usada elegantemente no código?

embalagem simples

func RetryFun(ctx context.Context, fn func(ctx context.Context) error) error {

   var err error

   for i := 0; i < 3; i++ {

      err = fn(ctx)

      if err == nil {

         break

      } else {

         time.Sleep(10 * time.Millisecond)

      }

   }

   return err

}

Exemplo de uso de exemplo de negócios

err = RetryFun(ctx, "example", ExampleRPCSend)

limitado

A assinatura do método que precisa ser repetida é limitada e apenas um método de nova tentativa de assinatura pode ser aplicado.

Encapsulamento de método genérico

Postagem de blog de referência

Você pode facilmente escrever um método de repetição geral e encapsular levemente o julgamento de err e o tempo de espera de repetição

func Decorate(decoPtr, f interface{}) error {

   fn := reflect.ValueOf(f)

   decoratedFunc := reflect.ValueOf(decoPtr).Elem()

   logicFunc := func(in []reflect.Value) []reflect.Value {

      ret := make([]reflect.Value, 0)

      for i := 0; i < 3; i++ {

         ret = fn.Call(in)

         if !needRetryErr(ret) {

            break

         } else {

            time.Sleep(10 * time.Millisecond)

         }

      }

      return ret

   }

   v := reflect.MakeFunc(fn.Type(), logicFunc)

   decoratedFunc.Set(v)

   return nil

}

Para encapsulamento separado do julgamento de erros, nem todos os erros precisam ser repetidos, mas deve haver tentativas seletivas. Esse julgamento, que será descrito mais adiante, é benéfico para evitar avalanches de novas tentativas.

Supõe-se aqui que o método está no caso usual de go, e o último valor de retorno é error

var RetryBizCode = []string{"err_01","err_02"}



func needRetryErr(out []reflect.Value) bool {

   // 框架返回的错误,网络错误

   if err, ok := out[len(out)-1].Interface().(error); ok && err != nil {

      return true

   }



   // BizCode业务错误码,需要重试的错误码

   if isContain(GetBizCode(out), RetryBizCode) {

      return true

   }

   

   return false

}

Exemplo de uso

retryFun := ExampleRPCSend

Decorate(&retryFun, ExampleRPCSend)

err := retryFun(ctx, "example")

Comparado com novas tentativas de negócios e encapsulamento simples, esse uso é mais "feio".

encapsulamento de middleware

A lógica de repetição pode ser encapsulada como middleware e implementada diretamente no middleware.

func RpcRetryMW(next endpoint.EndPoint) endpoint.EndPoint {

   return func(ctx context.Context, req interface{}) (resp interface{}, err error) {

      if !retryFlag(ctx) {

         return next(ctx, req)

      }

      // rpc装饰

      decoratorFunc := next

      if err := Decorate(&decoratorFunc, next); err != nil {

         return next(ctx, req)

      }

      return decoratorFunc(ctx, req)

   }

}

Adicionar esse middleware no nível do framework pode realizar a repetição de chamadas rpc

AddGlobalMiddleWares(RpcRetryMW)

O retryflag do middleware pode ser customizado, para que apenas os cenários necessários possam ser repetidos ou a definição inversa possa ser usada para não repetir determinados cenários.

func retryFlag(ctx context.Context) bool {

   ...

   return true

}

Se houver um sinalizador de julgamento , você precisa definir o sinalizador. Normalmente, realizaremos a operação setFlag na entrada da cena, ou seja, o serviço que solicita o primeiro toque.

func SetFlag(ctx context.Context, flag string) context.Context {

   return context.WithValue(ctx, flagKey, flag)

}

O link de chamada é muito longo. Como fazer o sinalizador passar? O lado do serviço precisa adicionar middleware, e a base rpc precisa passar o sinalizador.

func CtxFlagMW(next endpoint.EndPoint) endpoint.EndPoint {

   return func(ctx context.Context, req interface{}) (resp interface{}, err error) {

      flag, ok := getFlagFromReq(ctx, req)

      if ok {

         setFlag(ctx, flag)

      }

      return next(ctx, req)

   }

}



Use(CtxFlagMW)

Auxiliar idempotente de negócios

A nova tentativa deve ser realizada sob a condição de que o downstream seja idempotente, caso contrário, causará confusão de dados. Se a própria interface de negócios for idempotente, então ela pode ser usada diretamente, mas para a maioria das interfaces de negócios não são idempotentes, como intervir na repetição automática?

Através da análise, nosso negócio não é idempotente principalmente por causa da operação da biblioteca de gravação. Se pudermos julgar que a biblioteca de gravação foi operada antes de tentar novamente, pular a biblioteca de gravação pode satisfazer a idempotência da maioria dos cenários. Para julgar que o banco de dados de gravação foi operado, você pode criar uma tabela de log de transações local no banco de dados comercial por meio da transação local do banco de dados para registrar a transação local gravada.

image.png

Código

func TransactionDecorator(ctx context.Context, txFunc func(ctx context.Context) error) error {

   // 未接入的场景,走原逻辑不变

   if retryFlag(ctx) {

      return TransactionManager(ctx, txFunc)

   }



   // 生成本地账本

   event := genEventFromCtx(ctx)



   // 若本地账本已存在,则进行空补偿,跳过写库逻辑

   if ExistEvent(ctx, event) {

      return nil

   }



   // 本地账本不存在,则事务写入业务数据和本地账本

   return TransactionManager(ctx, func(ctx context.Context) error {

      // 业务逻辑

      if err := txFunc(ctx); err != nil {

         return err

      }

      // 写入本地账本

      if err := SaveEvent(ctx, event); err != nil {

         return err

      }

      return nil

   })

}

业务幂等只能是协助加一些手段,具体接入还需要业务判断是否足够,不能说保证了写库操作唯一就是保证了幂等。

预防重试雪崩

重试最大的风险就是带来请求的累积,把下游压垮。我们会从以下几方面预防重试雪崩效应。

1. needRetryErr方法,把不需要重试的错误拦截掉,直接返回,避免业务逻辑错误重试。

2. retryFlag方法,针对需要重试的场景设置标志,有重试标志才进行重试,不是把服务所有请求都重试,可以避免无效请求。

3.非超时错误的情况下,保证请求不放大。

有两种做法,一种如上通用做法,在调用下游出错的点上重试,等重试结束才向上返回。

第二种做法是先向上返回成功,内部进行重试。第二种做法的局限性大一些,适用的场景更少。在通用方法封装上如何实现向上先返回成功,具体实现可参考博文

4.超时情况下,可能多个服务都同时感知到超时,如何保证请求不放大?

超时错误,例如A->B->C->D,A,B,C同时感知到错误,那么都会发起重试,显然就放大了请求。那么想办法只让一个服务重试呢

needRetryErr方法可以识别错错误类型,也就是可以感知到超时错误。

SetFlag方法本身是在入口场景调用,那么可以设置入口场景标记entranceFlag,而在中间件CtxFlagMW中,只传递retryFlag,不传递entranceFlag,那么就只有A服务会有entranceFlag标记

needRetryErr方法判断err类型为超时,则retryFlag判断ctx内有retryFlag 以及 entranceFlag ,两个标记都有才发起重试,则能保证整条链路只在入口服务A处发起重试。

综上超时情况下,请求也能保证不放大,只有1个服务在重试。

Acho que você gosta

Origin juejin.im/post/7115344614025855012
Recomendado
Clasificación