就能解决问题。下面是程序。它的逻辑比较简单,就是如果有错的话就不断地调用“tcg.Next.CallGet(ctx, key, csc)”,直到没错了或达到了重试上限。每次重试之间有一个重试间隔(retry_interval)。
const (
retry_count = 3
retry_interval = 200
)
type RetryCallGet struct {
Next callGetter
}
func (tcg *RetryCallGet) CallGet(ctx context.Context, key string, csc pb.CacheServiceClient) ( []byte, error) {
var err error
var value []byte
for i:=0; i<retry_count; i++ {
value, err = tcg.Next.CallGet(ctx, key, csc)
log.Printf("Retry number %v|error=%v", i+1, err)
if err == nil {
break
}
time.Sleep(time.Duration(retry_interval)*time.Millisecond)
}
return value, err
}
服务重试跟其他功能不同的地方在于它有比较大的副作用,因此要小心使用。因为重试会成倍地增加系统负荷,甚至会造成系统雪崩。有两点要注意:
- 重试次数:一般来讲次数不要过多,这样才不会给系统带来过大负担
- 重试间隔时间:重试间隔时间要越来越长,这样能错开重试时间,而且越往后失败的可能性越高,因此间隔时间要越长。一般是用斐波那契数列(Fibonacci sequence)或2的幂。当然如果重试次数少的话可酌情调整。示例中用了最简单的方式,恒定间隔,生产环境中最好不要这样设置。
并不是所有函数都需要重试功能,只有非常重要的,不能失败的才需要。
服务超时:
服务超时给每个服务设定一个最大时间限制,超过之后服务停止,返回错误信息。它的好处是第一可以减少用户等待时间,因为如果一个普通操作几秒之后还不出结果就多半出了问题,没必要再等了。第二,一个请求一般都会占用系统资源,如线程,数据库链接,如果有大量等待请求会耗尽系统资源,导致系统宕机或性能降低。提前结束请求可以尽快释放系统资源。下面是程序。它在context里设置了超时,并通过通道选择器来判断运行结果。当超时时,ctx的通道被关(ctx.Done()),函数停止运行,并调用cancelFunc()停止下游操作。如果没有超时,则程序正常完成。
type TimeoutCallGet struct {
Next callGetter
}
func (tcg *TimeoutCallGet) CallGet(ctx context.Context, key string, c pb.CacheServiceClient) ( []byte, error) {
var cancelFunc context.CancelFunc
var ch = make(chan bool)
var err error
var value []byte
ctx, cancelFunc= context.WithTimeout(ctx, get_timeout*time.Millisecond)
go func () {
value, err = tcg.Next.CallGet(ctx, key, c)
ch<- true
} ()
select {
case <-ctx.Done():
log.Println("ctx timeout")
//ctx timeout, call cancelFunc to cancel all the sub-processes in the calling chain
cancelFunc()
err = ctx.Err()
case <-ch:
log.Println("call finished normally")
}
return value, err
}
这个功能应该设置在客户端还是服务端?服务重试没有问题只能在客户端。服务超时在服务端和客户端都可以设置,但设置在客户端更好,这样命运是掌握在自己手里。
下一个问题是顺序选择。 你是先做服务重试还是先做服务超时?结果是不一样的。先做服务重试时,超时是设定在所有重试上;先做服务超时,超时是设定在每一次重试上。这个要根据你的具体需求来决定,我是把超时定在每一次重试上。
服务限流(Rate Limiting):
服务限流根据服务的承载能力来设定一个请求数量的上限,一般是每秒钟能处理多少个并发请求。超过之后,其他所有请求全部返回错误信息。这样可以保证服务质量,不能得到服务的请求也能快速得到结果。这个功能与其他不同,它定义在服务端。当然你也可以给客户端限流,但最终还是要限制在服务端才更有意义。
下面是服务端“service”包中的“cacheServer.go", 它是服务端的接口函数。“CacheService”是它的结构,它实现了“Get”函数,也就服务端的业务逻辑。其他的修饰功能都是对他的补充修饰。
// CacheService struct
type CacheService struct {
Storage map[string][]byte
}
// Get function
func (c *CacheService) Get(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
fmt.Println("start server side Get called: ")
//time.Sleep(3000*time.Millisecond)
key := req.GetKey()
value := c.Storage[key]
resp := &pb.GetResp{Value: value}
fmt.Println("Get called with return of value: ", value)
return resp, nil
}
...
下面是“serverMiddleware.go”,它是服务端middleware的入口。它定义了结构“CacheServiceMiddleware”, 里面只有一个成员“Next", 它的类型是 “pb.CacheServiceServer”,是gRPC服务端的接口。注意这里我们的处理方式与客户端不同,它没有创建另外的接口, 而是直接使用了gRPC的服务端接口。客户端的做法是每个函数建立一个入口(接口),这样控制的颗粒度更细,但代码量更大。服务端所有函数共用一个入口,控制的颗粒度较粗,但代码量较少。这样做的原因是客户端需要更精准的控制。具体实现时,你可以根据应用程序的需求来决定选哪种方式。“BuildGetMiddleware”是服务端创建修饰结构的函数。ThrottleMiddleware是服务限流的实现结构。它里面也只有一个成员“Next”。在创建时,要把具体的middleware功能依次带入,现在只有一个就是“ThrottleMiddleware”。
type Cache