.NET中ClientInterceptorContext生命周期及Header重复添加问题
问题背景
我有一个基于.NET 4.6.2、使用Grpc.Core库的gRPC客户端,已按照官方建议将gRPC通道注册为单例复用。
创建通道实例的代码如下:
public static InvokerAndChannel<T> CreateGrpcCallInvokerAndChannel<T>( string endpoint, ChannelCredentials credentials, IServiceProvider serviceProvider) where T : class { var licenseCode = serviceProvider.GetRequiredService<ILicenseCodeResolver>().GetLicenseCode(); var channel = new Channel(endpoint, credentials, new ChannelOption(ChannelOptions.MaxSendMessageLength, "1mb").Yield()); var callInvoker = channel .Intercept( new ClientTracingInterceptor( new ClientTracingInterceptorOptions { RecordMessageEvents = false })) .Intercept(new StaticMetadataInterceptor(licenseCode)); return new InvokerAndChannel<T>(callInvoker, channel); }
调用管道包含StaticMetadataInterceptor,它负责为调用添加许可证Header,且该Interceptor是单例。
在对同一gRPC服务的多个方法进行并行调用时,发现StaticMetadataInterceptor接收到的是同一个ClientInterceptorContext,至少context.Options.Headers在不断增长,多次调用后会包含多个相同的Header,这是为什么?
以下是该Interceptor的代码:
internal sealed class StaticMetadataInterceptor : Interceptor { private readonly string _licenseCode; public StaticMetadataInterceptor(string licenseCode) { _licenseCode = licenseCode; } private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context) where TRequest : class where TResponse : class { var metadata = context.Options.Headers; if (metadata == null) { metadata = new Metadata(); context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, context.Options.WithHeaders(metadata)); } metadata.Add(GrpcStandardMetadataKeys.License, _licenseCode); } public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation) { ExtendMetadata(ref context); return continuation(request, context); } public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation) { ExtendMetadata(ref context); return continuation(request, context); } public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) { ExtendMetadata(ref context); return continuation(context); } public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation) { ExtendMetadata(ref context); return continuation(request, context); } public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation) { ExtendMetadata(ref context); return continuation(context); } }
原因分析
核心问题在于**ClientInterceptorContext的默认选项(包括Headers)是复用的实例**:
- 当你创建单例的CallInvoker和Interceptor时,gRPC底层会复用同一个默认的
CallOptions实例(不带自定义Headers的场景)给多个并行调用。 - 你的Interceptor直接在这个共享的
Metadata实例上添加Header,而非创建新副本。并行调用时,多个线程同时操作同一个Metadata集合,就会导致重复添加相同Header,最终Headers不断累积增长。
解决方案
修改ExtendMetadata方法,始终创建新的Metadata副本,避免修改共享实例:
private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context) where TRequest : class where TResponse : class { // 复制原有Headers到新实例,再添加许可证Header var newMetadata = context.Options.Headers != null ? new Metadata(context.Options.Headers) : new Metadata(); newMetadata.Add(GrpcStandardMetadataKeys.License, _licenseCode); // 更新context的Options为新的Headers实例 context = new ClientInterceptorContext<TRequest, TResponse>( context.Method, context.Host, context.Options.WithHeaders(newMetadata)); }
这样每个调用都会获得独立的Metadata实例,不会出现并行调用时Header重复累积的问题。
内容的提问来源于stack exchange,提问作者Pavel Voronin
相关产品推荐
相关产品推荐

