Dubbo Service Reference Resolution

Posted by cottonbuds2005 on Sun, 06 Mar 2022 18:22:41 +0100

Last time we talked about Dubbo's service exposure, this time we'll look at how Dubbo calls services

This article will be interpreted based on the architecture diagram of dubbo

Catalog

Client Start Process

Dubbo Service Call

Client Start Process

As we all know, when a client invokes a service, only the interface has no corresponding implementation class, so we need to generate a proxy and execute the service through the proxy.

        

You can see that we get the Bean of DemoService from ioc, but there is no DemoService bean in the client itself. At this time, we can see that such a configuration in the configuration file uses the reference tag. The service configured by the reference tag in dubbo will be proxied by another Bean, that is, the ReferenceBean. It will put the generated proxy object into IOC through the getObject() method, so point in

<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService"/>

The proxy object of the interface is checked here, and if it is not, a proxy object is initialized

public synchronized T get() {
        checkAndUpdateSubConfigs();

        if (destroyed) {
            throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
        }
        // Detect if ref is empty, or create it by init method
        if (ref == null) {
            // The init method is primarily used to handle configurations and to call createProxy to generate proxy classes
            init();
        }
        return ref;
    }

In the click-in init() method, you can see that the core method is to create proxy objects (you can see some information about our own configuration, interface paths, etc.)

Then there is a judgment about the number of providers, whether they are multiple or single, then generate invokers, and finally return the generated proxies based on invokers, focusing on how to generate invokers, where the protocols are adaptive, so the corresponding implementation classes are selected based on the information in the url, you can see that the registry is used in the protocol, and you come to RegistryProtocol

            // Individual registry or service provider (direct service connection, same below)
            if (urls.size() == 1) {
                // Call RegistryProtocol's refer to build an Invoker instance
                invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0)); // registry://192.168.200.129:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&pid=14276&qos.port=33333&refer=application%3Ddemo-consumer%26check%3Dfalse%26dubbo%3D2.0.2%26interface%3Dorg.apache.dubbo.demo.DemoService%26lazy%3Dfalse%26methods%3DsayHello%26pid%3D14276%26qos.port%3D33333%26register.ip%3D192.168.200.10%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1622603489641&registry=zookeeper&timestamp=1622603494381
            } else {
                // Multiple registries, multiple service providers, or a mix of both
                List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
                URL registryURL = null;
                // Get all Invoker s into invokers
                for (URL url : urls) {
                    // By ref_protocol calls refer to build Invoker, refprotocol will run
                    // Loads the specified Protocol instance according to the url protocol header and calls the refer method of the instance
                    invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
                    if (REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        registryURL = url; // use last registry url
                    }
                }
                if (registryURL != null) { // registry url is available
                    // use RegistryAwareCluster only when register's CLUSTER is available
                    // If the registry link is not empty, AvailableCluster will be used
                    URL u = registryURL.addParameter(CLUSTER_KEY, RegistryAwareCluster.NAME);
                    // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
                    // Create a StaticDirectory instance and merge multiple Invoker s with Cluster Cluster Extension Points FailoverCluster is the default
                    invoker = CLUSTER.join(new StaticDirectory(u, invokers));
                } else { // not a registry url, must be direct invoke.
                    invoker = CLUSTER.join(new StaticDirectory(invokers));
                }
            }

// invoker availability check
        if (shouldCheck() && !invoker.isAvailable()) {
            throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
        }
        if (logger.isInfoEnabled()) {
            logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
        }
        /**
         * @since 2.7.0
         * ServiceData Store
         */
        MetadataReportService metadataReportService = null;
        if ((metadataReportService = getMetadataReportService()) != null) {
            URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map);
            metadataReportService.publishConsumer(consumerURL);
        }
        // create service proxy really generates proxy PROXY_based on Invoker FACTORY=ProxyFactory$ Adaptive@xxxx
        return (T) PROXY_FACTORY.getProxy(invoker);

Go to refer() in RegistryProtocol s, where you get the registry based on the configuration information and connect to zk, then pass in a doRefer() containing cluster fault tolerance and load balancing clusters, registries, interface types, and URLs

