Author: vivo Internet server team – Wang Fei, Lin Yupan

The Dubbo generalization call feature can initiate remote calls in scenarios that do not rely on service interface API packages, which is especially suitable for framework integration and gateway class application development.

This paper combines the problems encountered in the actual development process that require remote calls to multiple three-party systems, explains how to use Dubbo generalization calls to simplify the development of project practices that reduce system coupling, and finally analyzes the principle of Dubbo generalization calls.

First, the background

Unified configuration platform is a platform that provides each module of the terminal device for file configuration and file issuance capabilities, module development in the background server for file configuration, and then the terminal device can obtain the corresponding configuration file in accordance with specific rules, file issuance can be issued in accordance with a variety of device dimensions, the specific project framework can participate in the following figure:

The existing delivery strategy is configured by the module development in the unified configuration background server, and whether the file is sent to the corresponding terminal device is determined by the dimension selected by the user on the platform.

However, other business parties also have the ability to configure the issuance rules of the A/B experimental platform within the company to use the unified configuration platform to poll the server every day to request new files, but whether the files configured by the unified configuration platform can be issued is determined by the A/B experimental platform, and the corresponding rules and the corresponding file id of the unified configuration platform will be configured in the A/B experimental platform, and then the unified configuration platform needs to call the A/B experimental platform interface for the request to determine whether the file can be issued.

With the increase of the company’s internal experimental platform, more and more of this kind of docking demand is determined by the three-party platform to determine whether the file is issued, and how to better and faster respond to this similar docking demand is a problem that we need to think deeply.

Second, the selection of the program

The delivery logic of the original unified configuration is to first find all the files that can be issued, and then determine whether a single profile satisfies the device dimension, and if so, it can be issued. Now after docking the A/B experimental platform, whether the file can be issued needs to be determined by the external system, and two schemes were considered when designing:

Option 1: 

Similarly, first find all the files that can be issued, and then judge whether they match according to (1) device dimensions for a single file, then (2) call the interface of the A/B experimental platform to obtain the file Id that the device can issue, and then call (3) the grayscale experimental platform to obtain the file id that the device can issue, and finally summarize the profile id obtained in the first three steps to obtain the files that can be issued, as shown in the following figure.

Solution 1 broke the original judgment logic of whether the file can be issued, and now in addition to the original judgment logic, it also needs additional steps to call other systems to append additional files that can be issued. And the follow-up inevitably docks with other three-party systems, and the first solution requires increasing the logic of calling the three-party interface to append the file id that can be issued. In addition, the regular dubbo call on the provider side requires the introduction of other experimental system two-party libraries and model types, which increases the strong coupling of the unified configuration system and other systems.

Solution 2: Use the Dubbo generalization call advanced feature to abstract a delivery dimension (remote call), which is specifically used for other scenarios where the tripartite experimental system wants to decide whether to issue a file, as shown in the following figure:

Scheme two unified abstraction of a remote call delivery dimension, you can maintain the original judgment logic, that is, first of all the files that can be issued in the system are found out first, and then matched according to the device dimension, if a file is configured with a remote call dimension, then find the function name, parameter type array and parameter value object array contained in this remote call dimension, and then call the three-party interface, so as to determine whether the file can be sent to the device, and finally get the file id list that can be issued.

In addition, using the advanced characteristics of Dubbo generalization call, the caller does not care about the detailed definition of the provider’s interface, but only needs to pay attention to which method is called, what parameters are passed and what return results are received, so as to avoid the need to rely on the two-party library of the service provider and the model classifier, which can greatly reduce the coupling between the consumer side and the provider side.

Based on the above analysis, we finally determined that the second solution takes the use of Dubbo generalization calls to abstract a unified dimension, let’s take a look at the specific implementation.

Third, concrete implementation

GenericService is a generalization interface provided by Dubbo for generalization calls. Only the $invoke method is provided, and the three entry parameters are the function name, the parameter type array, and the parameter value object array.

2. Create a service reference configuration object ReferenceConfig.

3. Set the request parameters and service calls, where the service call can be made by using the complete method name, parameter type array and parameter value array configured in the background.

So why does the caller involved in the Dubbo generalization call not care about the detailed definition of the provider’s interface, but only about which method is called, what parameters are passed, and what return results are received?

Before explaining the implementation principle of generalization call, let’s briefly explain the principle of direct call.

Fourth, Dubbo directly calls the relevant principles

The Dubbo direct call correlation principle involves two aspects: the Dubbo service exposure principle and the Dubbo service consumption principle

