Development of Call Protocols in Microservices

Microservices are now popular, and the way of calling between different services has also undergone a series of developments. This time we will take a look at their development process together. Looking together is not only conducive to unified understanding, but also helps us see the reasons for the change. reason.

Let’s take the simplest scenario, Client calls an addition function, adds up two integers and returns their sum. If it is called locally, it is simple and can’t be simpler. As long as you have learned a programming language a little, you will be done.

But once it becomes a remote call, the threshold will go up all of a sudden. First of all, you have to know Socket programming, at least learn our network protocol course first, and then read N brick-thick Socket programming books and learn several Socket programming models we have learned. This makes a job that could have been done after graduating from college become a job that five years of work experience may not necessarily do well, and getting the Socket programming done is the first step in the long march. There are still many problems behind!

Five problems that need to be solved in remote invocation

Question 1: How to specify the syntax of remote calls?

How does the client tell the server level that I am an addition and the other is a multiplication. Do I pass you the string “add”, or do I pass you an integer, such as 1 for addition and 2 for multiplication? How should the server level tell the client that my addition can only add integers at present, not decimals or strings; and another addition “add1”, which can achieve mixed addition of decimals and integers. What is the return value? What is returned when it is correct and what is returned when it is wrong?

Question 2: What if parameters are passed?

Do I pass two integers first, followed by an operator “add”, or pass the operator first, and then pass two integers? Is it the same as in our data structure, if it is all UDP, if you want to implement an inverse Polish expression, it is okay to put it in a message network packet. If it is TCP, it is a stream. In this stream, how to divide the two calls? When is the head and when is the tail? Mix the parameters this time with the last time. The data sent by one end of TCP may not be read at once by the other end. So, how do you count it as reading it?

Question 3: How to represent data?

In this simple example, what is passed is a fixed-length int value. This situation is okay. What should I do if it is a variable-length type, a struct, or even a class? If it is an int, the length is different on different platforms, what should I do?

Question 4: How to know which remote calls have been implemented at a server level?

From which port can this remote call be accessed? Assuming that the server level implements multiple remote calls, each may be implemented in a different process, and the listening port is different, and since the server level is implemented by itself, it is impossible to use a port that is recognized by everyone, and it is possible that multiple processes are deployed On one machine, you need to preempt the port. In order to prevent conflicts, random ports are often used. How can the Client find these listening ports?

Question 5: What should I do if an error, retransmission, Packet loss, performance and other issues occur?

Local calls do not have this problem, but once on the network, these problems need to be dealt with, because the network is unreliable. Although in the same connection, we can also guarantee the problem of Packet loss and retransmission through the TCP protocol, but if the server If it crashes and restarts, the current connection is disconnected, TCP cannot guarantee it. You need to re-invoke it by yourself. Will the retransmission do the same operation twice, and will the performance of remote calls be affected?

RPC protocol

Issue of agreement

A big bull Bruce Jay Nelson wrote a paper Implementing Remote Procedure Calls, which defined the calling standard of RPC. All subsequent RPC frameworks follow this standard pattern.

When the client application wants to initiate a remote call, it actually calls the local caller’s Stub locally. It is responsible for encoding the called interface, methods, and parameters through the agreed protocol specification, transmitting them through the local RPC Runtime, and sending the calling network packet to the server.

After receiving the request, the RPCRuntime on the server side is handed over to the provider Stub for decoding, then the method on the server side is called, and the server level executes the method to return the result. After the provider Stub returns the result encoding, it is sent to the Client, and the RPCRuntime on the client side receives it. The result is sent to the caller Stub to decode and return to the Client.

There are three levels. For the user layer and the server level, it is like a local call, focusing on the processing of business logic. For the Stub layer, it handles the agreed syntax, semantics, encapsulation, and decapsulation. For RPCRuntime, it mainly handles high-performance transmissions, as well as network errors and exceptions.

During RPC calls, all data types must be encapsulated in a similar format. And RPC calls and result returns also have strict formats.

  • XID uniquely identifies a pair of request and reply. Request is 0, reply is 1.

  • RPC has a version number, both ends to match the version number of the RPC protocol. If not, it will return Deny, the reason is RPC_MISMATCH.

  • The program has a number. If the server level cannot find the program, it will return PROG_UNAVAIL.

  • The program has a version number. If the version number of the program does not match, PROG_MISMATCH will be returned.

  • A program can have multiple methods, and the methods are also numbered. If the method is not found, it will return PROC_UNAVAIL.

  • Call requires authentication, if not passed, Deny.

  • Finally, the parameter list, if the parameter cannot be parsed, the GABAGE_ARGS is returned.

In the Client, the clnt_create is called to create a connection, and then the add_1 is called, which is a Stub function, which feels like calling locally. In fact, this function initiates an RPC call, which calls the ONC RPC library by calling the clnt_call to actually send the request. The process of calling is very complicated, I will talk about this in detail in a moment.

Of course, the server level also has a Stub program that listens to client requests. When the call arrives, it is judged that if it is add, then the real server level logic is called, that is, the two numbers are added up.

The server level returns the result to the Stub of the server. The Stub program sends the result to the Client. The Stub program of the client is waiting for the result. When the result reaches the Client Stub, the result is returned to the client application program to complete the entire calling process.

With this RPC framework, the first three of the first five questions “How to specify the syntax of remote calls?” “How to pass parameters?” and “How to represent data?” are basically solved. These three problems are collectively referred to as ** protocol convention problems **.

Transmission problem

However, errors, retransmissions, packet loss, performance and other problems have not been solved. We collectively refer to these problems as transmission problems. This does not need to worry about Stub, but is implemented by the ONC RPC class library. This is achieved by the big cows, we just need to call it.

Service Discovery Issues

The transport problem is solved, and we have another problem left, which is problem 4 “How to find the random port of the RPC server”. We call this problem the Service Discovery problem. In ONC RPC, Service Discovery is implemented through portmapper.

The portmapper will start on a well-known port. Since the RPC program is written by the user, it will listen on a random port, but when the RPC program starts, it will register with the portmapper. When the Client wants to access the RPC server level program, first query the portmapper, obtain the random port of the RPC server level program, then establish a connection to this random port, and start the RPC call. As can be seen from the figure, the RPC call of the mount command is implemented in this way.