×
Community Blog An Introduction to Spring Cloud Bus

An Introduction to Spring Cloud Bus

This article is an introduction to Spring Cloud Bus.

By Luoye

A series on how to use RocketMQ in the Spring ecosystem:

Spring Cloud Bus is positioned to be the message bus of the Spring Cloud system, which uses the message broker to connect all the nodes of the distributed system. The official Reference document of Bus is not rich in information; it does not even include a picture.

The latest code structure of Spring Cloud Bus is listed below:

1

Demo for Bus

Before we analyze how Bus is implemented, let’s take a look at two simple examples of how to use Spring Cloud Bus.

1. Add New Configuration Items to All Nodes

The example for Bus is relatively simple because the default configuration is already available in the AutoConfiguration layer of Bus. You only need to introduce the Spring Cloud Stream framework corresponding to the message middleware and dependencies of Spring Cloud Bus. After that, all running applications will use the same Topic to receive and send messages.

The Demo for Bus has been uploaded to GitHub. This Demo simulates the startup of five nodes. When you add a new configuration item to any of these five nodes, the configuration item will be added to all of them.

Demo Address: https://github.com/fangjian0423/rocketmq-binder-demo/tree/master/rocketmq-bus-demo

Access the Controller provided by any node to obtain the configuration address (the key is hangzhou):

curl -X GET 'http://localhost:10001/bus/env?key=hangzhou'

The results returned by all nodes are unknown because the hangzhou key does not exist in the configuration of any node.

Bus has a built-in EnvironmentBusEndpoint, which is used to add or update configuration through the message broker.

To add a new configuration item to a node, access the URL of the Endpoint of any node, which is /actuator/bus-env?name=hangzhou&value=Alibaba. For example, access the URL of node1:

curl -X POST 'http://localhost:10001/actuator/bus-env?name=hangzhou&value=alibaba' -H 'content-type: application/json'

Then, access the /bus/env of all nodes to obtain the configuration:

$ curl -X GET 'http://localhost:10001/bus/env?key=hangzhou'
unknown%
~ ⌚
$ curl -X GET 'http://localhost:10002/bus/env?key=hangzhou'
unknown%
~ ⌚
$ curl -X GET 'http://localhost:10003/bus/env?key=hangzhou'
unknown%
~ ⌚
$ curl -X GET 'http://localhost:10004/bus/env?key=hangzhou'
unknown%
~ ⌚
$ curl -X GET 'http://localhost:10005/bus/env?key=hangzhou'
unknown%
~ ⌚
$ curl -X POST 'http://localhost:10001/actuator/bus-env?name=hangzhou&value=alibaba' -H 'content-type: application/json'
~ ⌚
$ curl -X GET 'http://localhost:10005/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10004/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10003/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10002/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10001/bus/env?key=hangzhou'
alibaba%

A new configuration item has been added to all nodes, which is key=Hangzhou. The value is alibaba. The configuration item is added through the EnvironmentBusEndpoint provided by Bus.

Here’s a picture that was drawn by ChengxuyuanDD to show how Spring Cloud Config (in conjunction with Bus) refreshes all node configurations to describe the previous examples. The examples in this article are not refreshing (but adding) new configurations. The process is all the same:

2

2. Modify the Configuration of Some Nodes

For example, set the destination to rocketmq-bus-node2 on node1 (node2 can be matched as spring.cloud.bus.id is set to rocketmq-bus-node2:10002) and modify the configuration:

curl -X POST 'http://localhost:10001/actuator/bus-env/rocketmq-bus-node2?name=hangzhou&value=xihu' -H 'content-type: application/json'

Access /bus/env to obtain the configuration. The message is sent from node1, so Bus also modifies the configuration of node1:

~ ⌚
$ curl -X POST 'http://localhost:10001/actuator/bus-env/rocketmq-bus-node2?name=hangzhou&value=xihu' -H 'content-type: application/json'
~ ⌚
$ curl -X GET 'http://localhost:10005/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10004/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10003/bus/env?key=hangzhou'
alibaba%
~ ⌚
$ curl -X GET 'http://localhost:10002/bus/env?key=hangzhou'
xihu%
~ ⌚
$ curl -X GET 'http://localhost:10001/bus/env?key=hangzhou'
xihu%

Only the configuration of node1 and node2 is modified. The remaining three nodes remain unchanged.

