Getting Started with RPC

During school, everyone wrote a lot of programs, such as writing a hello world service class, and then calling it locally, as shown below. The characteristic of these programs is that the service consumer and the service provider are in a local calling relationship.

Once you step into a company, especially a large Internet company, you will find that the company's system consists of thousands of large and small services, each of which is deployed on different machines and is in charge of different teams. At this time, two problems will be encountered: 1) To build a new service, it is inevitable to rely on other people's services, and now other people's services are on the remote side, how to call? 2) If other teams want to use our service, how should our service be published so that others can call it? We will discuss these two issues below.

1 How to call someone else's remote service?

Since each service is deployed on different machines, the invocation between services cannot avoid the network communication process. Service consumers must write a piece of code related to network communication every time a service is invoked, which is not only complex but also prone to errors.

If there is a way for us to call a remote service like a local service, but make the caller transparent to the details of network communication, it will greatly improve productivity. For example, when the service consumer executes helloWorldService.sayHello("test"), the essence The above call is the remote service. This method is actually RPC (Remote Procedure Call Protocol), which is widely used in major Internet companies, such as Alibaba's hsf, dubbo (open source), Facebook's thrift (open source), Google grpc (open source), Twitter's finagle Wait.

To make network communication details transparent to users, we naturally need to encapsulate the communication details. Let's look at the next RPC call flow:

  • 1) The service consumer (client) calls the service in a local call mode;
  • 2) After the client stub receives the call, it is responsible for assembling methods, parameters, etc. into a message body that can be transmitted over the network;
  • 3) The client stub finds the service address and sends the message to the server;
  • 4) The server stub decodes the message after receiving it;
  • 5) The server stub calls the local service according to the decoding result;
  • 6) The local service executes and returns the result to the server stub;
  • 7) The server stub packages the returned result into a message and sends it to the consumer;
  • 8) The client stub receives the message and decodes it;
  • 9) The service consumer gets the final result.

The goal of RPC is to encapsulate these steps 2 to 8, so that users can be transparent about these details.

1.1 How to achieve transparent remote service invocation?

How to encapsulate the communication details so that the user can call the remote service like a local call? For java is to use a proxy! There are two ways for java proxy: 1) jdk dynamic proxy; 2) bytecode generation. Although the proxy implemented by the bytecode generation method is more powerful and efficient, the code is not easy to maintain. Most companies still choose the dynamic proxy method when implementing the RPC framework.

The following is a brief introduction to how the dynamic proxy achieves our needs. We need to implement the RPCProxyClient proxy class. The invoke method of the proxy class encapsulates the details of the communication with the remote service. The consumer first obtains the service provider's interface from RPCProxyClient, and it will be called when the helloWorldService.sayHello("test") method is executed. invoke method.

1.2 How to encode and decode messages?

1.2.1 Determine the message data structure

The previous section talked about the need to encapsulate the communication details in invoke, and the first step of communication is to determine the message structure of the communication between the client and the server. The client's request message structure generally needs to include the following:

1) Interface name

In our example, the interface name is "HelloWorldService". If it is not passed, the server will not know which interface to call;

2) Method name

There may be many methods in an interface. If the method name is not passed, the server will not know which method to call;

3) Parameter type & parameter value

There are many parameter types, such as bool, int, long, double, string, map, list, and even struct (class);

and the corresponding parameter values;

4) Timeout time

5) requestID, which identifies the unique request id. The use of requestID will be described in detail in the following section.

Similarly, the message structure returned by the server generally includes the following contents.

1) return value

2) Status code

3)requestID

1.2.2 Serialization

Once the data structure of the message is determined, the next step is to consider serialization and deserialization.

What is serialization? Serialization is the process of converting a data structure or object into a binary string, that is, the process of encoding.

What is deserialization? The process of converting binary strings generated during serialization into data structures or objects.

Why do you need serialization? After converting it into a binary string, it is good for network transmission! Why do you need deserialization? Convert the binary to an object for subsequent processing!

Nowadays, there are more and more serialization schemes. Each serialization scheme has advantages and disadvantages. They have their own unique application scenarios at the beginning of the design. Which one to choose? From the perspective of RPC, we mainly look at three points: 1) versatility, such as whether it can support complex data structures such as Map; 2) performance, including time complexity and space complexity, because the RPC framework will be used by almost all companies in the company. Service usage, if serialization can save a little time, the benefits for the entire company will be very considerable. Similarly, if serialization can save a little memory, network bandwidth can also be saved a lot; 3) Scalability, for the Internet For companies, business changes quickly. If the serialization protocol has good scalability, it supports the automatic addition of new business fields and the deletion of old fields without affecting the old services, which will greatly improve the robustness of the system.

At present, major domestic Internet companies widely use mature serialization solutions such as hessian, protobuf, thrift, and avro to build RPC frameworks. These are well-tested solutions.

1.3 Communication

After the message data structure is serialized into a binary string, the next step is network communication. There are currently two IO communication models: 1) BIO; 2) NIO. The general RPC framework needs to support these two IO models. For the principle, please refer to: "A Story Tells NIO Clearly" .