In doRefer, you register yourself with the registry, then subscribe to the service (focus) to generate an invoker for each provider, and wrap multiple invokers into one

    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        url = URLBuilder.from(url)
                .setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY))
                .removeParameter(REGISTRY_KEY)
                .build();
        //Get registry instance ZookeeperRegistry url= zookeeper://192.168.200.129:2181/org.apache.dubbo.registry.RegistryService?application=demo -consumer&dubbo=2.0.2&interface=org. Apache. Dubbo. Registry. RegistryService&pid=14276&qos. Port=33333&timestamp=1622603494381
        Registry registry = registryFactory.getRegistry(url);// Get an instance and use curator to connect to zk
        if (RegistryService.class.equals(type)) {
            return proxyFactory.getInvoker((T) registry, type, url);
        }

        // group="a,b" or group="*" Converts url query string to Map
        Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
        String group = qs.get(GROUP_KEY); // //Get group configuration
        if (group != null && group.length() > 0) {
            if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
                // Load MergeableCluster instance through SPI and call doRefer to continue executing service reference logic
                return doRefer(getMergeableCluster(), registry, type, url);
            }
        }
        return doRefer(cluster, registry, type, url);
    }


    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        // Create RegistryDirectory Instance
        RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
        // Set up a registry and protocol
        directory.setRegistry(registry);
        directory.setProtocol(protocol);
        // all attributes of REFER_KEY
        Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
        // Generate service consumer links consumer://192.168.200.10/org.apache.dubbo.demo.DemoService?application=demo -consumer&check=false&dubbo=2.0.2&interface=org. Apache. Dubbo. Demo. DemoService&lazy=false&methods=sayHello&pid=12468&qos. Port=33333&side=consumer&sticky=false&timestamp=1622604057385
        URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
        // Register service consumers and create new nodes in the consumers directory
        if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
            directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
            registry.register(directory.getRegisteredConsumerUrl());
        }
        directory.buildRouterChain(subscribeUrl);
        /**
         * Subscribe to node data such as providers, configurators, routers
         * Once you get the provider information under providers, you create a client (nettyclient) and connect to the server, focusing on the protocolBindingRefer in DubboProtocol s
         * ********So focus on this method call****
         */
        directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

        Invoker invoker = cluster.join(directory); // There may be multiple service providers under a registry, one invoker for each provider, where multiple invokers are merged into one
        ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
        return invoker;
    }

Entering the subscription section, the subscription itself is an extension point, and since we use zk, we end up with doSubscribe in ZookeeperRegistry, which, after a series of ZK operations, notifies all client-pulled provider nodes to encapsulate them as invoker s

    public synchronized void notify(List<URL> urls) {
        Map<String, List<URL>> categoryUrls = urls.stream()
                .filter(Objects::nonNull)
                .filter(this::isValidCategory)
                .filter(this::isNotCompatibleFor26x)
                .collect(Collectors.groupingBy(url -> {
                    if (UrlUtils.isConfigurator(url)) {
                        return CONFIGURATORS_CATEGORY;
                    } else if (UrlUtils.isRoute(url)) {
                        return ROUTERS_CATEGORY;
                    } else if (UrlUtils.isProvider(url)) {
                        return PROVIDERS_CATEGORY;
                    }
                    return "";
                }));

        List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
        this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);

        List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
        toRouters(routerURLs).ifPresent(this::addRouters);

        // Providers get the URL for providers
        List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
        /**
         * Encapsulate each provider url under/providers as invoker ****** Key *****
         */
        refreshOverrideAndInvoker(providerURLs);
    }

This encapsulates and connects the provider as invoker

This is created from the url, and the protocol itself is an extension point. By default, DubboProtocols are used to birth invoker. This invoker has an InvokerDelegate layer on top of it. We focus on the content in DubboProtocols.

When creating a DubboInvoker, you can see that there is also a client in the constructor, which first checks to see if the provider has a local connection, or if it is used directly, it needs to be created if it is not

    @Override
    public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
        optimizeSerialization(url);

        /**
         * create rpc invoker. Create DubboInvoker
         *  Focus on getClients (urls) getting clients from provider url s.
         *
         *  url=dubbo://192.168.200.10:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-consumer&bean.name=org.apache.dubbo.demo.DemoService&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&lazy=false&methods=sayHello&pid=20192&qos.port=33333&register=true&register.ip=192.168.200.10&release=&remote.application=demo-provider&side=consumer&sticky=false&timestamp=1622964228064
         */
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);

        return invoker;
    }

    private ExchangeClient[] getClients(URL url) {
        // whether to share connection shares connection or not
        boolean useShareConnect = false;

        int connections = url.getParameter(CONNECTIONS_KEY, 0);// Gets the number of connections, defaulting to 0, indicating no configuration
        List<ReferenceCountExchangeClient> shareClients = null;
        // if not configured, connection is shared, otherwise, one connection for one service
        if (connections == 0) {
            useShareConnect = true;

            /**
             * The xml configuration should have a higher priority than properties. xml Configuration takes precedence over properties
             *  Gets the value of the shareconnections property in <dubbo:consumer/> to indicate the number of shared connections
             */
            String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
            connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
                    DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);

            shareClients = getSharedClient(url, connections); // Get a specified number of shared connections (first acquisition will not be created yet)
        }
        // Connections represent the number of connections at this time, no longer distinguishing between shared or newly created connections
        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            // If shared, take directly from shareClients
            if (useShareConnect) {
                // Get Shared Clients
                clients[i] = shareClients.get(i);
            } else {
                //  If not shared, new connections initiate new clients where you can focus your attention
                clients[i] = initClient(url);
            }
        }

        return clients;
    }

