远程过程调用(Remote Procedure Call
,RPC
)是一个计算机通讯协议。该协议容许运行于一台计算机的程序调用另外一台计算机的子程序,而程序员无需额外地为这个交互做用编程。RPC
的主要目标是让构建分布式应用更加容易,在提供强大的远程调用能力的同时不损失本地调用的语义的简洁性。html
趁实习前的这段业余时间,我实现了一个轻量级的分布式RPC
框架,名字叫作 buddha,代码量不大,可是麻雀虽小却五脏俱全。本篇文章将一步步阐明buddha
的设计、框架组件的拆解以及须要考虑的因素。git
在网络中,全部的数据都将会被转化为字节进行传送,因此在代码层面上,一个RPC
框架须要实现特定格式的数据与字节数组之间的相互转化。像Java
已经提供了默认的序列化方式,可是若是是在高并发的场景下,使用Java
原生的序列化方式可能会遇到性能瓶颈。因而,出现了许多开源的、高效的序列化框架:如Kryo
、fastjson
和Protobuf
等。buddha
目前支持Kryo
和fastjson
两种序列化框架。程序员
因为TCP
只关心字节流,并不知晓上层的数据格式。若是客户端应用层一次要发送的数据过大时,TCP
会将该数据进行分解传送,所以在服务端须要进行粘包处理(由TCP
来保证数据的有序性);若是客户端一次要发送的数据量很小时,TCP
并不会立刻把数据发送出去,而是将其存储在缓冲区,当达到某个阈值的时候再发送出去,所以在服务端须要进行拆包的工做。github
经过以上分析,咱们了解了TCP
粘包或者拆包的缘由,解决这个问题的关键在于向数据包添加边界信息,经常使用的方法有以下三个。编程
发送端给每一个数据包添加包首部,首部中至少包含数据包的长度,这样在接收端接收到数据时,经过读取首部的长度信息获得该数据包有效数据的长度。json
发送端将每一个数据包封装为固定长度(多余用0
填充),这样接收端在接收到数据后根据约定好的固定长度读取每一个数据包的数据。数组
使用特殊符号将每一个数据包区分开来,接收端也是经过该特殊符号的划分数据包的边界。缓存
buddha
采用第一种方式来解决TCP
拆包、粘包的问题。网络
BIO
每每用于经典的每链接每线程模型,之因此使用多线程,是由于像accept()
、read()
和write()
等函数都是同步阻塞的,这意味着当应用为单线程且进行IO
操做时,若是线程阻塞那么该应用必然会进入挂死状态,可是实际上此时CPU
是处于空闲状态的。开启多线程,就可让CPU
去为更多的线程服务,提升CPU
的利用率。可是在活跃线程数较多的状况下,采用多线程模型回带来以下几个问题。多线程
线程的建立和销毁代价颇高,在Linux
操做系统中,线程本质上就是一个进程,建立和销毁线程属于重量级的操做。
在JVM
中,每一个线程会占用固定大小的栈空间,而JVM
的内存空间是有限的,所以若是线程数量过多那么线程自己就会占据过多的资源。
线程的切换成本较高,每次线程切换须要涉及上下文的保存、恢复以及用户态和内核态的切换。若是线程数过多,那么会有较大比例的CPU
时间花费在线程切换上。
使用线程池的方式解决前两个问题,可是线程切换带来的开销仍是存在。因此在高并发的场景下,传统的BIO
是无能为力的。而NIO
的重要特色是:读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,能够当即返回,这就容许咱们不使用多线程充分利用CPU
。若是一个链接不能读写,能够把这个事件记录下来,而后切换到别的就绪的链接进行数据读写。在buddha
中,Netty
被用来编写结构更加清晰的NIO
程序。
在实际应用中,RPC
服务的提供者每每须要使用集群来保证服务的稳定性与可靠性。所以须要实现一个服务注册中心,服务提供者将当前可用的服务地址信息注册至注册中心,而客户端在进行远程调用时,先经过服务注册中心获取当前可用的服务列表,而后获取具体的服务提供者的地址信息(该阶段能够进行负载均衡),根据地址信息向服务提供者发起调用。客户端能够缓存可用服务列表,当注册中心的服务列表发生变动时须要通知客户端。同时,当服务提供者变为不可用状态时也须要通知注册中心服务不可用。buddha
使用ZooKeeper
实现服务注册与发现功能。
buddha
是我学习验证RPC
过程当中诞生的一个轻量级分布式RPC
框架,代码放在了 GitHub。