How to implement the IO communication framework of RPC? 1) Self-research using java nio method, which is more complicated and likely to have hidden bugs. I have seen some Internet companies use this method; 2) Based on mina, mina was popular in the early years, but the version in recent years The update is slow; 3) Based on netty, many RPC frameworks are now directly based on the IO communication framework of netty, such as Alibaba's HSF, dubbo, Twitter's finagle, etc.

1.4 Why should requestID be included in the message?

If netty is used, the channel.writeAndFlush() method is generally used to send the message binary string. After this method is called, it is asynchronous for the entire remote call (from sending the request to receiving the result), that is, for the current thread. , after the request is sent, the thread can be executed later. As for the server-side result, it is sent to the client in the form of a message after the server-side processing is completed. So here are two problems:

1) How to make the current thread "pause", and then execute backwards after the result comes back?

2) If there are multiple threads making remote method calls at the same time, there will be a lot of messages sent by both parties on the socket connection established between the client and server, and the sequence may be random. After the server processes the results, it will The message is sent to the client, and the client receives a lot of messages. How to know which message result was called by which thread?

As shown in the figure below, thread A and thread B send requests requestA and requestB to the client socket at the same time, the socket sends requestB and requestA to the server successively, and the server may return responseA first, although the requestA request arrives later. We need a mechanism to ensure that responseA is thrown to ThreadA, and responseB is thrown to ThreadB.

How to solve it?

1) Each time the client thread calls the remote interface through the socket, it generates a unique ID, namely requestID (requestID must be guaranteed to be unique in a Socket connection). Generally, AtomicLong is often used to accumulate numbers from 0 to generate a unique ID;

2) Store the callback object callback of the processing result in the global ConcurrentHashMap put(requestID, callback);

3) When the thread calls channel.writeAndFlush() to send a message, it then executes the get() method of the callback to try to obtain the result returned by the remote. Inside get(), use synchronized to obtain the lock of the callback object callback, and then first check whether the result has been obtained, if not, then call the wait() method of the callback to release the lock on the callback, so that the current thread is in a waiting state.

4) After the server receives the request and processes it, it sends the response result (this result contains the previous requestID) to the client. The thread on the client's socket connection that monitors the message receives the message, analyzes the result, and gets the requestID. Then get(requestID) from the previous ConcurrentHashMap to find the callback object, then use synchronized to obtain the lock on the callback, set the result of the method call to the callback object, and then call callback.notifyAll() to wake up the thread in the waiting state.

2 How to publish your own service?

How to let others use our service? Some students said that it is very simple, just tell the user the IP and port of the service. This is indeed the case. The crux of the issue here is whether it is automatic notification or human flesh notification.

The way of human flesh notification: If you find that one machine for your service is not enough, you need to add another one. At this time, you need to tell the caller that I now have two IPs, and you need to poll the call to achieve load balancing; the caller bites He gritted his teeth and changed it. As a result, one day a machine hung up. The caller found that half of the services were unavailable. He could only manually modify the code to delete the ip of the machine that hung up. Of course, the real production environment will not use human flesh.

Is there a way to achieve automatic notification, that is, the addition and removal of machines are transparent to the caller, and the caller no longer needs to hard-write the service provider address? Of course, zookeeper is now widely used to implement automatic service registration and discovery functions!

In simple terms, zookeeper can act as a 服务注册表(Service Registry), allowing multiple to 服务提供者form a cluster, so that 服务消费者specific service access addresses (ip + ports) can be obtained through the service registry to access specific service providers. As shown below:

Specifically, zookeeper is a distributed file system. Whenever a service provider is deployed, it must register its own service on a certain path of zookeeper: /{service}/{version}/{ip:port}, For example, if our HelloWorldService is deployed to two machines, two directories will be created on zookeeper: /HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888.

Zookeeper provides a "heartbeat detection" function, which will periodically send a request to each service provider (in fact, a long socket connection is established), if there is no response for a long time, the service center considers that the service provider has "hanged up" , and remove it. For example, if the machine 100.19.20.02 is down, the path on zookeeper will only be /HelloWorldService/1.0.0/100.19.20.01:16888.

The service consumer will listen to the corresponding path (/HelloWorldService/1.0.0). Once the data on the path changes (increase or decrease), zookeeper will notify the service consumer that the list of service provider addresses has changed, so as to update it.

More importantly, zookeeper's inherent fault tolerance and disaster tolerance (such as leader election) can ensure the high availability of the service registry.

3 Summary

RPC is a framework that almost every student who enters an Internet company from school must learn first. I interviewed a student who had worked in a large Internet company for two years before, but RPC is still at the use level, which is not right. This article is only a rough description of RPC. I hope it will be helpful to everyone. Please point out and correct the mistakes.

4 Some open source RPC frameworks

https://github.com/alibaba/dubbo

http://thrift.apache.org/?cm_mc_uid=87762817217214314008006&cm_mc_sid_50200000=1444181090

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324950010&siteId=291194637