More than a little improvement, a detailed interpretation of Dubbo 3.0 Preview

Keywords: Programming Dubbo Apache network

Since October 27, 2011, Dubbo has been used by many non-Ali companies, including Dangdang Networks, NetEase Koala and other Internet companies, as well as China Life, Qingdao Haier and other large traditional enterprises. More user information, access to Dubbo @GitHub,issue#1012: Wanted: who's using dubbo.

Since December last year, Dubbo 3.0 has officially entered the development stage, and has attracted the attention of the community and the majority of Dubbo users. This article will give you a detailed explanation of the new features and functions of the 3.0 preview.

Here are two interesting questions about Dubbo.

  • Why is Dubbo version 2.0 when it's open source? Is there a 1.0 version before?

The author has done adaptation compatibility of Dubbo protocol. Dubbo does have version 1.x, and it is totally different from the open source version of 2.0 in protocol design and model design. The following figure is about the development path of Dubbo:

  • Are open source versions of Dubbo being used internally in Ali?

Yes, it's quite certain that the current open source version of Dubbo is widely used in Alibaba, while Ali's core business unit is HSF 2.2, which is compatible with Dubbo usage and Remoting protocol. Of course, we are now upgrading HSF2.2, relying directly on the open source version of Deubbo for kernel unification. So there is no doubt that Dubbo is a distributed service framework verified by large-scale online systems.

Essentials of Dubbo 3.0 Preview

The new support and improvement of Dubbo 3.0 in design and function are mainly as follows:

  • Asynchronization of Filter Chain in Dubbo Kernel

It should be pointed out here that the planned asynchronous de-blocking in 3.0 and the asynchrony provided in 2.7 are two layers of features. Asynchronism in 2.7 is based on request-response session model in traditional RPC, while asynchronism in 3.0 will be built from bottom to top at the level of communication protocol, focusing on cross-process and full-link asynchronism. Through the underlying protocol, streaming mode can be supported not only to support a variety of session models, but also to support backpressure, current limitation and other characteristics at the protocol level, which makes the whole distributed system more flexible. In summary, 2.7 focuses on asynchronization more limited to point-to-point asynchronization (a consumer calls a provider), 3.0 focuses on asynchronization, while 3.0 focuses on asynchronization on the whole call chain in width, and upward in height can be packaged into Rx programming model. Interestingly, Spring 5.0 released Flux support, and then started to address cross-process asynchrony.

  • Functionally, reactive support

In recent years, the word reactive programming has been increasing rapidly. Reactive programming on Wikipedia is interpreted as reactive programming is a programming paradigm oriented around data flows and the propagation of change. Dubbo 3.0 will implement the rx interface of Reactive Stream, thus enabling users to enjoy the response enhancement brought by RP, even for RP. Architecture upgrade. Of course, we hope that reactive can not only upgrade event-driven application integration, but also play a positive role in Load Balance and fault tolerance.

  • Exploration of the Orientation of Cloud Primitive/Service Mesh

Our strategy is to enter the Envoy community to realize Dubbo's idea of mesh integration. At present, the Dubbo protocol has been supported by Envoy. Of course, Dubbo Mesh still has a long way to go before it can be used. Its work on location, load balancing and service governance needs to be continued on the data side. In addition, the construction of control panel has not been put on the agenda in the community.

  • Fusion and support within Ali

Dubbo 3.0 sets the strategy of internal and external integration, that is to say, the core of 3.0 will eventually be deployed in Alibaba's production system. It is believed that Dubbo users can get a core of good performance, stability and service governance practices through large-scale tests, and users will be more assured to adopt 3.0 in the production system. This is also the most important mission of Dubbo 3.0.

Asynchronous Design of Filter Chain

One of Dubbo's most powerful designs is its abstract design on the Filter chain. With the open support of its extension mechanism, users can enhance Dubbo's functionality and allow each extension point to be customized to preserve.

Dubbo's Filter is defined as follows:

@SPI

public interface Filter {


    /**

     * do invoke filter.

     * <p>

     * <code>

     * // before filter

     * Result result = invoker.invoke(invocation);

     * // after filter

     * return result;

     * </code>

     *

     * @param invoker    service

     * @param invocation invocation.

     * @return invoke result.

     * @throws RpcException

     * @see org.apache.dubbo.rpc.Invoker#invoke(Invocation)

     */

    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

}

According to the statement "Calling a remote service method is like calling a local method", this way of returning the Result response directly is very good. It is simple and direct to use. The problem is that the time has changed to the era of paying attention to experience, requiring the Reactive response, and returning to the basic point: invoke an invocation needs to go through different processes of the network. Processing, naturally, is an asynchronous process. That is to say, invocation and Result are two different events, which require two process methods to process in the Filter chain. So how to transform this key SPI? There are two options:

The first is to change the return value of invoke to Completable Future. The benefit is clear. Result is not synchronized with the recommendation. However, the signature change of the underlying interface will result in tremendous amount of code modification, and the original SPI extension will not be supported at the same time.

Second, the Result interface directly inherits Completion Stage, which represents the asynchronous computation of the response. In this way, the first disadvantage can be avoided. Therefore, the 3.0.0 Preview version reconstructs the internal call link implementation: it implements the full asynchronous call in the framework based on Comppletable Future, while it supports the synchronous and asynchronous call mode in the peripheral programming.

It is worth noting that this refactoring is only implemented within the framework and has no impact on users, that is to say, the interface remains fully compatible. To understand how the Dubbo asynchronous API is used, refer to How to Implement a Fully Asynchronous Call Chain Based on Dubbo. This article will focus on the ideas and principles of implementation. The main points of this reconstruction are as follows:

  • Full asynchronous invocation model is adopted inside the framework, and only synchronous and asynchronous adaptation is done outside.
  • Built-in Filter chain supports asynchronous callback;

Basic workflow

First, let's look at a general thread model for asynchronous calls across networks:

The communication framework sends the request message asynchronously, and after the request message is successfully sent, returns the CompletableFuture representing the business result to the business thread. After that, the processing of Future will be different according to the type of call:

  1. For synchronous requests (as shown in the scenario above), business threads call future.get to synchronize blocking waiting for results, and when the business results returned from the network layer are received, future.get returns and eventually passes the results to the initiator of the call.
  2. For asynchronous requests, the business thread does not call future.get, but saves the future in the call context or returns it directly to the caller, and registers the callback listener for the future so that the listener can further process the result when the real business result returns from the communication layer.

Next, take a look at the invocation process of an asynchronous Dubbo RPC request:

  1. Consumption programmed to Proxy proxy, sent a call request, and the request was passed down the Filter chain.
  2. Invoker.invoke() forwards the request asynchronously to the network layer and receives Future on behalf of the returned result.
  3. Future is wrapped into Result, which in turn represents the result of this remote call (because of the asynchronous nature of Result, it may not contain the true return value at this time).
  4. Result continues to return along the call chain. When passing through each Filter, the Filter can choose to register the Listener listener to perform result preprocessing when the business results return.
  5. Finally, Proxy calls result.recreate() to return the result to the consumer:
  • If the method is a Completable Future signature, it returns to Future.
  • If the method is a common synchronous signature, the default value of the object is returned, and Future can be obtained through RpcContext.

6. After receiving the Future representing the asynchronous business results, the caller can choose to register the callback listener to monitor the return of the real business results.

The synchronous call and the asynchronous call are basically the same, and they are also in the callback mode. Only a blocking get call is made before the link returns to ensure that it returns when the actual result is received. Filter triggers callback onResponse()/onError() at the same time when registering Listener because Future is already in complete state.

With respect to Result mentioned in the flowchart, Result represents the return result in an RPC call of Dubbo, and Result itself adds an interface representing the state in 3.0, similar to Future, which now represents an incomplete call.

There are two ways to enable Result to represent asynchronous returns:

1. Result is a Future, and a more reasonable way in Java 8 is to inherit the CompletionStage interface.

public interface Result extends CompletionStage {


  }

2. Let Result instances hold Future instances. The difference between Result instances and 1 is whether to choose "inheritance" or "combination" in design.

public class AsyncRpcResult implements Result {

     private CompletableFuture<RpcResult> resultFuture;

  }

At the same time, in order to make Result more intuitive to reflect the characteristics of its asynchronous results, and to facilitate Result-oriented interface programming, we can consider adding some asynchronous interfaces for Result:

public interface Result extends Serializable {


    Result thenApplyWithContext(Function<Result, Result> fn);


    <U> CompletableFuture<U> thenApply(Function<Result, ? extends U> fn);


    Result get() throws InterruptedException, ExecutionException;


}

Filter SPI

Filter is Dubbo's prefabricated interceptor extension SPI, which is used to preprocess requests and post-process results. The framework itself has built-in interceptor implementations. From the user level, I believe that this SPI should also be the most expanded one. In version 3.0, Filter returns to the design pattern of single responsibility and extracts the callback interface into Listener separately.

@SPI

public interface Filter {


    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;


    interface Listener {


        void onResponse(Result result, Invoker<?> invoker, Invocation invocation);


        void onError(Throwable t, Invoker<?> invoker, Invocation invocation);

    }

}

The above is the SPI definition of Filter. There is only one invoke() method in the core definition of Filter to pass the call request.

At the same time, a new callback interface, Listener, is added. Each Filter implementation can define its own Listenr callback, so as to realize asynchronous monitoring of returned results. Reference is made to the implementation of the additional Listener callback for MonitorFilter:

class MonitorListener implements Listener {


        @Override

        public void onResponse(Result result, Invoker<?> invoker, Invocation invocation) {

            if (invoker.getUrl().hasParameter(Constants.MONITOR_KEY)) {

                collect(invoker, invocation, result, RpcContext.getContext().getRemoteHost(), Long.valueOf(invocation.getAttachment(MONITOR_FILTER_START_TIME)), false);

                getConcurrent(invoker, invocation).decrementAndGet(); // count down

            }

        }


        @Override

        public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {

            if (invoker.getUrl().hasParameter(Constants.MONITOR_KEY)) {

                collect(invoker, invocation, null, RpcContext.getContext().getRemoteHost(), Long.valueOf(invocation.getAttachment(MONITOR_FILTER_START_TIME)), true);

                getConcurrent(invoker, invocation).decrementAndGet(); // count down

            }

        }

}

Generalized call asynchronous interface support

In order to make asynchronous calls more intuitive, the generalization interface adds the CompletableFuture < Object > $invokeAsync (String method, String [] parameterTypes, Object [] args) interface:

public interface GenericService {


    /**

     * Generic invocation

     *

     * @param method         Method name, e.g. findPerson. If there are overridden methods, parameter info is

     *                       required, e.g. findPerson(java.lang.String)

     * @param parameterTypes Parameter types

     * @param args           Arguments

     * @return invocation return value

     * @throws GenericException potential exception thrown from the invocation

     */

    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;


    default CompletableFuture<Object> $invokeAsync(String method, String[] parameterTypes, Object[] args) throws GenericException {

        Object object = $invoke(method, parameterTypes, args);

        if (object instanceof CompletableFuture) {

            return (CompletableFuture<Object>) object;

        }

        return CompletableFuture.completedFuture(object);

    }


}

In this way, when we want to make an asynchronous call, we can use this directly:

CompletableFuture<Object> genericService.$invokeAsync(method, parameterTypes, args);

For more specific use cases, see The Generalized Call Example

https://github.com/apache/incubator-dubbo-samples/tree/3.x/dubbo-samples-generic/dubbo-samples-generic-call

Asynchronism and Performance

Group should note that the asynchronous implementation within the framework itself does not improve the performance of a single call. On the contrary, because of the existence of thread switching and callback logic, asynchrony may lead to a decline in the performance of a single call, but the advantage of asynchrony is that it can reduce the occupation of resources, improve the concurrency and throughput of the whole system, which need to be addressed for RPC. The scenario of network latency is very useful. For more information on the benefits of asynchronization design, please refer to other articles on asynchronization principles.

Responsive programming support