Just take a look to see if there is a configuration for lazy loading first, or if it is not, create a normal one. You may remember that Exchanger has a friend who also has an Exchanger when exposed on the server side. This is an extension in itself, and HeaderExchanger is used by default.

    private ExchangeClient initClient(URL url) {

        // client type setting. Get the client type, netty by default
        String str = url.getParameter(CLIENT_KEY, url.getParameter(SERVER_KEY, DEFAULT_REMOTING_CLIENT));

        // Add codec and url
        url = url.addParameter(CODEC_KEY, DubboCodec.NAME); // dubbo codec, important
        // enable heartbeat by default adds a heartbeat parameter to the url
        url = url.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT));

        // BIO is not allowed since it has severe performance issue.
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported client type: " + str + "," +
                    " supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
        }

        ExchangeClient client;
        try {
            // connection should be lazy Gets the lazy configuration and determines the type of client to create based on the configuration value
            if (url.getParameter(LAZY_CONNECT_KEY, false)) {
                // Create lazy-loading ExchangeClient instance
                client = new LazyConnectExchangeClient(url, requestHandler);
            } else {
                /**
                 * Create a normal exchangeClient instance Analysis This block creates an exchangeClient that binds a Netty Client
                 *
                 * ExchangeHandler requestHandler = new ExchangeHandlerAdapter Internal implementation in DubboProtocol
                 */
                client = Exchangers.connect(url, requestHandler);
            }

        } catch (RemotingException e) {
            throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
        }

        return client;
    }

Connections are made in HeaderExchanger, and netty is used by default. As mentioned earlier, netty is no longer repeated.

public class HeaderExchanger implements Exchanger {

    public static final String NAME = "header";

    @Override
    public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
        /**
         * There are several calls, as follows:
         * 1. Create HeaderExchangeHandler object parameter handler= DubboProtocol. RequHandler is important and involves processing when sending a request
         * 2. Create DecodeHandler Object
         * 3. Building Client Instances with Transporters
         * 4. Create HeaderExchangeClient object
         *
         * Focus on: Transporters.connect
         */
        return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
    }

    @Override
    public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        // Create a HeaderExchangeServer instance, which contains multiple logic, as follows:
        //   1. new HeaderExchangeHandler(handler) load handling request writeback results
        //	 2. New DecodeHandler (new HeaderExchangeHandler)) decodes request data and response results and how to submit them to subsequent processes for further processing
        //   3. Transporters. Bind (url, new DecodeHandler) (new HeaderExchangeHandler) (handler)) focuses on this bind method.
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
    }

}

So here we get the wrapped invoker and go back to where the proxy is generated from invoker (above, invoker, below), proxy_factory itself is an extension point, using JavassistProxyFactory by default

Notable here is that new InvokerInvocationHandler, which implements jdk's InvocationHandler, injects invoker in and intercepts when executing the corresponding method

 

 

The connection to the server and the registration are all done here. The next step is the call to the service.

Dubbo Service Call

The client proxy generation was just described above, so when the next generated proxy method is called, it's not Dubbo's call chain. Let's look directly at the code

You can see that demoService is the InvokerInvocationHandler just generated, and when the method is executed, the invoke method is called

First we exclude methods from the Object class, then we dispatch toString methods, and then we encapsulate our methods and parameters into RpcInvocation

MockClusterInvoker determines whether it is a mock call or not, and if it is not, it is passed directly to AbstractClusterInvoker. There are three main operations here: first, routing from the list of Invokers in the registry, then initializing the load balancing policy, and then passing in the FailoverClusterInvoker subclass

There is a fault tolerance mechanism in FailoverClusterInvoker, which has a default number of retries (default 2) +1, and then loops, selecting the appropriate Invoker through load balancing and calling it

Next comes DubboInvoker, where it is worth noting that a Future is created to subscribe to the results of the call

The client's netty is then called to send in the HeaderExchangeChannel, which is a familiar name to see because HeaderExchange Channel exists on both the server and the client

The message is then sent in dubbo encapsulated NettyChannel, but before the message is actually sent, it has to be encoded twice and once, after which the request is transmitted to the server via channel

Next, let's look at the code on the server side. The first thing the server receives the request is decoding, so we directly see the decoding in NettyServer. As for the design of a Dubbo protocol before decoding, the protocol header will have 16 bytes of location, so the information stored will not be explained one by one.

 

You can see that in the form of a do while, 16 bytes of the protocol header are read first, and if not 16, a NEED_is set MORE_ The state of the INPUT and throws it, waiting for the next read

After reading the protocol header, it will be parsed and judged, such as getting the request id to determine if it is a heartbeat

Finally, the request is judged and returned to the business processor

In business processing you can see that some of the information in our request has been parsed into objects, such as our method name

The call passes through HeartbeatHandler to determine if it is a heartbeat

Next comes the point, dubbo can set request dispatch, what does it mean, that is, when processing a task, it can be executed in the io thread of netty or in the pool of worker threads, depending on the type of task. I do not configure it here, so it executes in the worker thread by default, so it comes to AllChannelHandler

 

You can see how events are handled in an asynchronous thread

        

You can see that the received method decodes again because we didn't decode the incoming parameters before, so we have to decode them here.

Get the request id here first, after all, one-to-one correspondence, call will get a future, when the future has a result, it will write the response results back to the client through the channel

The specific calling process first obtains the invoker from the information and then invokes it through invoker. Since invoker is a proxy, the implementation class of the server side is executed during the call, and the result is returned

 

This is an introduction to the calling process of dubbo.

Topics: Java Netty Zookeeper Distribution rpc