grpc-ecosystem / grpc-spring

Spring Boot starter module for gRPC framework.
https://grpc-ecosystem.github.io/grpc-spring/
Apache License 2.0
3.41k stars 808 forks source link

Add custom metadata #1087

Closed seal90 closed 2 months ago

seal90 commented 3 months ago

The context

I want to make business applications more focused on business. I design a program to proxy all IO of business applications. It looks like this: servicefilter-imagine

This is a bit similar to server-side discovery, so I need to tell the servicefilter the target service name. I found three ways to add it, and the last one I think is the best way. Are there any other better ways?

The code like this:

@GrpcClient(value = "redis-servicefilter-proxy", interceptors = ServicefilterMetadataClientInterceptor.class)

    public static class ServicefilterMetadataClientInterceptor implements ClientInterceptor {
        @Override
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions, Channel channel) {
            return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {

                @Override
                public void start(Listener<RespT> responseListener, Metadata headers) {
                    headers.put(X_SERVICEFILTER_SERVICE_NAME, "redis-servicefilter-proxy");
                    super.start(responseListener, headers);
                }
            };
        }
    }

@interface Metadata { String key(); String value(); }

// GrpcClientBeanPostProcessor protected List interceptorsFromAnnotation(final GrpcClient annotation) throws BeansException { final List list = Lists.newArrayList(); // start GrpcClient.Metadata[] metadatas = annotation.metadatas(); if(metadatas.length > 0) {

    Metadata metadata = new Metadata();
    for(GrpcClient.Metadata meta : metadatas) {
        metadata.put(Metadata.Key.of(meta.key(), Metadata.ASCII_STRING_MARSHALLER), meta.value());
    }
    ClientInterceptor headerInterceptor = MetadataUtils.newAttachHeadersInterceptor(metadata);
    list.add(headerInterceptor);
}
// end
for (final Class<? extends ClientInterceptor> interceptorClass : annotation.interceptors()) {
    final ClientInterceptor clientInterceptor;
    if (this.applicationContext.getBeanNamesForType(interceptorClass).length > 0) {
        clientInterceptor = this.applicationContext.getBean(interceptorClass);
    } else {
        try {
            clientInterceptor = interceptorClass.getConstructor().newInstance();
        } catch (final Exception e) {
            throw new BeanCreationException("Failed to create interceptor instance", e);
        }
    }
    list.add(clientInterceptor);
}
for (final String interceptorName : annotation.interceptorNames()) {
    list.add(this.applicationContext.getBean(interceptorName, ClientInterceptor.class));
}
return list;

}


* Add GrpcChannelConfigurer
```java
@Configuration
public class ServicefilterConfig {

    static final Metadata.Key<String> X_SERVICEFILTER_SERVICE_NAME =
            Metadata.Key.of("x-servicefilter-target-service-name", Metadata.ASCII_STRING_MARSHALLER);

    @Bean
    GrpcChannelConfigurer servicefilterConfigurer() {
        return (builder, name) -> builder.intercept(new ClientInterceptor() {
            @Override
            public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions, Channel channel) {
                return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {

                    @Override
                    public void start(Listener<RespT> responseListener, Metadata headers) {
                        headers.put(X_SERVICEFILTER_SERVICE_NAME, name);
                        super.start(responseListener, headers);
                    }
                };
            }
        });
    }

}

Program code example here: https://github.com/servicefilter/example/blob/main/README.md

The application's environment

Which versions do you use?

ST-DDT commented 3 months ago

In order to help you, we need to understand your intentions/purpose better.

seal90 commented 2 months ago

As shown in the figure, 'biz-service' and 'servicefilter' are two applications on the same machine. biz-service is a business applicatoin. It provides external services through grpc & protobuf, and calls other services, db, cache, etc. through this protocol (convert the interface provided by sdk to protobuf), so that all external calls made by biz-service are converted For service calls, in order to simplify biz-service, the service discovery capability is implemented by servicefilter. Therefore, biz-service needs to tell servicefilter the service name to be called. Servicefilter obtains the service instance from the registration center according to the service name and then completes the call.

The servicefilter uses ‘x-servicefilter-target-service-name’ as the key for the service name. This key and service name need to be placed in the requested Metadata. In addition to the above three adding methods, are there any more friendly adding methods?

ST-DDT commented 2 months ago

Why don't you use the address for the service discovery similar to the DiscoveryClientNameResolver?

seal90 commented 2 months ago

I want the business application to focus more on the business itself, and move the non-business code to an external program (servicefilter) as much as possible for processing, so the business application needs to tell the servicefilter the name of the application I want to call, and the servicefilter can realize discovery and transfer. In this way, business applications become very simple, and the servicefilter capabilities are universal and can be managed using simple configuration files. The overall development, operation and maintenance of the system will be much simpler. Use ‘DiscoveryClientNameResolver‘ to increase the transmission of metadata information? If so, please explain how to use it.

ST-DDT commented 2 months ago

so the business application needs to tell the servicefilter the name of the application I want to call

So basically it's effectively a service registry

In this way, business applications become very simple, and the servicefilter capabilities are universal and can be managed using simple configuration files.

So is setting the configuration:

grpc:
  client:
    counter-service:
      address: "service-filter:counter-service"

Dependending on your NameResolverProvider implementation you can also omit the actual address in the config and it will use the name of the client (annotation) itself.

Use ‘DiscoveryClientNameResolver‘ to increase the transmission of metadata information? If so, please explain how to use it.

It does not add additional metadata to the request it just resolves the address the service tries to use. Similar to DNS. So instead of doing ask dns where the service-filter is and then call service-filter to forward the call to whatever service using the call metadata, you ask the service-filter where the whatever service is

Which service registry do you use? Eureka, Consul, Nacos, ...?

seal90 commented 2 months ago

I want to use servicefilter application proxy for all network requests of business application. Take a request that requires a business application to call the database as an example. 1 The request first reaches servicefilter and is processed by servicefiter. 2 Forward to business application s1 3 s1 needs to call the db service db-001. At this time, s1 does not call db-001 directly, but calls it through the protobuf interface provided by servicefilter. 4 s1 tells servicefilter to call db-001 5 servicefilter goes to the registration center to query the instance of db-001, and then initiates a call According to this process, servicefilter needs to know the service name db-001

    @GrpcClient(value = "redis-servicefilter-proxy", interceptors = ServicefilterMetadataClientInterceptor.class)
    private RedisOperateServiceGrpc.RedisOperateServiceBlockingStub redisOperateServiceBlockingStub;
seal90 commented 2 months ago

servicefilter-imagine

ST-DDT commented 2 months ago

For me this looks like a service registry/discovery client setup and I think an implementation using a name resolver would be straight forward.

For me, calling the "service-filter" as a proxy in every step sounds like a waste of performance, but you might have some requirements/idea/concept that I dont know of/understand.

The alternative using headers should work as well. You should enshure that external calls to your service filter ignore the header to avoid leaking access to your DB. If it is an internal only service it doesnt matter.

The following class might help you with it/should require less code.

https://grpc.github.io/grpc-java/javadoc/io/grpc/stub/MetadataUtils.html

Does this answer your question?

seal90 commented 2 months ago

Yes, thank you.