origin
Recently in the company to share the handjerk RPC, so make a summary.
Concept paper
What is RPC?
RPC is called Remote Procedure Call and is used to resolve calls between services in a distributed system. Colloquially, this means that a developer can call a remote service just as if it were a local method. Therefore, the role of RPC is mainly reflected in the following two aspects:
- Mask the difference between a remote call and a local call to make it feel like a call to a method within the project;
- Hiding the complexity of the underlying network communication allows us to focus more on the business logic.
Basic architecture of RPC framework
Let’s talk about the basic architecture of the RPC framework through a diagramThe RPC framework consists of the three most important components, which are the client, the server, and the registry. In an RPC call flow, these three components interact like this:
-
After the server is started, it will publish the list of services it provides to the registry, and the client subscribes to the service address to the registry.
-
The client will call the server through the local Proxy module Proxy, and the Proxy module is responsible for converting methods, parameters and other data into network byte stream.
-
The client selects one of the service addresses from the service list and sends the data to the server through the network.
-
After receiving the data, the server decodes and obtains the request information.
-
The server invokes the corresponding service based on the decoded request information, and then returns the call result to the client.
The RPC framework communicates the process and the roles involved
From the above figure, you can see that RPC frameworks typically have these components: service governance (registry discovery), load balancing, fault tolerance, serialization/deserialization, codec, network transport, thread pool, dynamic proxy, and of course some RPC frameworks also have connection pool, logging, security, and other roles.
Specific invocation procedure
-
The service consumer (client) invokes the service as a local invocation
-
After receiving the call, the client stub encapsulates methods and parameters into a message body that can be transmitted over the network
-
The Client Stub encodes the message and sends it to the server
-
The Server Stub decodes the message after receiving it
-
The Server Stub invokes the local service based on the decoded result
-
The local service executes and returns the result to the Server Stub
-
The Server Stub returns the import result, which is encoded and sent to the consumer
-
The Client Stub receives the message and decodes it
-
The service consumer (client) gets the result
RPC Message Protocol
During an RPC call, parameters need to be marshalled into messages to be sent, and the receiver needs to ungroup messages into parameters. The processing results also need to be marshalled or unmarshalled. What parts a message consists of and how the message is represented constitute a message protocol. The message protocol used in RPC invocation is called RPC message protocol.
Practical article
From the above concepts, we know about the components of an RPC framework, so we need to consider these components when designing an RPC framework. From the definition of RPC, we can see that the RPC framework needs to mask the low-level details so that users can feel that calling a remote service is as easy as calling a local method, so we need to consider these issues:
- How can users use our RPC framework with as little configuration as possible
- How do you register a service with ZK(where the registry selects ZK) and leave the user unaware
- How do I invoke a transparent (as far as possible user-unaware) invocation service provider
- How to enable dynamic load balancing with multiple service providers
- How the framework enables users to customize extension components (such as extending custom load balancing policies)
- How to define the message protocol, and codec
- . , etc.
These issues were addressed in designing the RPC framework.
Technology selection
-
Registries Currently mature registries include Zookeeper, Nacos, Consul, Eureka, where ZK is used as the registry and does not provide the function of switching and user-defined registries.
-
IO communication framework This implementation uses Netty as the underlying communication framework because Netty is a high performance event driven non-blocking IO(NIO) framework that provides no other implementation and does not support user-defined communication framework
-
Message Protocol This implementation uses a custom message protocol, which is described later
Overall Project structure
From this structure, we can know that the modules beginning with the name RPC are the modules of RPC framework, which is also the content of the RPC framework of this project, while consumer is the service consumer, provider is the service provider, and provider-API is the exposed service API.
Overall dependence
Project Implementation Introduction
To ensure that the user uses our RPC framework with as little configuration as possible, we designed the RPC framework as a starter, and the user just relies on the starter, and that’s basically it.
Why design two (client-starter/server-starter)?
This is to better reflect the concept of client and server, consumers depend on the client, service providers depend on the server, and minimize dependencies.
Why design a starter?
Using the Spring Boot auto-assembly mechanism, we load the spring.factories file in the starter, configure the following code, and then our starter configuration class takes effect. We configure some beans in the configuration class.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration
Copy the code
Publish services and consume services
-
For publishing services
The service provider needs to add @rpcService annotation to the exposed service. This custom annotation is based on @service. It is a composite annotation that has the functionality of @service annotation. Will be registered against these two metadata- Publishing Service Principles:
After the service provider is started, the configuration class of the server starter takes effect according to the Spring Boot auassembly mechanism, and the bean is annotated @rpcService in the backend handler of a bean (RpcServerProvider). Register the annotation metadata with ZK.
- Publishing Service Principles:
-
For consumer services
The consumer service requires a custom @rpcAutoWired annotation identifier, which is a composite annotation based on @AutoWired.-
Principle of Consumer Service
To make the invocation of the service provider unobserved by the client, you need to use a dynamic proxy. As shown above, HelloWordService has no implementation class. You need to assign a proxy class to it and invoke the request in the proxy class. Based on Spring Boot auto-assembly, the service consumer is started, and the RpcClientProcessor, the bean post-processor, starts to work. It basically iterates through all the beans and determines whether the properties in each bean are modified by the @rpcAutoWired annotation. Some will dynamically assign this property to a proxy class, which will call the invoke method of the proxy class.The invoke method of the proxy class obtains server-side metadata through service discovery, encapsulates the request, and initiates the invocation through Netty.
-
The registry
This project registry uses ZK because registries are used by both service consumers and service providers. So put ZK in the RPC-core module.The rPC-core module is shown in the figure above. The core functions are in this module. Services are registered under the Register package.
Service registration interface, implemented using ZK.
Load balancing Policy
Load balancing is defined in RPC-core. Currently, FullRoundBalance and RandomBalance are supported. By default, the random policy is used. Specified by rpc-client-spring-boot-starter.
Multiple instances are found through ZK service discovery, and one instance is obtained through the load balancing policy
Can be configured in the consumerrpc.client.balance=fullRoundBalance
Instead, you can also customize the load balancing policy by implementing the LoadBalance interface and adding the created classes to the IOC container. Since we configured @ConditionalonMissingBean, the user-defined bean will be loaded first.
Custom message protocol, codec
The so-called protocol is that the communication parties agree on the rules in advance, and the server knows how to parse the data sent.
-
Custom message protocol
-
Magic number: Magic number is a code negotiated by two communication parties, usually in a fixed number of bytes. The purpose of the magic number is to prevent anyone from sending data to the server port. For example, Java Class files store the magic number 0xCAFEBABE at the beginning of the file. When loading the Class file, it first verifies that the magic number is correct
-
Protocol version: The structure or fields of the protocol may be changed as service requirements change. The resolution method varies according to protocol versions.
-
Serialization algorithm: The serialization algorithm field indicates how the data sender should convert the requested object into binary and how to convert the binary into an object, such as JSON, Hessian, Java native serialization, etc.
-
Packet type: Different types of packets may exist in different service scenarios. The RPC framework contains request, response, heartbeat, and other types of packets.
-
Status: The status field identifies whether the request is normal (SUCCESS, FAIL).
-
Message ID: A unique request ID that can be used to correlate responses and also for link tracing.
-
Data length: Indicates the length of the data to determine whether it is a complete data packet
-
Data content: Request body content
-
-
Codec Codec is implemented in the RPC-core module, under the package com.rrTv.rpc.core.codec.
Custom encoder by inheriting from Netty
MessageToByteEncoder<MessageProtocol<T>>
Class implements message encoding.Custom decoder implements message decoding by inheriting Netty’s ByteToMessageDecoder class.
Pay attention to TCP packet sticking and unpacking during decoding
What is TCP sticking and unpacking
The TCP transport protocol is flow oriented and has no packet boundaries, that is, no message boundaries. When sending data to a server, a client may split a complete packet into multiple tabloids or combine multiple packets into a single packet for sending. Hence unpacking and gluing.
During network communication, the size of packets that can be sent each time is limited by many factors, such as MTU transmission unit size and sliding window. Therefore, if the size of the network packet data transmitted at one time exceeds the size of the transmission unit, then our data may be split into multiple packets to be sent. If each request for network packet data is small, say 10,000 requests, TCP will not send each 10,000 requests. This is optimized by the Nagle (batch send, used to solve network congestion caused by sending small packets frequently) algorithm used by TCP.
Therefore, the network transmission will appear like this:
- The server happens to read two complete packets A and B, and there is no unpacking/sticking problem.
- The server receives the data packet stuck together by A and B, and needs to parse out A and B.
- The server receives complete A and part of B data packet B-1, and the server needs to parse complete A and wait to read complete B data packet.
- The server receives part of the data packet A-1 from A. In this case, it needs to wait for the complete data packet from A.
- Packet A is large, and the server needs to receive it multiple times.
How do I solve the TCP packet sticking and unpacking problem
The fundamental way to solve the problem is to find the boundaries of the message:
- Message Length Each data packet requires a fixed length. The receiver is considered to have obtained a complete message when it has read all the messages of a fixed length. When the length of the data sent by the sender is less than a fixed length, the space needs to be filled. The message fixed-length method is very simple to use, but its disadvantages are also very obvious. It is impossible to set the value of the fixed length. If the length is too large, it will waste bytes, and if the length is too small, it will affect the message transmission, so the message fixed-length method will not be used in general.
- Special delimiter A special delimiter is added to the end of each sent message so that the receiver can split the message according to the special delimiter. The delimiter must be selected so that it is not identical to the characters in the body of the message to avoid conflicts. Otherwise an incorrect message split may occur. It is recommended to encode the message, such as base64 encoding, and then select characters other than the 64 encoded characters as the specific delimiter
- Message Length + Message content Message length + message content is one of the most commonly used protocols in project development. The receiver reads the message content according to the message length.
This project is to use the “message length + message content” method to solve the TCP sticking and unpacking problems. So when you decode the data, you have to determine whether the data is long enough to read, if it’s not long enough, it’s not ready, so you go ahead and read the data and decode it, and this is the way to get a complete packet.
Serialization and deserialization
Serialization and deserialization in RPC – the core module com. RRTV. RPC. Core. The serialization package, provided the HessianSerialization and JsonSerialization serialization. By default, HessianSerialization is used. Users cannot customize.
Serialization performance:
- On the space
- On the time
Network transmission, using Netty
Netty code is fixed, it is important to note that the handler order should not be wrong, in the server side, the encoding is the outbound operation (can be placed after the inbound), decoding and receiving the response is the inbound operation, the decoding should be first.
Client RPC invocation mode
Mature RPC frameworks generally provide four invocation modes: Sync, asynchronous Future, Callback, and Oneway.
-
Sync Synchronous invocation. After the client thread initiates the RPC call, the current thread blocks until the server returns the result or handles the timeout exception.
-
Asynchronous Future calls
After the client makes the call, it will not block and wait. Instead, it will get the Future object returned by the RPC framework. The result of the call will be cached by the server. The process is blocked and waiting when the client actively obtains the result -
When the client initiates the call, the Callback object is passed to the RPC framework, and it is returned directly without waiting synchronously for the return result. When the server response result is obtained or the timeout exception occurs, the user registration Callback is performed
-
Oneway one-way invocation the client initiates a request and returns it directly, ignoring the returned result
The first is used here: the client synchronizes the invocation, the others are not implemented. In RpcFuture, use CountDownLatch to implement the blocking wait (timeout wait)
Overall architecture and processes
The process is divided into three parts: service provider start process, service consumer start process, and invocation process
- Service provider startup
- The service provider relies on the RPC-server-spring-boot-starter
- ProviderApplication start, according to the springboot automatic assembly mechanism, RpcServerAutoConfiguration automatic configuration to take effect
- The RpcServerProvider is a bean backend processor that publishes services and registers service metadata with ZK
- The rpcServerProvider.run method starts a Netty service
- Service Consumer startup
- The service consumer relies on an RPC-client-spring-boot-starter
- ConsumerApplication start, according to the springboot automatic assembly mechanism, RpcClientAutoConfiguration automatic configuration to take effect
- Add service discovery, load balancing, proxy, and other beans to the IOC container
- The post-processor RpcClientProcessor scans the bean and dynamically assigns the properties decorated by @rpcAutoWired to proxy objects
- Call the process
- Service consumers a request to http://localhost:9090/hello/world? name=hello
- Service consumers call helloWordService. SayHello () method, is the agent to perform ClientStubInvocationHandler. Invoke () method
- Service consumer obtains service metadata from ZK service discovery, error 404 not found
- Service consumer custom protocol that encapsulates request header and request body
- The service consumer encodes the message through a custom encoder, RpcEncoder
- The service consumer obtains the IP and port of the service provider through service discovery and initiates the invocation through the Netty network transport layer
- The service consumer enters the return result (timeout) wait through the RpcFuture
- The service provider receives a consumer request
- The service provider decodes the message through a custom RpcDecoder
- The service provider decodes the data and sends it to the RpcRequestHandler for processing, executing server-side local methods through reflection calls and getting the results
- The service provider encodes the message through an encoder RpcEncoder for the result of execution. (Since the request and response protocols are the same, the encoder and decoder can be used together.)
- The service consumer decodes the message through a custom RpcDecoder
- The service consumer writes messages to the request and response pools through the RpcResponseHandler and sets the RpcFuture’s response result
- The service consumer gets the results
The above process can be combined with code analysis, which will be presented later
Environment set up
- Operating system: Windows
- Integrated development tool: IntelliJ IDEA
- Project technology stack: SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.final
- Project dependency management tool: Maven 4.0.0
- Registry: Zookeeeper 3.7.0
Project test
- Start the Zookeeper server: bin/ zkserver. CMD
- Start the provider module ProviderApplication
- Start the Consumer module ConsumerApplication
- Test: browser enter http://localhost:9090/hello/world? Name =hello, return hello: hello, RPC call succeeded
Project Code Address
Gitee.com/listen_w/rp…