一次超时事件的排查

139 阅读2分钟

背景

生产环境中,调用公司封装的kms服务进行解密,偶报超时错误。但是看接口实际耗时只有100多ms。

一开始怀疑是errgroup中的ctx用错了,导致cancel掉了请求。后面确认以后发现不是。

最后查看基础框架封装的源码,默认的请求超时时间被设置成了30ms,真是气死。

当时有两个模块,一个是模块是对端接口,gateway会在ctx里面设置deadline,然后向下传递,这个模块没报错,因为ctx里面有deadline了,就没有用client设置的timeout
	if _, ok := ctx.Deadline(); ok {
		return next
	}

另一个模块是消费kafka回调函数里面的ctx,这个ctx里面没有设置deadline,所以会用client的timeout

grpc-go如何实现超时

通过下面的代码,可以看到grpc-go是通过context实现超时控制的。

import (
	"context"
	"time"

	pb "example.com/example.protobuf"
	"google.golang.org/grpc"
)

func main() {
	// 假设已经设置了连接和客户端
	conn, _ := grpc.Dial("your_grpc_server_address", grpc.WithInsecure()) // 使用实际的连接参数替换
	client := pb.NewYourServiceClient(conn)

	// 设置超时
	timeout := 3 * time.Second
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// 使用带超时的 context 进行 gRPC 调用
	req := &pb.YourRequest{} // 使用实际的请求结构体替换
	resp, err := client.YourMethod(ctx, req)
	if err != nil {
		// 处理错误,可能是超时导致的
	}
	// 如果调用成功,处理响应
}

基础框架如何把timeout参数和grpc-go的超时机制整合

通过grpc的拦截器【只需要几个拦截器,把拦截器做成自定义hook的形式,方便添加更多的业务逻辑】,在发送请求前,读取请求里面的timeout参数,通过context.WithTimeout注进context来实现超时控制。

通过ctx注进去,然后请求前拿出来设置上去

func WithClientConfig(ctx context.Context, conf settings.ClientConfig) context.Context {
	return context.WithValue(ctx, clientConfigKey{}, conf)
}

设置timeout

func withTimeout(ctx context.Context, next func(context.Context) error) func(context.Context) error {
        //如果是stream类的rpc,则不许呀设置超时
	if rpc.IsStream(ctx) {
		return next
	}

        //如果context自己设置了超时,就不读配置里面的timeout参数去设置context了
	if _, ok := ctx.Deadline(); ok {
		return next
	}

	var timeout int64

	conf := grpcclient.GetClientConfig(ctx)

	timeout = conf.Timeout
	if timeout <= 0 {
		timeout = defaultTimeout
	}

	return func(ctx context.Context) error {
		ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Millisecond)
		defer cancel()
		return next(ctx)
	}
}

kms客户端缓存优化

kms客户端可以设置两个缓存,一个是加密的缓存,一个是解密的缓存。

根据uid后2位取模,设置加密的缓存,缓存过期时间为1小时。相同uid后缀的消息,在1小时以内都用相同的密钥加密。过期以后,重新去获取密钥,后面1小时的,又用另外一个密钥加密。

加密获取密钥对以后,把解密的密钥缓存下来,过期时间设置为24小时。这样,新发的消息,在24小时内被拉取,都不需要请求kms去解密,走缓存即可。

效果:加密密钥小时级别变化,解密大概率走缓存,性能好。即保证了安全性,又保证了性能。