摘要: 本文主要说明RPC的原理,以及经过Hadoop来举例在实践中如何实现RPC,本文主要经过摘取网上Blog(参见Reference)来整理RPC原理。java
在学校期间你们都写过很多程序,好比写个hello world服务类,而后本地调用下,以下所示。这些程序的特色是服务消费方和服务提供方是本地调用关系。typescript
public class Test { public static void main(String[] args) { HelloWorldService helloWorldService = new HelloWorldServiceImpl(); helloWorldService.sayHello("test"); } }
而一旦踏入公司尤为是大型互联网公司就会发现,公司的系统都由成千上万大大小小的服务组成,各服务部署在不一样的机器上,由不一样的团队负责。网络
这时就会遇到两个问题:数据结构
因为各服务部署在不一样机器,服务间的调用免不了网络通讯过程,服务消费方每调用一个服务都要写一坨网络通讯相关的代码,不只复杂并且极易出错。并发
若是有一种方式能让咱们像调用本地服务同样调用远程服务,而让调用者对网络通讯这些细节透明,那么将大大提升生产力,好比服务消费方在执行helloWorldService.sayHello("test")时,实质上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call Protocol),在各大互联网公司中被普遍使用,如阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle(开源)等。负载均衡
要让网络通讯细节对使用者透明,咱们须要对通讯细节进行封装,咱们先看下一个RPC调用的流程涉及到哪些通讯细节:框架
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。异步
怎么封装通讯细节才能让用户像以本地调用方式调用远程服务呢?对java来讲就是使用代理!java代理有两种方式:socket
尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时仍是选择动态代理方式。分布式
下面简单介绍下动态代理怎么实现咱们的需求。咱们须要实现RPCProxyClient代理类,代理类的invoke方法中封装了与远端服务通讯的细节,消费方首先从RPCProxyClient得到服务提供方的接口,当执行helloWorldService.sayHello("test")方法时就会调用invoke方法。
public class RPCProxyClient implements java.lang.reflect.InvocationHandler{ private Object obj; public RPCProxyClient(Object obj){ this.obj=obj; } /** * 获得被代理对象; */ public static Object getProxy(Object obj){ return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new RPCProxyClient(obj)); } /** * 调用此方法执行 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //结果参数; Object result = new Object(); // ...执行通讯相关逻辑 // ... return result; } }
public class Test { public static void main(String[] args) { HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class); helloWorldService.sayHello("test"); } }
上节讲了invoke里须要封装通讯细节(通讯细节再后面几章详细探讨),而通讯的第一步就是要肯定客户端和服务端相互通讯的消息结构。客户端的请求消息结构通常须要包括如下内容:
1)接口名称
在咱们的例子里接口名是“HelloWorldService”,若是不传,服务端就不知道调用哪一个接口了;
2)方法名
一个接口内可能有不少方法,若是不传方法名服务端也就不知道调用哪一个方法;
3)参数类型&参数值
参数类型有不少,好比有bool、int、long、double、string、map、list,甚至如struct(class);以及相应的参数值;
4)超时时间
5)requestID,标识惟一请求id,在下面一节会详细描述requestID的用处。
同理服务端返回的消息结构通常包括如下内容。
1)返回值
2)状态code
3)requestID
一旦肯定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。
什么是序列化?序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。
什么是反序列化?将在序列化过程当中所生成的二进制串转换成数据结构或者对象的过程。
为何须要序列化?转换为二进制串后才好进行网络传输嘛!
为何须要反序列化?将二进制转换为对象才好进行后续处理!
现现在序列化的方案愈来愈多,每种序列化方案都有优势和缺点,它们在设计之初有本身独特的应用场景,那到底选择哪一种呢?从RPC的角度上看,主要看三点:
目前互联网公司普遍使用Protobuf、Thrift、Avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。
消息数据结构被序列化为二进制串后,下一步就要进行网络通讯了。目前有两种经常使用IO通讯模型:1)BIO;2)NIO。通常RPC框架须要支持这两种IO模型。
如何实现RPC的IO通讯框架呢?
若是使用netty的话,通常会用channel.writeAndFlush()方法来发送消息二进制串,这个方法调用后对于整个远程调用(从发出请求到接收到结果)来讲是一个异步的,即对于当前线程来讲,将请求发送出来后,线程就能够日后执行了,至于服务端的结果,是服务端处理完成后,再以消息的形式发送给客户端的。因而这里出现如下两个问题:
以下图所示,线程A和线程B同时向client socket发送请求requestA和requestB,socket前后将requestB和requestA发送至server,而server可能将responseA先返回,尽管requestA请求到达时间更晚。咱们须要一种机制保证responseA丢给ThreadA,responseB丢给ThreadB。
怎么解决呢?
public Object get() { synchronized (this) { // 旋锁 while (!isDone) { // 是否有结果了 wait(); //没结果是释放锁,让当前线程处于等待状态 } } }
private void setDone(Response res) { this.res = res; isDone = true; synchronized (this) { //获取锁,由于前面wait()已经释放了callback的锁了 notifyAll(); // 唤醒处于等待的线程 } }
如何让别人使用咱们的服务呢?有同窗说很简单嘛,告诉使用者服务的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选举),能够确保服务注册表的高可用性。