Responsive programming makes it easier for developers to write high-performance asynchronous code. Unfortunately, dubbo did not support responsive programming for a long time. Simply speaking, dubbo did not support the use of Mono/Flux streaming object (the concept of c reative-stream inflow) in rpc calls, which brought inconvenience to users. (For more detailed information on responsive programming, see here: http://reactivex.io/).

RSocket is an open source network communication protocol supporting reactive-stream semantics. It encapsulates the complex logic of c reative semantics, which makes it easy for the upper layer to implement network programs. (See here for RSocket details: http://rsocket.io/).

dubbo provides simple RSocket-based support for responsive programming in 3.0.0-SNAPSHOT. Users can use Mono and Flux objects in request parameters and return values. Here we give an example of how to use it. (Sample source code can be obtained here: https://github.com/apache/incubator-dubbo-samples/tree/3.x/dubbo-samples-rsocket).

Firstly, the interface is defined as follows:

public interface DemoService {


    Mono<String> requestMonoWithMonoArg(Mono<String> m1, Mono<String> m2);


    Flux<String> requestFluxWithFluxArg(Flux<String> f1, Flux<String> f2);


}

Then the demo interface is implemented:

public class DemoServiceImpl implements DemoService {

    @Override

    public Mono<String> requestMonoWithMonoArg(Mono<String> m1, Mono<String> m2) {

        return m1.zipWith(m2, new BiFunction<String, String, String>() {

            @Override

            public String apply(String s, String s2) {

                return s+" "+s2;

            }

        });

    }


    @Override

    public Flux<String> requestFluxWithFluxArg(Flux<String> f1, Flux<String> f2) {

        return f1.zipWith(f2, new BiFunction<String, String, String>() {

            @Override

            public String apply(String s, String s2) {

                return s+" "+s2;

            }

        });

    }

}

Then configure and start the server. Note that the protocol name is rsocket:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"

       xmlns="http://www.springframework.org/schema/beans"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">


    <!-- provider's application name, used for tracing dependency relationship -->

    <dubbo:application name="demo-provider"/>


    <!-- use registry center to export service -->

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>


    <!-- use dubbo protocol to export service on port 20880 -->

    <dubbo:protocol name="rsocket" port="20890"/>


    <!-- service implementation, as same as regular local bean -->

    <bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>


    <!-- declare the service interface to be exported -->

    <dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>


</beans>
public class RsocketProvider {


    public static void main(String[] args) throws Exception {

        new EmbeddedZooKeeper(2181, false).start();

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"spring/rsocket-provider.xml"});

        context.start();

        System.in.read(); // press any key to exit

    }


}

Then configure and start the consumer as follows. Note that the agreement name is rsocket:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"

       xmlns="http://www.springframework.org/schema/beans"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">


    <!-- consumer's application name, used for tracing dependency relationship (not a matching criterion),

    don't set it same as provider -->

    <dubbo:application name="demo-consumer"/>


    <!-- use registry center to discover service -->

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>


    <!-- generate proxy for the remote service, then demoService can be used in the same way as the

    local regular interface -->

    <dubbo:reference id="demoService" check="true" interface="org.apache.dubbo.samples.basic.api.DemoService"/>


</beans>
public class RsocketConsumer {


    public static void main(String[] args) {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"spring/rsocket-consumer.xml"});

        context.start();

        DemoService demoService = (DemoService) context.getBean("demoService"); // get remote service proxy


        while (true) {

            try {

                Mono<String> monoResult = demoService.requestMonoWithMonoArg(Mono.just("A"), Mono.just("B"));

                monoResult.doOnNext(new Consumer<String>() {

                    @Override

                    public void accept(String s) {

                        System.out.println(s);

                    }

                }).block();


                Flux<String> fluxResult = demoService.requestFluxWithFluxArg(Flux.just("A","B","C"), Flux.just("1","2","3"));

                fluxResult.doOnNext(new Consumer<String>() {

                    @Override

                    public void accept(String s) {

                        System.out.println(s);

                    }

                }).blockLast();


            } catch (Throwable throwable) {

                throwable.printStackTrace();

            }

        }

    }

}

You can see that there is nothing special about configuration except rsocket for protocol name.

Realization principle

Previously, users could not use Mono/Flux as a stream object (the concept of flow in c reative-stream) in parameters or return values. Because the flow object has its own asynchronous property, when the business passes the flow object as a parameter or return value to the framework, the framework can not serialize the flow object correctly.

dubbo implements creative support based on RSocket. RSocket encapsulates the complex logic of creative semantics and provides concise abstractions to the upper layer as follows:

/**

   * Fire and Forget interaction model of {@code RSocket}.

   *

   * @param payload Request payload.

   * @return {@code Publisher} that completes when the passed {@code payload} is successfully

   *     handled, otherwise errors.

   */

  Mono<Void> fireAndForget(Payload payload);


  /**

   * Request-Response interaction model of {@code RSocket}.

   *

   * @param payload Request payload.

   * @return {@code Publisher} containing at most a single {@code Payload} representing the

   *     response.

   */

  Mono<Payload> requestResponse(Payload payload);


  /**

   * Request-Stream interaction model of {@code RSocket}.

   *

   * @param payload Request payload.

   * @return {@code Publisher} containing the stream of {@code Payload}s representing the response.

   */

  Flux<Payload> requestStream(Payload payload);


  /**

   * Request-Channel interaction model of {@code RSocket}.

   *

   * @param payloads Stream of request payloads.

   * @return Stream of response payloads.

   */

  Flux<Payload> requestChannel(Publisher<Payload> payloads);

We just need to add our rpc logic on this basis.

  • From the client's point of view, after the framework establishes the connection, it only needs to encode the request information into Payload, and then it can initiate the request to the server through the request Stream method.
  • From the server's perspective, rsocket calls the request Stream method we implemented after receiving the request. After we decode the request information from Payload, we call the business method and get the return value of Flux type.
  • It is important to note that the business return value is usually Flux, while RSocket requires Flux, so we need to intercept business data through map operator and encode BizDO as Payload before we can submit it it to my RSocket. RSocket is responsible for data transmission and creation semantics.

Through the above analysis, we know how Dubbo implements responsive programming support based on RSocket. With the support of responsive programming, business can realize asynchronous logic more conveniently.

Summary

At present, Dubbo 3.0 will provide relevant support with contemporary features (such as responsive programming), while drawing on the design strengths of HSF inside Ali to achieve the integration of the two. Many parts of the current preview version are still under discussion. I hope you can give positive feedback, and we will learn and refer to them modestly.


Original link
This article is the original content of Yunqi Community, which can not be reproduced without permission.

Posted by ruddernz on Sat, 11 May 2019 05:33:04 -0700