4.1 Dubbo service exposure principle

4.1.1 The overall process of service remote exposure

On the whole, the Dubbo framework does service exposure into two parts, the first step will be held by the service instance through the proxy conversion into Invoker, the second step will convert Invoker through a specific protocol (such as Dubbo) into Exporter, the framework to do this layer of abstraction is also greatly convenient for functional extension.

Invoker here can be simply understood as a real instance of a service object, which is the Dubbo framework entity domain, and all models will move closer to it and can make invoke calls to it. It may be a local implementation, a remote implementation, or a cluster implementation.

The source code is as follows:

The implementation class ref is first encapsulated as Invoker, then the invoker is converted to an exporter, and finally the exporter is put into the cache exporter.

4.1.2 Details of Service Exposure

4.1.2.1 Encapsulates the implementation class ref as Invoker

(1) The entry to the Dubbo remote exposure is in ServiceBean’s export() method, and since servicebean inherits the serviceconfig class, the logic that actually performs the exposure is serviceconfig’s doExport() method.

(2) Dubbo supports the same service to expose multiple protocols, such as exposing Dubbo and REST protocols at the same time, and also supports multiple registries such as zookeeper and nacos, and the framework will do a service exposure to the protocols used in turn, and each protocol registration metadata will be written to multiple registries, specifically to execute doExportUrlsFor1Protocol.

(3) Then create the Invoker object through the dynamic proxy, generate an AbstractProxylnvoker instance on the server side, all real method calls will be delegated to the proxy, and then the proxy forwards to the service implementer ref call; Dynamic agents generally have: JavassistProxyFactory and JdkProxyFactory two ways, the JavassistProxyFactory selected here.

4.1.2.2 Convert invoker to exporter

Exporter exporter 

= protocol.export(wrapperInvoker);

After converting the service instance ref to Invoker, the service exposure process begins.

This goes through a series of filter links, culminating in more fine-grained control through RegistryProtocol#export, such as first exposing the service before registering the service metadata. The registry does the following things in turn when it comes to service exposure:

Delegate specific protocol (Dubbo) to perform service exposure, create NettyServer listening ports, and save service instances.

Create a registry object to create a TCP connection with the registry.

Register the service metadata to the registry.

Subscribe to the configurators node and listen for dynamic property change events of the service.

Service destruction finishing work, such as port closure, anti-registration service information, etc.

Here we focus on the process of delegating specific protocols for service exposure doLocalExport (final InvokeroriginInvoker).

(Exporter) protocol.export(invokerDelegete) method is processed by a series of interceptors before the DubboProtocol export method is called.

The important point here is to put the exporter in the cache, and the key here is

serviceGroup/serviceName:serviceVersion:port, the last thing you get here is com.alibaba.dubbo.demo.DemoService:20880, and then DubboExporter. The in-memory cache exporterMap here is an important property that is reused when subsequent consumer calls to the service provider.

At this point, the remote exposure process of the server provider is basically introduced.

4.2 Implementation principle of Dubbo service consumption

4.2.1 The overall process of service consumption

On the whole, the Dubbo framework is also divided into two major parts of service consumption, the first step is to generate Invoker by holding a remote service instance, which is the core remote proxy object on the client. The second step will convert Invoker through dynamic proxy to a dynamic proxy reference that implements the user interface. Invoker here hosts features such as network connectivity, service calls, and retries, and on the client side, it can be a remote implementation or a cluster implementation.

The source code is as follows:

4.2.2 Details of the Consumption of Services

4.2.2.1 Use Protocol to convert interfaceClasses to Invoker

(1) The entry point for service references is in ReferenceBean#getObject,

Since Referencebean’ inherits the serviceconfig class, the Get method of Reference is then called.

(2) The Invoker will then be generated based on the referenced interface type that will hold the remote service instance.

(3) Through a series of filter chains, finally call the RegistryProtocol doRever method.

This logic mainly completes the creation of the registry instance, the function of registering metadata to the registry and subscribing.

Where was the specific remote Invoker created? Where is the client call interceptor constructed?

When a subscription is initiated for the first time in directory.subscrib(), a data pull operation is performed, and the RegistryDirectory#notify method is triggered, where the notification data is the full amount of data of a certain category, such as providers and routers category data. When notifying providers data, the Invoker conversion is done within the RegistryDirectory#toInvokers method.

Core code

This goes through a series of filter chains, and then finally calls DubboProtocol’s refer method to create specific invokers.

