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 the network communication details transparent to users, we naturally need to encapsulate the communication details. Let's first look at the process of the next RPC call:

  • 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.

为什么需要序列化?转换为二进制串后才好进行网络传输嘛!为什么需要反序列化?将二进制转换为对象才好进行后续处理!

现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看 三点:1)通用性,比如是否能支持Map等复杂的数据结构;2)性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列 化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;3)可扩展性,对互联网公司而言,业务变化 快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,删除老的字段,而不影响老的服务,这将大大提供系统的健壮性。

目前国内各大互联网公司广泛使用hessian、protobuf、thrift、avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。

1.3  通信

消息数据结构被序列化为二进制串后,下一步就要进行网络通信了。目前有两种IO通信模型:1)BIO;2)NIO。一般RPC框架需要支持这两种IO模型,原理可参考:《一个故事讲清楚 NIO》

如何实现RPC的IO通信框架?1)使用java nio方式自研,这种方式较为复杂,而且很有可能出现隐藏bug,见过一些互联网公司使用这种方式;2)基于mina,mina在早几年比较火热,不过这 些年版本更新缓慢;3)基于netty,现在很多RPC框架都直接基于netty这一IO通信框架,比如阿里巴巴的HSF、dubbo,Twitter的 finagle等。

1.4  消息里为什么要带有requestID?

如果使用netty的话,一般会用channel.writeAndFlush()方法来发送消息二进制串,这个方法调用后对于整个远程调用(从发 出请求到接收到结果)来说是一个异步的,即对于当前线程来说,将请求发送出来后,线程就可以往后执行了,至于服务端的结果,是服务端处理完成后,再以消息 的形式发送给客户端的。于是这里出现以下两个问题:

1)怎么让当前线程“暂停”,等结果回来后,再向后执行?

2)如果有多个线程同时进行远程方法调用,这时建立在client server之间的socket连接上会有很多双方发送的消息传递,前后顺序也可能是随机的,server处理完结果后,将结果消息发送给 client,client收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的?

如下图所示,线程A和线程B同时向client socket发送请求requestA和requestB,socket先后将requestB和requestA发送至server,而server可 能将responseA先返回,尽管requestA请求到达时间更晚。我们需要一种机制保证responseA丢给ThreadA,responseB 丢给ThreadB。

怎么解决呢?

1)client线程每次通过socket调用一次远程接口前,生成一个唯一的ID,即requestID(requestID必需保证在一个Socket连接里面是唯一的),一般常常使用AtomicLong从0开始累计数字生成唯一ID;

2)将处理结果的回调对象callback,存放到全局ConcurrentHashMap里面put(requestID, callback);

3)当线程调用channel.writeAndFlush()发送消息后,紧接着执行callback的get()方法试图获取远程返回的结果。 在get()内部,则使用synchronized获取回调对象callback的锁,再先检测是否已经获取到结果,如果没有,然后调用callback 的wait()方法,释放callback上的锁,让当前线程处于等待状态。

4)服务端接收到请求并处理后,将response结果(此结果中包含了前面的requestID)发送给客户端,客户端socket连接上专门监 听消息的线程收到消息,分析结果,取到requestID,再从前面的ConcurrentHashMap里面get(requestID),从而找到 callback对象,再用synchronized获取callback上的锁,将方法调用结果设置到callback对象里,再调用 callback.notifyAll()唤醒前面处于等待状态的线程。

2 如何发布自己的服务?

如何让别人使用我们的服务呢?有同学说很简单嘛,告诉使用者服务的IP以及端口就可以了啊。确实是这样,这里问题的关键在于是自动告知还是人肉告知。

人肉告知的方式:如果你发现你的服务一台机器不够,要再添加一台,这个时候就要告诉调用者我现在有两个ip了,你们要轮询调用来实现负载均衡;调用 者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip。现实生产环境当然不会使用人肉方式。

有没有一种方法能实现自动告知,即机器的增添、剔除对调用方透明,调用者不再需要写死服务提供方地址?当然可以,现如今zookeeper被广泛用于实现服务自动注册与发现功能!

简单来讲,zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。如下图所示:

具体来说,zookeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径 上: /{service}/{version}/{ip:port}, 比如我们的HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为/HelloWorldService /1.0.0/100.19.20.01:16888  /HelloWorldService/1.0.0/100.19.20.02:16888。

zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么 zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),zookeeper都会通知服务消费方服务提供者地址列表已经发生改变,从而进行更新。

更为重要的是zookeeper 与生俱来的容错容灾能力(比如leader选举),可以确保服务注册表的高可用性。

3 小结

RPC几乎是每一个从学校进入互联网公司的同学都要首先学习的框架,之前面试过一个在大型互联网公司工作过两年的同学,对RPC还是停留在使用层面,这是不应该的。本文也仅是对RPC的一个比较粗糙的描述,希望对大家有所帮助,错误之处也请指出修正。

4 一些开源的RPC框架

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=324950053&siteId=291194637