How AREX supports recording and playback of Dubbo custom private protocol

background

AREX is an open source automated regression testing platform based on real requests and data. It uses Java Agent technology and comparison technology to achieve fast and effective regression testing through traffic recording and playback capabilities.

Dubbo is a high-performance distributed service framework. It is based on RPC service invocation and service governance. It has the characteristics of transparent remote invocation, load balancing, service registration and discovery, high scalability, and service governance.

Currently AREX supports the recording of interface requests using the Dubbo2x and Dubbo3x protocols, and supports playback of use cases using these two native versions of the Dubbo protocol in the Schedule Service developed based on the Dubbo3x version. However, the user-defined extended Dubbo protocol, serialization methods, and even many personalized transformations based on Dubbo (such as dubbox) are currently not supported.

For example, a user in the community gave us feedback that the Dubbo protocol used by his company is based on the transformation made by Dubbox. The package structure and serialization and deserialization methods of the protocol have been modified. It can be understood as a brand new protocol. A specific type of Dubbo protocol cannot use AREX for recording and playback operations. In order to solve this problem, we forked a customized version on the main branch to realize the recording and playback of this protocol, but this method is only applicable to this specific protocol and lacks universality.

Considering that many community users may have similar problems in the future, we need a general solution that allows users to re-develop themselves to adapt to various custom Dubbo protocols. This article records the specific implementation ideas and details, hoping to provide reference for other users in future use.

plan

For the convenience of understanding, let's first introduce the workflow of each service component of AREX.

When using the AREX traffic recording function, the AREX Java Agent will record the data traffic and request information of the Java application in the production environment, and send this information to the AREX data access service (Storage Service), which will be imported into the Mongodb database to store. When replay testing is required, the AREX Schedule Service will extract the recorded data (request) of the application under test from the database through the data access service according to the user's configuration and needs, and then send an interface request to the target verification service . At the same time, the Java Agent will return the recorded external dependency (external request/DB) response to the application under test, and the target service will return a response message after processing the request logic. Then the scheduling service will compare the recorded response message with the playback response message to verify the correctness of the system logic, and push the comparison result to the analysis service (Report Service), which will generate a playback report for testers to check .

The difficulty of the problem is that since the scheduling service implements playback through generalized calls, when the Dubbo version, protocol, and serialization method of a service are inconsistent with the service provider (Provider), various problems will occur and the call will fail. .

AREX hopes to reduce the interference to user services as much as possible, so it does not want to care about the Dubbo configuration of user services. In order to solve the problem of inconsistency in Dubbo version, protocol, and serialization method, you can consider sending the playback request to the user service itself, and let the user service perform adaptation by itself.

With this in mind, we consider using Java SPI (Service Provider Interface, Service Provider Interface) to solve the problem. The Java SPI is a standard service discovery mechanism in Java. It helps developers extend and replace components in a program without modifying the code.

Users only need to implement the playback interface defined by the scheduling service, and then use the same framework as the recording to perform the playback operation. During playback, the scheduling service dynamically loads user-implemented interfaces through the Java SPI mechanism. In this way, the user service can adapt itself to the Dubbo version, protocol, and serialization method. For the native Dubbo protocol, AREX implements its extension and loads it in by default.

code analysis

SPI definition

Invoker

public interface ReplayExtensionInvoker {
    /**
     * check caseType by InvokerConstants#XXXCaseType.
     */
    boolean isSupported(String caseType);

    ReplayInvokeResult invoke(ReplayInvocation invocation);

    default int order() {
        return 1;
    }
}

Referring to the wording of Invoker in the Dubbo source code, an interface named ReplayExtensionInvoker is defined. This interface defines two methods: and , which are isSupportedused invoketo check the supported caseTypetypes and perform playback operations respectively. invokeThe parameter of the method Invocationis a packaged request, which contains various information of the request, such as method name, parameter value and so on. invokeThe return value of the method is an InvokeResultobject, which contains the playback result information. A default method is also defined in the interface order, which is used to specify the execution order of the implementing class.

  • isSupported()

caseTypeIndicates the protocol type of the playback use case, which can be represented by the InvokerConstantpredefined constants in the class . XXXCaseTypeUsers need to bind (match) the playback extension they implement with the specified by implementing the method of ReplayExtensionInvokerthe interface . In this way, when AREX performs a playback operation, it will automatically match the corresponding playback extension according to the protocol type in the playback use case, so as to realize the playback function of the corresponding protocol type.isSupportedcaseType

  • order()

caseTypeMultiple extensions may be implemented for the same , here the order is specified to load first, in descending order.

