Skip to content

It is recommended that the GRPC server add a timeout limit. #3189

Open
@bitker

Description

@bitker

建议GRPC服务端加入超时限制。
防止某些情况下导致服务端执行耗时阻塞任务时,客户端长时间链接导致资源浪费。
某些场景:如服务端在执行CURL任务时可能会有很久的延时(当然是开发者没有设置超时限制)。这样会导致很多客户端一直等待。
个人经历:某次开发时使用了gcahe中的GetOrSetFuncLock这个函数,由于此函数中又执行了CURL请求,最后导致了大批量并发链接等待。当时我个人原因没有注意到此函数会阻塞等待,导致了客户端很多链接在等待处理(经查发现等待了半小时没有返回)。后来切换gozero后没有出现长时等待。经复盘发现gozero的服务端是有超时设置
可能很多人会说客户端链接时可以设置ctx的超时控制。我也知道这个,但是如果有个别开发者粗心没有设置此超时的话将会导致大量链接等待了。
gozero的服务端超时控制是在中间件中将ctx上下重新设置超时控制。以下是部分gozero的超时控制流程。
希望哪位大佬 能改个加入gf的grpc就完美了

type (
	// MethodTimeoutConf defines specified timeout for gRPC method.
	MethodTimeoutConf struct {
		FullMethod string
		Timeout    time.Duration
	}

	methodTimeouts map[string]time.Duration
)

// UnaryTimeoutInterceptor returns a func that sets timeout to incoming unary requests.
func UnaryTimeoutInterceptor(timeout time.Duration,
	methodTimeouts ...MethodTimeoutConf) grpc.UnaryServerInterceptor {
	timeouts := buildMethodTimeouts(methodTimeouts)
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler) (any, error) {
		t := getTimeoutByUnaryServerInfo(info.FullMethod, timeouts, timeout)
		ctx, cancel := context.WithTimeout(ctx, t)
		defer cancel()

		var resp any
		var err error
		var lock sync.Mutex
		done := make(chan struct{})
		// create channel with buffer size 1 to avoid goroutine leak
		panicChan := make(chan any, 1)
		go func() {
			defer func() {
				if p := recover(); p != nil {
					// attach call stack to avoid missing in different goroutine
					panicChan <- fmt.Sprintf("%+v\n\n%s", p, strings.TrimSpace(string(debug.Stack())))
				}
			}()

			lock.Lock()
			defer lock.Unlock()
			resp, err = handler(ctx, req)
			close(done)
		}()

		select {
		case p := <-panicChan:
			panic(p)
		case <-done:
			lock.Lock()
			defer lock.Unlock()
			return resp, err
		case <-ctx.Done():
			err := ctx.Err()
			if errors.Is(err, context.Canceled) {
				err = status.Error(codes.Canceled, err.Error())
			} else if errors.Is(err, context.DeadlineExceeded) {
				err = status.Error(codes.DeadlineExceeded, err.Error())
			}
			return nil, err
		}
	}
}

func buildMethodTimeouts(timeouts []MethodTimeoutConf) methodTimeouts {
	mt := make(methodTimeouts, len(timeouts))
	for _, st := range timeouts {
		if st.FullMethod != "" {
			mt[st.FullMethod] = st.Timeout
		}
	}

	return mt
}

func getTimeoutByUnaryServerInfo(method string, timeouts methodTimeouts,
	defaultTimeout time.Duration) time.Duration {
	if v, ok := timeouts[method]; ok {
		return v
	}

	return defaultTimeout
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    featurehelp wantedplannedThis issue/proposal is planned into our next steps.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions