RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种经过网络从远程计算机程序上请求服务,而不须要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通讯程序之间携带信息数据。在OSI网络通讯模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,而后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器得到进程参数,计算结果,发送答复信息,而后等待下一个调用信息,最后,客户端调用进程接收答复信息,得到进程结果,而后调用执行继续进行。java
有多种 RPC模式和执行。最初由 Sun 公司提出。IETF ONC 宪章从新修订了 Sun 版本,使得 ONC RPC 协议成为 IETF 标准协议。如今使用最广泛的模式和执行是开放式软件基础的分布式计算环境(DCE)。程序员
在支付系统的微服务架构中,基础服务的构建是重中之重, 本文重点分析如何使用Apache Thrift + Google Protocol Buffer来构建基础服务。数据库
1、RPC vs Restful缓存
在微服务中,使用什么协议来构建服务体系,一直是个热门话题。 争论的焦点集中在两个候选技术: (binary) RPC or Restful。性能优化
以Apache Thrift为表明的二进制RPC,支持多种语言(但不是全部语言),四层通信协议,性能高,节省带宽。相对Restful协议,使用Thrifpt RPC,在同等硬件条件下,带宽使用率仅为前者的20%,性能却提高一个数量级。可是这种协议最大的问题在于,没法穿透防火墙。服务器
以Spring Cloud为表明所支持的Restful 协议,优点在于可以穿透防火墙,使用方便,语言无关,基本上可使用各类开发语言实现的系统,均可以接受Restful 的请求。 但性能和带宽占用上有劣势。网络
因此,业内对微服务的实现,基本是肯定一个组织边界,在该边界内,使用RPC; 边界外,使用Restful。这个边界,能够是业务、部门,甚至是全公司。数据结构
2、 RPC技术选型架构
RPC技术选型上,原则也是选择本身熟悉的,或者公司内部内定的框架。 若是是新业务,则如今可选的框架其实也很少,却也足够让人纠结。并发
Apache Thrift
国外用的多,源于facebook,后捐献给Apache基金。是Apache的顶级项目Apache Thrift。使用者包括facebook, Evernote, Uber, Pinterest等大型互联网公司。 而在开源界,Apache hadoop/hbase也在使用Thrift做为内部通信协议。 这是目前最为成熟的框架,优势在于稳定、高性能。缺点在于它仅提供RPC服务,其余的功能,包括限流、熔断、服务治理等,都须要本身实现,或者使用第三方软件。
Dubbo
国内用的多,源于阿里公司。 性能上略逊于Apache Thrift,但自身集成了大量的微服务治理功能,使用起来至关方便。 Dubbo的问题在于,该系统目前已经很长时间没有维护更新了。官网显示最近一次的更新也是8个月前。
Google Protobuf
和Apache Thrift相似,Google Protobuf也包括数据定义和服务定义两部分。问题是,Google Protobuf一直只有数据模型的实现,没有官方的RPC服务的实现。 直到2015年才推出gRPC,做为RPC服务的官方实现。但缺少重量级的用户。
以上仅作定性比较。定量的对比,网上有很多资料,可自行查阅。 此外,还有一些不错的RPC框架,好比Zeroc ICE等,不在本文的比较范围。
Thrift 提供多种高性能的传输协议,但在数据定义上,不如Protobuf强大。
同等格式数据, Protobuf压缩率和序列化/反序列化性能都略高。
Protobuf支持对数据进行自定义标注,并能够经过API来访问这些标注,这使得Protobuf在数据操控上很是灵活。好比能够经过option来定义protobuf定义的属性和数据库列的映射关系,实现数据存取。
数据结构升级是常见的需求,Protobuf在支持数据向下兼容上作的很是不错。只要实现上处理得当,接口在升级时,老版本的用户不会受到影响。
而Protobuf的劣势在于其RPC服务的实现性能不佳(gRPC)。为此,Apache Thrift + Protobuf的RPC实现,成为很多公司的选择。
3、Apache Thrift + Protobuf
如上所述,利用Protobuf在灵活数据定义、高性能的序列化/反序列化、兼容性上的优点,以及Thrift在传输上的成熟实现,将二者结合起来使用,是很多互联网公司的选择。
服务定义:
service HelloService{
binary hello(1: binary hello_request);
}
协议定义:
message HelloRequest{
optional string user_name = 1; //访问这个接口的用户
optional string password = 2; //访问这个接口的密码
optional string hello_word = 3; //其余参数;
}
message HelloResponse{
optional string hello_word = 1; //访问这个接口的用户
}
想对于纯的thrift实现,这种方式虽然看起来繁琐,但其在可扩展性、可维护性和服务治理上,能够带来很多便利。
4、服务注册与发现
Spring cloud提供了服务注册和发现功能,若是须要本身实现,能够考虑使用Apache Zookeeper做为注册表,使用Apache Curator来管理Zookeeper的连接,它实现以下功能:
侦听注册表项的变化,一旦有更新,能够从新加载注册表。
管理到zookeeper的连接,若是出现问题,则进行重试。
Curator的重试策略是可配置的,提供以下策略:
BoundedExponentialBackoffRetry
ExponentialBackoffRetry
RetryForever
RetryNTimes
RetryOneTime
RetryUntilElapsed
通常使用指数延迟策略,好比重试时间间隔为1s,2s, 4s, 8s……指数增长,避免把服务器打死。
对服务注册来讲,注册表结构须要详细设计,通常注册表结构会按照以下方式组织:
机房区域-部门-服务类型-服务名称-服务器地址
因为在zookeeper上的注册和发现有必定的延迟,因此在实现上也得注意,当服务启动成功后,才能注册到zookeeper上;当服务要下线或者重启前,须要先断开同zookeeper的链接,再中止服务。
5、链接池
RPC服务访问和数据库相似,创建连接是一个耗时的过程,链接池是服务调用的标配。目前尚未成熟的开源Apache Thrift连接池,通常互联网公司都会开发内部自用的连接池。本身实现能够基于JDBC连接池作改进,好比参考Apache commons DBCP连接池,使用Apache Pools来管理连接。 在接口设计上,链接池须要管理的是RPC 的Transport:
public interface TransportPool { /** * 获取一个transport * @return* @throws TException */ public TTransport getTransport() throws TException;}
链接池实现的主要难点在于如何从多个服务器中选举出来为当前调用提供服务的链接。好比目前有10台机器在提供服务,上一次分配的是第4台服务器,本次应该分配哪一台?在实现上,须要收集每台机器的QOS以及当前的负担,分配一个最佳的链接。
6、API网关
随着公司业务的增加,RPC服务愈来愈多,这也为服务调用带来挑战。若是有一个应用须要调用多个服务,对这个应用来讲,就须要维护和多个服务器之间的连接。服务的重启,都会对链接池以及客户端的访问带来影响。为此,在微服务中,普遍会使用到API网关。API网关能够认为是一系列服务集合的访问入口。从面向对象设计的角度看,它与外观模式相似,实现对所提供服务的封装。
网关做用
API网关自己不提供服务的具体实现,它根据请求,将服务分发到具体的实现上。 其主要做用:
API路由: 接受到请求时,将请求转发到具体实现的worker机器上。避免使用方创建大量的链接。
协议转换: 原API可能使用http或者其余的协议来实现的,统一封装为rpc协议。注意,这里的转换,是批量转换。也就是说,原来这一组的API是使用http实现的,如今要转换为RPC,因而引入网关来统一处理。对于单个服务的转换,仍是单独开发一个Adapter服务来执行。
封装公共功能: 将微服务治理相关功能封装到网关上,简化微服务的开发,这包括熔断、限流、身份验证、监控、负载均衡、缓存等。
分流:经过控制API网关的分发策略,能够很容易实现访问的分流,这在灰度测试和AB测试时特别有用。
解耦合
RPC API网关在实现上,难点在于如何作到服务无关。咱们知道使用Nginx实现HTTP的路由网关,能够实现和服务无关。而RPC网关因为实现上的不规范,很难实现和服务无关。统一使用thrift + protobuf 来开发RPC服务能够简化API网关的开发,避免为每一个服务上线而带来的网关的调整,使得网关和具体的服务解耦合:
每一个服务实现的worker机器将服务注册到zookeeper上;
API网关接收到zookeeper的变动,更新本地的路由表,记录服务和worker(链接池)的映射关系。
当请求被提交到网关上时,网关能够从rpc请求中提取出服务名称,以后根据这个名称,找到对应的worker机(链接池),调用该worker上的服务,接受到结果后,将结果返回给调用方。
权限和其余
Protobuf的一个重要特性是,数据的序列化和名称无关,只和属性类型、编号有关。 这种方式,间接实现了类的继承关系。以下所示,咱们能够经过Person类来解析Girl和Boy的反序列化流:
message Person {
optional string user_name = 1;
optional string password = 2; }message Girl {
optional string user_name = 1;
optional string password = 2;
optional string favorite_toys = 3; }message Boy {
optional string user_name = 1;
optional string password = 2;
optional int32 favorite_club_count = 3;
optional string favorite_sports = 4; }
咱们只要对服务的输入参数作合理的编排,将经常使用的属性使用固定的编号来表示,既可使用通用的基础类来解析输入参数。好比咱们要求全部输入的第一个和第二个元素必须是user_name和password,则咱们就可使用Person来解析这个输入,从而能够实现对服务的统一身份验证,并基于验证结果来实施QPS控制等工做。
7、熔断与限流
Netflix Hystrix提供不错的熔断和限流的实现,参考其在GitHub上的项目介绍。这里简单说下熔断和限流实现原理。
熔断通常采用电路熔断器模式(Circuit Breaker Patten)。当某个服务发生错误,每秒错误次数达到阈值时,再也不响应请求,直接返回服务器忙的错误给调用方。 延迟一段时间后,尝试开放50%的访问,若是错误仍是高,则继续熔断;不然恢复到正常状况。
限流指按照访问方、IP地址或者域名等方式对服务访问进行限制,一旦超过给定额度,则禁止其访问。 除了使用Hystrix,若是要本身实现,能够考虑使用使用Guava RateLimiter
8、服务演化
随着服务访问量的增长,服务的实现也会不断演化以提高性能。主要的方法有读写分离、缓存等。
读写分离
针对实体服务,读写分离是提高性能的第一步。 实现读写分离通常有如下几种方式:
一、在同构数据库上使用主从复制的方式: 通常数据库,好比MySQL、HBase、Mongodb等,都提供主从复制功能。数据写入主库,读取、检索等操做都从从库上执行,实现读写分离。这种方式实现简单,无需额外开发数据同步程序。通常来讲,对写入有事务要求的数据库,在读取上的性能会比较差。虽然能够经过增长从库的方式来sharding请求,但这也会致使成本增长。
二、在异构数据库上进行读写分离。发挥不一样数据库的优点,经过消息机制或者其余方式,将数据从主库同步到从库。 好比使用MySQL做为主库来写入,数据写入时投递消息到消息服务器,同步程序接收到消息后,将数据更新到读库中。可使用Redis,Mongodb等内存数据库做为读库,用来支持根据ID来读取;使用Elastic做为从库,支持搜索。
三、微服务技术是程序员都离不开的话题,说到这里,也给你们推荐一个交流学习平台:架构交流群650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,如下的课程体系图也是在群里获取。相信对于已经工做和遇到技术瓶颈的码友,在这里会有你须要的内容。
缓存使用
若是数据量大,使用从库也会致使从库成本很是高。对大部分数据来讲,好比订单库,通常须要的只是一段时间,好比三个月内的数据。更长时间的数据访问量就很是低了。 这种状况下,没有必要将全部数据加载到成本高昂的读库中,即这时候,读库是缓存模式。 在缓存模式下,数据更新策略是一个大问题。
对于实时性要求不高的数据,能够考虑采用被动更新的策略。即数据加载到缓存的时候,设置过时时间。通常内存数据库,包括Redis,couchbase等,都支持这个特性。到过时时间后,数据将失效,再次被访问时,系统将触发从主库读写数据的流程。
对实时性要求高的数据,须要采用主动更新的策略,也就是接受Message后,当即更新缓存数据。
固然,在服务演化后,对原有服务的实现也会产生影响。 考虑到微服务的一个实现原则,即一个服务仅管一个存储库,原有的服务就被分裂成多个服务了。 为了保持使用方的稳定,原有服务被从新实现为服务网关,做为各个子服务的代理来提供服务。
以上是RPC与微服务的所有内容,如下是thrift + protobuf的实现规范的介绍。
附1、基础服务设计规范
基础服务是微服务的服务栈中最底层的模块, 基础服务直接和数据存储打交道,提供数据增删改查的基本操做。
附1.1 设计规范
文件规范
rpc接口文件名以 xxx_rpc_service.thrift 来命名; protobuf参数文件名以 xxx_service.proto 来命名。
这两种文件所有使用UTF-8编码。
命名规范
服务名称以 “XXXXService” 的格式命名, XXXX是实体,必须是名词。如下是合理的接口名称。
OrderService
AccountService
附1.2 方法设计
因为基础服务主要是解决数据读写问题,因此从使用的角度,对外提供的接口,能够参考数据库操做,标准化为增、删、改、查、统计等基本接口。接口采用 操做+实体来命名,如createOrder。 接口的输入输出参数采用 接口名+Request 和 接口名Response 的规范来命名。 这种方式使得接口易于使用和管理。
file: xxx_rpc_service.thrift
/**
**/
namespace java com.phoenix.service
/**
**/
service XXXRpcService {
/**
建立实体
输入参数:
输出参数
createXXXResponse: 建立成功,返回建立实体的ID列表;
异常
**/
binary createXXX(1: binary create_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
/**
更新实体
输入参数:
输出参数
updateXXXResponse: 更新成功,返回被更行的实体的ID列表;
异常
**/
binary updateXXX(1: binary update_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
/**
删除实体
输入参数:
输出参数
removeXXXResponse: 删除成功,返回被删除的实体的ID列表;
异常
**/
binary removeXXX(1: binary remove_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
/**
根据ID获取实体
输入参数:
输出参数
getXXXResponse: 返回对应的实体列表;
异常
**/
binary getXXX(1: binary get_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
/**
查询实体
输入参数:
输出参数
queryXXXResponse: 返回对应的实体列表;
异常
**/
binary queryXXX(1: binary query_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
/**
统计符合条件的实体的数量
输入参数:
输出参数
countXXXResponse: 返回对应的实体数量;
异常
**/
binary countXXX(1: binary count_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)
}
附1.3 参数设计
每一个方法的输入输出参数,采用protobuf来表示。
file: xxx_service.protobuf
/** * * 这里是版权申明**/option java_package ="com.phoenix.service";import"entity.proto";import"taglib.proto";/** * 建立实体的请求 */message CreateXXXRequest { optional string user_name = 1; //访问这个接口的用户 optional string password = 2; //访问这个接口的密码 repeated XXXX xxx = 21; // 实体内容;}/ * 建立实体的结果响应 * **/message CreateXXXResponse {repeated int64 id = 11;//成功建立的实体的ID列表}
附1.4 异常设计
RPC接口也不须要太复杂的异常,通常是定义三类异常。
file errors.thrift
/**
因为调用方的缘由引发的错误, 好比参数不符合规范、缺少必要的参数,没有权限等。
这种异常通常是能够重试的。
**/
exception UserException {
1: required ErrorCode error_code;
2: optional string message;
}
/**
因为服务器端发生错误致使的,好比数据库没法链接。这也包括QPS超过限额的状况,这时候rateLimit返回分配给的QPS上限;
**/
exception systemException {
1: required ErrorCode error_code;
2: optional string message;
3: i32 rateLimit;
}
/**
根据给定的ID或者其余条件没法找到对象。
**/
exception systemException {
1: optional string identifier;
}
附2、服务SDK
固然,RPC服务不该该直接提供给业务方使用,须要提供封装好的客户端。 通常来讲,客户端除了提供访问服务端的代理外,还须要对常有功能进行封装,这包括服务发现、RPC链接池、重试机制、QPS控制。这里首先介绍服务SDK的设计。 直接使用Protobuf做为输入参数和输出参数,开发出来的代码很繁琐:
GetXXXRequest.Builder request = GetXXXRequest.newBuilder();request.setUsername("username");request.setPassword("password");request.addId("123");GetXXXResponse response = xxxService.getXXX(request.build());if(response.xxx.size()==1)XXX xxx = response.xxx.get(0);
如上,有大量的重复性代码,使用起来不直观也不方便。 于是须要使用客户端SDK来作一层封装,供业务方调用:
class XXXService {//根据ID获取对象public XXX getXXX(String id){GetXXXRequest.Builder request = GetXXXRequest.newBuilder();request.setUsername("username");request.setPassword("password");request.addId("123");GetXXXResponse response = xxxService.getXXX(request.build());if(response.xxx.size()==1)returnresponse.xxx.get(0);returnnull;}}
对全部服务器端接口提供对应的客户端SDK,也是微服务架构最佳实践之一。以此封装完成后,调用方便可以像使用普通接口同样,无需了解实现细节。
若是想学习Java工程化、高性能及分布式、深刻浅出。性能调优、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级架构进阶群:180705916,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们