Implementation of Bus

1. Concepts of Bus

1) Event

A remote event RemoteApplicationEvent is defined in Bus. This event inherits the event ApplicationEvent of Spring and has four implementations:

3

  • EnvironmentChangeRemoteApplicationEvent: The remote environment change event. This event mainly receives the Map<String,String> data and inserts the data into the Environment events of Spring context. The example in this article is completed by using this event in conjunction with EnvironmentBusEndpoint and EnvironmentChangeListener.
  • AckRemoteApplicationEvent: The remote acknowledgement event. Bus will send an AckRemoteApplicationEvent event after receiving a remote event successfully.
  • RefreshRemoteApplicationEvent: The remote configuration refreshing event. This event is used in conjunction with the @RefreshScope annotation and all @ConfigurationProperties annotations to dynamically refresh the configuration class annotated by these annotations.
  • UnknownRemoteApplicationEvent: The remote unknown event. If an exception is thrown when internal messages of Bus are converted into remote events, the exception will be encapsulated as this event.

Bus also has a non-RemoteApplicationEvent event, SentApplicationEvent, a message sending event. This event is used in conjunction with Trace to record the sending of remote messages.

All these events are used in conjunction with ApplicationListener. For example, EnvironmentChangeRemoteApplicationEvent is used in conjunction with EnvironmentChangeListener to add or update the configuration:

public class EnvironmentChangeListener
        implements ApplicationListener<EnvironmentChangeRemoteApplicationEvent> {
    private static Log log = LogFactory.getLog(EnvironmentChangeListener.class);
    @Autowired
    private EnvironmentManager env;
    @Override
    public void onApplicationEvent(EnvironmentChangeRemoteApplicationEvent event) {
        Map<String, String> values = event.getValues();
        log.info("Received remote environment change request. Keys/values to update "
                + values);
        for (Map.Entry<String, String> entry : values.entrySet()) {
            env.setProperty(entry.getKey(), entry.getValue());
        }
    }
}

Call the EnvironmentManager#setProperty method to set the configuration upon receiving the EnvironmentChangeRemoteApplicationEvent event from other nodes. This method sends an EnvironmentChangeEvent event for each configuration item, which is listened for by ConfigurationPropertiesRebinder to perform the rebind operation for adding or updating the configuration.

2) Actuator Endpoint

Bus provides two Endpoints (EnvironmentBusEndpoint and RefreshBusEndpoint) to add or update the configuration or refresh the global configuration. Their corresponding Endpoint IDs (or URLs) are bus-env and bus-refresh, respectively.

3) Configuration

Message sending in Bus inevitably involves the Topic and Group information. Such content has been encapsulated in BusProperties. The default prefix is spring.cloud.bus. For example:

  • spring.cloud.bus.refresh.enabled is used to enable or disable the Listener that listens for the global refresh event.
  • spring.cloud.bus.env.enabled is used to enable or disable the Endpoint for adding or updating the configuration.
  • spring.cloud.bus.ack.enabled is used to enable or disable the sending of the AckRemoteApplicationEvent event.
  • spring.cloud.bus.trace.enabled is used to enable or disable the Listener that listens for the Trace.

The default Topic for sending messages is springCloudBus, which can be changed through configuration. You can set the Group to the broadcasting mode or set it to the latest mode using UUID in conjunction with the offset.

Each Bus application has a unique Bus ID. The official format of the Bus ID is complex:

${vcap.application.name:${spring.application.name:application}}:${vcap.application.instance_index:${spring.application.index:${local.server.port:${server.port:0}}}}:${vcap.application.instance_id:${random.value}}

We recommend manual Bus ID setting because the destinations of Bus remote events need to match the Bus ID:

spring.cloud.bus.id=${spring.application.name}-${server.port}

2. Underlying Analysis of Bus

The underlying analysis of Bus involves the following questions:

  • How are the messages sent?
  • How are the messages received?
  • How is the destination matched?
  • How is the next action triggered after a remote event is received?

The BusAutoConfiguration class is annotated by the @EnableBinding(SpringCloudBusClient.class) annotation.

The introduction of @EnableBinding usage is in the article Introduction to the Spring Cloud Stream System. Its value is SpringCloudBusClient.class. It will create the DirectChannel for the input and output in SpringCloudBusClient based on the proxy:

public interface SpringCloudBusClient {
    String INPUT = "springCloudBusInput";
    String OUTPUT = "springCloudBusOutput";
    @Output(SpringCloudBusClient.OUTPUT)
    MessageChannel springCloudBusOutput();
    @Input(SpringCloudBusClient.INPUT)
    SubscribableChannel springCloudBusInput();
}

The Binding properties springCloudBusInput and springCloudBusOutput can be changed by modifying the configuration file (for example, by modifying the topic):

spring.cloud.stream.bindings:
  springCloudBusInput:
    destination: my-bus-topic
  springCloudBusOutput:
    destination: my-bus-topic

Message receiving and sending:

// BusAutoConfiguration
@EventListener(classes = RemoteApplicationEvent.class) // 1
public void acceptLocal(RemoteApplicationEvent event) {
    if (this.serviceMatcher.isFromSelf(event)
            && !(event instanceof AckRemoteApplicationEvent)) { // 2
        this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build()); // 3
    }
}
@StreamListener(SpringCloudBusClient.INPUT) // 4
public void acceptRemote(RemoteApplicationEvent event) {
    if (event instanceof AckRemoteApplicationEvent) {
        if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event)
                && this.applicationEventPublisher != null) { // 5
            this.applicationEventPublisher.publishEvent(event);
        }
        // If it's an ACK we are finished processing at this point
        return;
    }
    if (this.serviceMatcher.isForSelf(event)
            && this.applicationEventPublisher != null) { // 6
        if (!this.serviceMatcher.isFromSelf(event)) { // 7
            this.applicationEventPublisher.publishEvent(event);
        }
        if (this.bus.getAck().isEnabled()) { // 8
            AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this,
                    this.serviceMatcher.getServiceId(),
                    this.bus.getAck().getDestinationService(),
                    event.getDestinationService(), event.getId(), event.getClass());
            this.cloudBusOutboundChannel
                    .send(MessageBuilder.withPayload(ack).build());
            this.applicationEventPublisher.publishEvent(ack);
        }
    }
    if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) { // 9
        // We are set to register sent events so publish it for local consumption,
        // irrespective of the origin
        this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this,
                event.getOriginService(), event.getDestinationService(),
                event.getId(), event.getClass()));
    }
}
  1. Use the Spring event listener to listen for all local RemoteApplicationEvent. For example, the bus-env sends the EnvironmentChangeRemoteApplicationEvent locally, and the bus-refresh sends the RefreshRemoteApplicationEvent locally. These events will be listened to here.
  2. Verify that an event received by the application is not an AckRemoteApplicationEvent. (Otherwise, Bus will get stuck in an endless loop, receiving and sending messages repeatedly.) Then, verify that the event was sent by the application itself. If both conditions are met, perform Step 3.
  3. Create a Message using the remote event as the payload. Then, use the MessageChannel (the Binding name is springCloudBusOutput) created by Spring Cloud Stream to send the message to the broker.
  4. @StreamListener consumes MessageChannel (the Binding name is springCloudBusInput) created by Spring Cloud Stream, and the message received is a remote message.
  5. If the remote event is an AckRemoteApplicationEvent, the trace function is enabled, and the event is not sent by the application itself, which indicates that the event is sent by another application. The local sending of AckRemoteApplicationEvent indicates that the application acknowledges that it has received the remote event sent by another application. Thus, the process ends.
  6. If the remote event is sent by another application to this application, perform Step 7 and Step 8. Otherwise, perform Step 9.
  7. If the remote event is sent by another application, send the event locally. If the application has already been processed by the corresponding message recipient, a repeat sending is not needed.
  8. If AckRemoteApplicationEvent is enabled, create an AckRemoteApplicationEvent and send this event remotely and locally. (The reason for local sending is that the AckRemoteApplicationEvent was not sent in Step 5, which means that the application acknowledges itself. Remote sending is to tell other applications that the application has received the message.)
  9. If Trace is enabled, create and send the SentApplicationEvent locally.

After bus-env is triggered, the EnvironmentChangeListener of all nodes will detect the configuration change, and controllers of all these nodes will print the following information:

o.s.c.b.event.EnvironmentChangeListener  : Received remote environment change request. Keys/values to update {hangzhou=alibaba}