The invoker returned here is used to update the RegistryDirectory’s methodInvokerMap property, and eventually the corresponding invoker list will be found according to the method when the consumer method is actually called.

4.2.2.2 Create a proxy using ProxyFactory

The above proxyFactory is a ProxyFactory$Adaptive instance, and its getProxy interior ends up being a JavassistProxyFactory wrapped in StubProxyFactoryWrapper. Go directly to the JavassistProxyFactory.getProxy method.

Invoker: MockClusterInvoker instance

【interfaces】:[interface com.alibaba.dubbo.demo.DemoService, interface com.alibaba.dubbo.rpc.service.EchoService]

The proxy object we end up returning is actually a proxy0 object, and when we call its sayHello method, it calls the internal handler.invoke method.

Dubbo generalization call is generally in the form of direct exposure by the server provider, and the consumer side adopts the form of service generalization call, so the focus here is on the difference and connection between Dubbo generalization call and direct call on the consumer-side service reference and initiation consumption.

Fifth, the difference and connection between Dubbo generalization call and direct call

5.1 Generate Invoker by holding a remote service instance

The source of interfaceClass here is different, createProxy (Mapmap) is called in the init() method of ReferenceConfig, and the specific interfaceClass will be different according to whether it is a return call, as follows:

Direct call:

interfaceClass→com.alibaba.dubbo.demo.DemoService

Generalization call:

interfaceClass→com.alibaba.dubbo.rpc.service.GenericService

The final acquisition of invoker is also different

Direct call:

interface com.alibaba.dubbo.demo.DemoService ->  dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-consumer&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=24932&qos.port=33333®ister.ip=xx.xx.xx.xx&remote.timestamp=1640744945905&side=consumer×tamp=1640745033688

Generalization call:

interface com.alibaba.dubbo.rpc.service.GenericService ->  dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=test&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=true&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=27952&qos.port=33333®ister.ip=xx.xx.xx.xx&remote.timestamp=1640748337173&side=consumer×tamp=1640748368427

5.2 The Service initiates the consumption process

In 4.2.2 Service Consumer Initiation Request Details Step (1) encapsulates the request parameters (method name, method parameter type, method parameter value, service name, additional parameters) into an Invocation.

The direct call to RpcInvoaction is as follows:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={}]

The RpcInvoaction for generalized calls is as follows: 

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String; , class [Ljava.lang.Object; ], arguments=[sayHello, [Ljava.lang.String; @22c1bff0, [Ljava.lang.Object; @30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

We can find that the RpcInvocation object generated here is different, but the service exposed by the service provider will not change, so there must be a conversion process here, and the key to parameter conversion here lies in the GenericImplFilter class on the service provider’s side.

Core Process:

(1) Whether it is a generalization call to judge

(2) Extraction of parameters

(3) Serialize the parameters and construct a new RpcInvocation object

Serialize the former RpcInvocation object:

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String; , class [Ljava.lang.Object; ], arguments=[sayHello, [Ljava.lang.String; @22c1bff0, [Ljava.lang.Object; @30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

After serialization RpcInvocation object:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

The following call logic is consistent with the direct call, such as getting the list with the key sayHello (specifying the method name) from the local cache from the local cache Map< list> methodInvokerMap, and then making subsequent calls.

So when to trigger the invoke method of GenericFilter, here is actually related to the call chain of the filter, from the annotation on the GenericFilter class, we can see that the @Activate (group = Constants.PROVIDER, order = -20000), the description is effective on the service provider side.

In addition, how the service provider side knows whether the call is a direct call or a generalized call, which involves the consumer-side GenericImplFilter class corresponding to the service provider’s GenericFilter, the code is as follows:

5.3 Overall flowchart of generalization calls

Sixth, summary

High cohesion and low coupling are an important goal of our architecture design, and Dubbo’s generalized call feature only needs to know the full interface path, request parameter type and request parameter value of the service to directly call to get the request result, which can avoid relying on specific three-party jar packets, thereby reducing the coupling of the system. In the process of daily learning and development, in addition to the general use of a technology, we also need to pay attention to some advanced features to make a more appropriate architectural design.

Resources:

“Understanding Apache Dubbo and Practice” Yi Ji, Lin Lin

 Dubbo source code analysis – generalization call 

 Dubbo generalization call usage and principle analysis 

END

Guess you like it

Performance tuning for high-performance Java computing services

JNI in action in intensive computing scenarios

Vivo is based on JaCoCo’s test coverage design and practice