The author | ray volume source | alibaba cloud native public number
RSocket distributed communication protocol is the core content of Spring Reactive. Since Spring Framework 5.2, RSocket has been a built-in function of Spring. Spring Boot 2.3 also adds spring-boot-starter-rsocket, simplifying rsocket service writing and service invocation. The core architecture of RSocket communication consists of two modes: Broker and direct service communication.
Brokers have more flexible communication patterns, such as Alibaba RSocket Broker, which uses an event-driven model architecture. At present, more architectures are service-oriented design, which is often referred to as the mode of service registration discovery and service direct communication. The most well-known one is The Spring Cloud technology stack, which involves configuration push, service registration discovery, service gateway, interruption protection and so on. In service-oriented distributed network communications, such as REST apis, gRPC, and Alibaba Dubbo, which are well integrated with Spring Cloud, users are not concerned with the low-level details of service registration discovery and client load balancing. A very stable distributed network communication architecture can be completed.
As a rising star of communication protocol, RSocket is the core of binary asynchronous message communication. Can it be combined with Spring Cloud technology stack to realize service registration discovery and client load balancing, so as to realize service-oriented architecture more efficiently? In this article we will discuss the combination of Spring Cloud and RSocket for service registration discovery and load balancing.
Service Registration Discovery
The principle of service registry discovery is very simple and involves three roles: service provider, service consumer, and service registry. A typical architecture is as follows:
After the application is started, the service provider, such as RSocket Server, will register the information related to the application with the service registry, such as the application name, IP address, Web Server listening port number, etc. Of course, it will also include some meta information, such as the service group, the version of the service. If it is WebSocket communication, it also needs to provide WS mapping path, etc. Many developers will submit the list of service interfaces of service providers to the service registry as tags, which is convenient for follow-up service query and governance.
In this article, we will use Consul as the service registry. Consul is relatively easy to download and run Consul agent-dev to start the corresponding service. Of course, you can use Docker Compose. Docker-compose up -d can then start Consul service.
When we register and query services with the service center, we need to have an application name corresponding to Spring Cloud, namely the value of Spring.Application. name corresponding to Spring Boot, which is called the application name here. That is, subsequent service lookups are based on the application name. If you call ReactiveDiscoveryClient. GetInstances (String serviceId); When looking up a list of service instances, the serviceId parameter is actually the name of the Spring Boot application. In consideration of service registration and subsequent RSocket service routing and ease of understanding, we are going to design a simple naming convention.
Suppose you have a service application named Calculator that provides two services simultaneously: Math calculator service (MathCalculatorService) and currency calculator service (ExchangeCalculatorService), then how can we named the application and the corresponding service interface name?
In this case, we used a naming convention similar to Java Package and inverted domain name. For example, the calculator application would correspond to com-example-Calculator style. Why was it underlined instead of dotted? In THE DNS resolution as a host name is illegal, can only exist as a subdomain name, not as a host name, and the current design of service registry follow the DNS protocol, so we use the way of hyphen to name the application. In this way, the combination of domain name inversion and application name can ensure that applications do not have the same name. In addition, it is convenient to convert Java Package name, namely – and. The conversion between.
So how should the service interface that the application contains be named? The full name of the service interface is a combination of the application name and interface name. The rules are as follows:
String serviceFullName = appName.replace("-", ".") + "." + serviceInterfaceName;
For example, the following service names are canonical:
-
com.example.calculator.MathCalculatorService
-
com.example.calculator.ExchangeCalculatorService
And com. Example. Calculator. Math. MathCalculatorService is wrong, because in the application name and the interface between name much math. Why this naming convention? Let’s first look at how the service consumer invokes the remote service. Assume that the service consumer to a service interface, such as com. Example. Calculator. MathCalculatorService, so he how to launch the service call?
-
Firstly, the corresponding application name (appName) is comprehensively extracted according to Service. Such as com. Example. Calculator. Corresponding appName MathCalculatorService service is com – example – the calculator. If there is no relationship between the application and the service interface, then to get the service provider information for the service interface, you may also need the application name, which can be relatively cumbersome. If the interface name contains the corresponding application information, it is much easier to understand that the application is part of the overall service.
-
Call ReactiveDiscoveryClient. GetInstances (appName) to obtain a list of application of corresponding service instance (ServiceInstance), ServiceInstance object will contain such as IP addresses, Web port number, RSocket listening port number and other meta information.
-
According to the RSocketRequester. Builder. Transports (servers) construct RSocketRequester object capable of load balancing.
-
RSocketRequester API call using the full name of the service and the specific function name as the route. The sample code is as follows:
rsocketRequester .route("com.example.calculator.MathCalculatorService.square") .data(number) .retrieveMono(Integer.class)
Using the naming convention above, we can extract the application name from the full name of the service interface, then interact with the service registry to find the corresponding instance list, then establish a connection with the service provider, and finally make a service call based on the service name. This naming convention basically achieves the minimum dependency, the developer is completely based on the service interface call, very simple.
RSocket service preparation
With a naming convention for the service and a service registry, writing an RSocket service is as simple as writing a Spring Bean. Introduce the spring-boot-starter-rsocket dependency, create a Controller class, add the corresponding MessagMapping annotation as the basic route, and then implement the function interface to add the function name. The sample code is as follows:
@Controller @MessageMapping("com.example.calculator.MathCalculatorService") public class MathCalculatorController implements MathCalculatorService { @MessageMapping("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); }}
The above code may seem a bit strange, but adding @Controller and @messagemapping may seem a bit nonsensics since it’s a service implementation. Of course, these annotations are technical details, and as you can see, the service implementation of RSocket is based on Spring Message, which is message-oriented. Here we just need to add a custom @springrSocketService annotation to solve this problem:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @MessageMapping() public @interface SpringRSocketService { @AliasFor(annotation = MessageMapping.class) String[] value() default {}; }
Going back to the implementation code for the service, let’s use the @springrSocketService annotation instead, so that our code looks exactly like a standard RPC service interface and is easy to understand. In addition, the two annotations @springrSocketService and @rSockethandler also facilitate our subsequent Bean scanning and IDE plug-in assistance.
@SpringRSocketService("com.example.calculator.MathCalculatorService") public class MathCalculatorImpl implements MathCalculatorService { @RSocketHandler("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); }}
Finally we add the spring-cloud-starter-y-y-discovery dependency and set the bootstrap.properties Then set the port and meta information that RSocket listens to in application.properties. We also send the list of service interfaces provided by the application as tags to the service registry, which is also convenient for our subsequent service management. Here’s an example:
spring.application.name=com-example-calculator spring.cloud.consul.discovery.instance-id=com-example-calculator-${random.uuid} spring.cloud.consul.discovery.prefer-ip-address=true server.port=0 spring.rsocket.server.port=6565 spring.cloud.consul.discovery.metadata.rsocketPort=${spring.rsocket.server.port} spring.cloud.consul.discovery.tags=com.example.calculator.ExchangeCalculatorService,com.example.calculator.MathCalculato rService
After the RSocket service application is started, we can see the service registration information on Consul console, as shown in the following screenshot:
RSocket Client access
Client access is a bit more complicated, mainly based on the overall service interface to do a series of related operations, but we already have a naming convention, so it is not a problem. The client application will also access the service registry, so that we can obtain the ReactiveDiscoveryClient bean, and then according to the service interface name, Such as com. Example. Calculator. ExchangeCalculatorService construct RSocketRequester with load balancing.
Principle is very simple also, said earlier, according to the service interface name, for its corresponding application name, and then call ReactiveDiscoveryClient. GetInstances (appName) to obtain the corresponding service application instance list, The ServiceInstance list is then converted to RSockt’s LoadbalanceTarget list, which is essentially a POJO transformation, Finally, transfer the LoadbalanceTarget list for Flux encapsulation (such as using the Sink interface) and pass it to RSocketRequester.Builder will complete the construction of RSocketRequester with load balancing capability. For detailed code details, you can refer to the project’s code base.
The next thing to notice is how the server instance list changes, such as the application goes online, the service pauses, and so on. Here, I use a scheduled task scheme to periodically query the address list corresponding to the service. Of course, there are other mechanisms, if the standard Spring Cloud service discovery interface, currently requires client polling, of course, can also be combined with the Spring Cloud Bus or messaging middleware to monitor changes in the server list. If the client senses the change of the service list, it only needs to call the Reactor Sink interface to send a new list. RSocket Load Balance will automatically respond to the change, such as closing the failing connection and creating a new connection.
In the actual communication between applications, some service providers may be unavailable. For example, the service provider suddenly breaks down or its network is unavailable. As a result, some services in the service application list are unavailable. Don’t worry, RSocket Load Balance has a retry mechanism. When a service invocation fails, a connection is retrieved from the list for communication, and the faulty connection is marked as available 0 and will not be used by subsequent requests. Service list push and fault-tolerant retry mechanism during communication ensure high availability of distributed communication.
Finally, let’s start the client-app and make a remote RSocket call from the client, as shown in the screenshot below:
The com-Example-Calculator service application in the figure above consists of three instances in which calls to the service are made alternately (RoundRobin policy).
Some considerations for the development experience
Although service registration and discovery, client load balancing, invocation and fault tolerance are all done without problems, there are still some user experience issues, which we also explain here to make the development experience better.
1. Communicate based on service interfaces
Most RPC communications are based on interfaces, such as Apache Dubbo, gRPC, etc. So can RSocket do that? The answer is absolutely yes. On the server side, we already implement the RSocket service based on the service interface. Now we just need to implement the call based on the interface on the client side. For Java developers, this is not a big problem, we just need to build on the Java Proxy mechanism, and the Proxy corresponding InvocationHandler will use RSocketRequester to implement the invoke() function call. Details please refer to the details of the application in the code RSocketRemoteServiceBuilder. Java file, and the client – app has also been included in the module to understand bean implementation based on the interface call.
2. Single parameter problem of service interface function
When using RSocketRequester to call the remote interface, the corresponding handler can only accept a single parameter. This is similar to gRPC’s design, but also takes into account the support of different object serialization frameworks. However, considering the actual use experience, multi-parameter functions may be involved, so that the caller can have a better development experience, then how to deal with this situation? We can add some more user-friendly default functions without affecting the service communication interface. For example:
public interface ExchangeCalculatorService { double exchange(ExchangeRequest request); default double rmbToDollar(double amount) { return exchange(new ExchangeRequest(amount, "CNY", "USD")); }}
Through the default method of the interface, we can provide convenient functions for the caller, such as byte array (byte[]) transmitted over the network. However, in the default function, we can add File object support for convenient use by the caller. The function API in Interface is responsible for service communication protocol, and the default function is used to improve user experience. The combination of these two functions can easily solve the problem of function multiple parameters. Of course, the default function can also be used as the outpost of data verification to some extent.
3. RSocket Broker support
RSocket has a Broker architecture, in which the service provider is hidden behind the Broker. The request is received by the Broker and then forwarded to the service provider.
Can service-discovery based mechanism load balancing be mixed with the RSocket Broker model? For example, long tail or complex network applications can be registered with an RSocket Broker that processes requests and forwards them. This is actually not complicated, we talked about the application and service interface naming conventions, here we just need to add an application name prefix. Suppose we have an RSocker Broker cluster, let’s call it broker0, and of course instances of that Broker cluster are registered with a service registry such as Consul. So the call to RSocket Broker service, the service name is adjusted for broker0: com. Example. Calculator. MathCalculatorService, added appName before service name: With this prefix, which is actually another canonical form of URI, we can extract the name of the application before the colon, and then go to the service registry to get a list of instances of the application.
Going back to the Broker interworking scenario, we would query the service registry for a list of broker0 services and then create a connection with the list of broker0 cluster instances so that subsequent service calls based on that interface would be sent to the Broker for processing. This is a pattern that completes the mix of service registry discovery and Broker pattern.
With the aid of this orientation specified service interfaces and application, the correlation between convenience we also do some beta testing, if you want the com. Example. Calculator. MathCalculatorService call diversion to beta applications, You can use the com – example – calculator – walk: com. Example. The calculator. MathCalculatorService invoking a service this way, In this way, the traffic corresponding to the service invocation will be forwarded to the corresponding instance of com-Example – Calculator-beta1, as a beta test.
Going back to the previous specification, if you really can’t bind the application name to the service interface, you can implement the service invocation this way, Such as calculator – server: com. Example. Calculator. Math. MathCalculatorService, you just need more complete documentation, this way, of course, also can solve system before access to the current architecture, Application migration costs are also low. If your previous service-oriented architecture is also based on interface communication, you can migrate to RSocket this way with minimal client code adjustments.
conclusion
By integrating service registry discovery with an actual naming convention, we have achieved an elegant coordination between service registry discovery and RSocket routing, including load balancing. Compared to other RPC solutions, you don’t need to import RPC’s own service registry, just reuse Spring Cloud’s service registry such as Alibaba Nacos, Consul, Eureka, and ZooKeeper, with no extra overhead and maintenance costs. For more details on RSocket RPC, see the Official Spring blog Easy RPC with RSocket.
For more detailed code details, click the link to see the corresponding code base!