If you listen for AckRemoteApplicationEvent on the current node, you will receive information from all nodes. For example, the AckRemoteApplicationEvent listened for on node5 is listed below:

ServiceId [rocketmq-bus-node5:10005] listeners on {"type":"AckRemoteApplicationEvent","timestamp":1554124670484,"originService":"rocketmq-bus-node5:10005","destinationService":"**","id":"375f0426-c24e-4904-bce1-5e09371fc9bc","ackId":"750d033f-356a-4aad-8cf0-3481ace8698c","ackDestinationService":"**","event":"org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent"}
ServiceId [rocketmq-bus-node5:10005] listeners on {"type":"AckRemoteApplicationEvent","timestamp":1554124670184,"originService":"rocketmq-bus-node1:10001","destinationService":"**","id":"91f06cf1-4bd9-4dd8-9526-9299a35bb7cc","ackId":"750d033f-356a-4aad-8cf0-3481ace8698c","ackDestinationService":"**","event":"org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent"}
ServiceId [rocketmq-bus-node5:10005] listeners on {"type":"AckRemoteApplicationEvent","timestamp":1554124670402,"originService":"rocketmq-bus-node2:10002","destinationService":"**","id":"7df3963c-7c3e-4549-9a22-a23fa90a6b85","ackId":"750d033f-356a-4aad-8cf0-3481ace8698c","ackDestinationService":"**","event":"org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent"}
ServiceId [rocketmq-bus-node5:10005] listeners on {"type":"AckRemoteApplicationEvent","timestamp":1554124670406,"originService":"rocketmq-bus-node3:10003","destinationService":"**","id":"728b45ee-5e26-46c2-af1a-e8d1571e5d3a","ackId":"750d033f-356a-4aad-8cf0-3481ace8698c","ackDestinationService":"**","event":"org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent"}
ServiceId [rocketmq-bus-node5:10005] listeners on {"type":"AckRemoteApplicationEvent","timestamp":1554124670427,"originService":"rocketmq-bus-node4:10004","destinationService":"**","id":"1812fd6d-6f98-4e5b-a38a-4b11aee08aeb","ackId":"750d033f-356a-4aad-8cf0-3481ace8698c","ackDestinationService":"**","event":"org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent"}

Now, let’s go back to the four questions from the beginning of this section:

  • How are the messages sent: Bus sends events to the springCloudBustopic using the BusAutoConfiguration#acceptLocal method through Spring Cloud Stream.
  • How are the messages received: Bus receives messages from the springCloudBustopic using the BusAutoConfiguration#acceptRemote method through Spring Cloud Stream.
  • How is the destination matched: Bus matches the destination using the remote event receiving method in the BusAutoConfiguration#acceptRemote method.
  • How is the next action triggered after a remote event is received: Bus performs actions in the next step after receiving the local RemoteApplicationEvent through the Spring event mechanism. (For example, the EnvironmentChangeListener receives the EnvironmentChangeRemoteApplicationEvent, and the RefreshListener receives the RefreshRemoteApplicationEvent.)

Summary

Spring Cloud Bus does not have too much content. However, you need to understand the Spring Cloud Stream system and the Spring event mechanism to understand how Spring Cloud Bus processes local and remote events.

Currently, Bus provides only a few built-in remote events, while most events are related to configuration. We can use the RemoteApplicationEvent in conjunction with the @RemoteApplicationEventScan annotation to build our own microservice message system.

About the Author

Fang Jian (Luoye), GitHub ID: @fangjian0423 Is an open-source fan, Alibaba Cloud Senior Development Engineer, and Alibaba Cloud EDAS Developer. Fang Jian is one of the owners of the open-source Spring Cloud Alibaba project.

0 0 0
Share on

You may also like

Comments

Related Products

  • Function Compute

    Alibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.

    Learn More
  • ApsaraMQ for RocketMQ

    ApsaraMQ for RocketMQ is a distributed message queue service that supports reliable message-based asynchronous communication among microservices, distributed systems, and serverless applications.

    Learn More
  • Managed Service for Prometheus

    Multi-source metrics are aggregated to monitor the status of your business and services in real time.

    Learn More
  • Elastic High Performance Computing Solution

    High Performance Computing (HPC) and AI technology helps scientific research institutions to perform viral gene sequencing, conduct new drug research and development, and shorten the research and development cycle.

    Learn More