背景
生产环境中,调用公司封装的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去解密,走缓存即可。
效果:加密密钥小时级别变化,解密大概率走缓存,性能好。即保证了安全性,又保证了性能。