Invocation

public interface ReplayInvocation {
    /**
     * eg: dubbo:127.0.0.1:20880
     * protocol + host + port
     */
    String getUrl();

    Map<String, Object> getAttributes();

    ReplayExtensionInvoker getInvoker();

    void put(String key, Object value);

    /**
     * Get specified class item from attributes.
     * key: refer to InvokerConstants.
     */
    <T> T get(String key, Class<T> clazz);

    /**
     * Get object item from attributes.
     * key: refer to InvokerConstants.
     */
    Object get(String key);
}

InvocationIt is defined as an interface, and users need to implement some methods in this interface in order to obtain request parameters during playback.

  • getUrl()

Returns the requested protocol, host and port number, referring to other protocols, such as gRPC, Triple, Rest, etc. Url is essential. It is hoped that it can support the expansion of other protocols in the future, and Url is used as a parameter alone, which is composed of protocol name + IP address + port number. For example: dubbo:127.0.0.1:20880.

  • getAttributes()

The scheduling service packages other parameters of the request in a Map object of key-value pair type, where the key of type String needs to be InvokerConstantsconsistent with the name agreed in . When implementing playback extension, the user can getAttributes()obtain the Map object by calling the method, and then obtain the corresponding parameter value InvokerConstantfrom for processing during playback.

Result

public class ReplayInvokeResult {
    /**
     * invoke result.
     */
    private Object result;

    private Map<String, String> responseProperties;

    /**
     * if invoke failed.
     */
    privdate String errorMsg;

    /**
     * if invoke failed.
     */
    private Exception exception;
}

The ReplayInvokeResult class represents the result of the request invocation, and it contains the following fields:

  • result: The result of the request call, stored in a variable of type Object.
  • responseProperties: Some implicit parameters that need to be passed by the provider are stored in a Map.
  • type variable.
  • errorMsg: If the request call fails, the string storing the error message.
  • exception: An object that stores exception information if the request call fails.

Extended loading

In the extension loading process, first ClassLoaderobtain an URLClassLoaderobject by getting the of the current class, and then call its method through reflection addURL()to load the extension implementation class.

loadJar()The method accepts a jarPathparameter, which indicates the jar package path where the extended implementation class is located, and then ClassLoaderobtains a reference to URLClassLoaderthe object and addURL()the method by judging the current JDK version and obtaining . Then, judge ClassLoaderwhether the current is of URLClassLoadertype, if so, call the method through reflection addURL()to load the jar package, and add the classes in the jar package to ClassLoader, so that the method of the extended implementation class can be dynamically called when the program is running. If loading fails, an error message is output.

public void loadJar(String jarPath) {
       try {
           int javaVersion = getJavaVersion();
           ClassLoader classLoader = this.getClassLoader();
           File jarFile = new File(jarPath);
           if (!jarFile.exists()) {
               LOGGER.error("JarFile doesn't exist! path:{}", jarPath);
           }
           Method addURL = Class.forName("java.net.URLClassLoader").getDeclaredMethod("addURL", URL.class);
           addURL.setAccessible(true);
           if (classLoader instanceof URLClassLoader) {
               addURL.invoke(classLoader, jarFile.toURI().toURL());
           }    
       } catch (Exception e) {
           LOGGER.error("loadJar failed, jarPath:{}, message:{}", jarPath, e.getMessage());
       }
   }

how to use

Step 1: Develop SPI

1. Add pom dependencies

To develop SPI extensions, you first need to pom.xmladd dependencies to the project's file arex-schedule-extension.

      <dependency>
           <groupId>com.arextest</groupId>
           <artifactId>arex-schedule-extension</artifactId>
       </dependency>

2. Implement SPI

To implement the SPI extension, it needs to be implemented com.arextest.schedule.extension.invoker.ReplayExtensionInvoker.

It should be noted that in the provider, it is judged whether the current traffic is playback traffic through Dubbo's implicit parameter attachments, Arex-Record-Idand the generated is Arex-Record-Idalso attachmentspassed back through .

Therefore, when implementing ReplayExtensionInvokerthe interface, you need to ReplayInvocation#attributesget it from the property Arex-Record-Idand add it to the attachments; at the same time, add attachmentsthe from to the property so that these parameters can be used correctly in the playback traffic.Arex-Record-IdReplayInvokeResult#responseProperties

The following is a default dubboInvokerexample for reference:

package com.arextest.dubboInvoker;

import com.arextest.schedule.extension.invoker.InvokerConstants;
import com.arextest.schedule.extension.invoker.ReplayExtensionInvoker;
import com.arextest.schedule.extension.invoker.ReplayInvocation;
import com.arextest.schedule.extension.model.ReplayInvokeResult;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.service.GenericService;

import java.util.List;
import java.util.Map;

public class DefaultDubboInvoker implements ReplayExtensionInvoker {

    [@Override](https://my.oschina.net/u/1162528)
    public boolean isSupported(String caseType) {
        return InvokerConstants.DUBBO_CASE_TYPE.equalsIgnoreCase(caseType);
    }

    [@Override](https://my.oschina.net/u/1162528)
    public ReplayInvokeResult invoke(ReplayInvocation replayInvocation) {
        ReplayInvokeResult replayInvokeResult = new ReplayInvokeResult();
        try {

            RpcContext.getServiceContext().setAttachments(replayInvocation.get(InvokerConstants.HEADERS, Map.class));
            ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
            reference.setApplication(new ApplicationConfig("defaultDubboInvoker"));
            reference.setUrl(replayInvocation.getUrl());
            reference.setInterface(replayInvocation.get(InvokerConstants.INTERFACE_NAME, String.class));
            reference.setGeneric(true);
            GenericService genericService = reference.get();
            if (genericService == null) {
                return replayInvokeResult;
            }

            Object result = genericService.$invoke(replayInvocation.get(InvokerConstants.METHOD_NAME, String.class),
                    (String[]) replayInvocation.get(InvokerConstants.PARAMETER_TYPES, List.class).toArray(new String[0]),
                    (replayInvocation.get(InvokerConstants.PARAMETERS, List.class)).toArray());

            replayInvokeResult.setResult(result);
            replayInvokeResult.setResponseProperties(RpcContext.getServerContext().getAttachments());
        } catch (Exception e) {
            replayInvokeResult.setException(e);
            replayInvokeResult.setErrorMsg(e.getMessage());
        }
        return replayInvokeResult;
    }

}

3. Create the SPI extension file

Create a text file in the project's resources/META-INF/services directory named com.arextest.schedule.extension.invoker.ReplayExtensionInvoker. ReplayExtensionInvokerThe content of the file is the fully qualified name of the extended implementation class, that is, the Reference of the class that implements the interface.

4. Make jar package

Package the implemented SPI extension into a jar package, pay attention to include all related dependencies.

Step 2: Import AREX project

1. jar package mapping Docker image

First, you need to map the jar package to the Docker image, which can be docker-compose.ymlachieved by modifying the file, mapping the local directory to the Docker image, and adding the jar package to the corresponding directory on the machine.

2. Modify the scheduling service startup parameters

Next, to modify the startup parameters of the Schedule Service, it is also docker-compose.ymlrealized by modifying the file. Add the following startup parameters to Schedule's startup parameters.

-Dreplay.sender.extension.jarPath=/usr/local/tomcat/custom-dubbo/xxx.jar

Where xxx.jaris the name of the jar package packaged in the previous step, /usr/local/tomcat/custom-dubbo/and is the storage path of the jar package in the Docker image. The function of this startup parameter is to tell AREX where to find the jar package of SPI extension.

Outlook

There are three areas that can be improved in the future:

The loading mode of the jar package

At present, users need to manually manage and configure the path of the jar package, and need to manually modify docker-compose.ymlthe file, which is a little cumbersome to operate. In the future, I hope to be able to configure relevant parameters and upload jar packages on the AREX platform.

Parameter convention

At present, the management of parameters is implemented in the code in the form of hardcode, and new access parties need to make an agreement in advance, and then update the SPI package. In the future, we hope to configure parameters on the AREX platform, so that new access parties can realize their needs through simple configuration.

Support for other protocols

Currently, the invoker framework can directly support various personalized extensions of the Dubbo protocol. If users need to access other protocols, such as gRPC, Triple, Rest, etc., they can also raise [issue] ( https://github.com/arextest/arex/issues) and discuss in the community to expand the invoker framework scope of application.


Huawei officially released HarmonyOS 4 miniblink version 108 successfully compiled. Bram Moolenaar, the father of Vim, the world's smallest Chromium kernel, passed away due to illness . ChromeOS split the browser and operating system into an independent Bilibili (Bilibili) station and collapsed again . HarmonyOS NEXT: use full The self-developed kernel Nim v2.0 is officially released, and the imperative programming language Visual Studio Code 1.81 is released. The monthly production capacity of Raspberry Pi has reached 1 million units. Babitang launched the first retro mechanical keyboard
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/arextest/blog/10093561