在支付系统的微服务架构中,基础服务的建设是重中之重。本文重点介绍如何使用ApacheThrift+GoogleProtocolBuffer构建基础服务。一、RPCvsRestful在微服务中,用什么协议构建服务系统一直是一个热门话题。辩论集中在两种候选技术上:(二进制)RPC或Restful。以ApacheThrift为代表的BinaryRPC支持多种语言(但不是所有语言),四层通信协议,高性能,节省带宽。与Restful协议相比,使用ThrifptRPC,在相同硬件条件下,带宽占用仅为前者的20%,但性能提升了一个数量级。但是这个协议最大的问题是它不能穿透防火墙。以SpringCloud为代表的Restful协议具有能够穿透防火墙、使用方便、语言无关等优点。基本上可以用各种开发语言实现的系统都可以接受Restful请求。但在性能和带宽使用方面存在劣势。所以业界微服务的实现,基本上就是确定一个组织边界,在这个边界内,使用RPC;在边界之外,使用Restful。这个边界可以是一个企业,一个部门,甚至是整个公司。2.RPC技术选型RPC技术选型,原则是选择自己熟悉的框架或者公司内部的框架。如果是新业务,现在可用的框架其实不多,但也足够让人纠结了。ApacheThrift在国外应用广泛,起源于facebook,后捐赠给ApacheFoundation。ApacheThrift是Apache的旗舰项目。用户包括facebook、Evernote、Uber、Pinterest等大型互联网公司。在开源世界中,Apachehadoop/hbase也在使用Thrift作为内部通信协议。这是目前最成熟的框架,具有稳定性和高性能的优点。缺点是只提供RPC服务,其他功能,包括限流、熔断、服务治理等,需要自己实现或者借助第三方软件。Dubbo在国内应用广泛,起源于阿里公司。性能略逊于ApacheThrift,但集成了大量微服务管理功能,使用起来相当方便。Dubbo的问题是系统长期没有维护和更新。官网显示最后一次更新也是8个月前。与ApacheThrift类似,GoogleProtobuf也包括数据定义和服务定义两部分。问题是GoogleProtobuf一直只实现了数据模型,并没有官方的RPC服务实现。直到2015年,gRPC才作为RPC服务的正式实现推出。但缺乏重量级用户。以上只是定性比较。对于定量比较,网上有很多资料,可以自行查阅。另外还有一些不错的RPC框架,比如ZerocICE等,不在本文的对比范围之内。Thrift提供了多种高性能传输协议,但在数据定义方面不如Protobuf强大。对于相同格式的数据,Protobuf压缩率和序列化/反序列化性能略高。Protobuf支持数据的自定义注解,可以通过API访问这些注解,这使得Protobuf在数据操作上非常灵活。例如protobuf定义的属性与数据库列的映射关系,可以通过option定义,实现数据访问。数据结构升级是一个普遍的需求,Protobuf在支持数据向后兼容方面做得很好。只要实现处理得当,接口升级时不会影响旧版本的用户。Protobuf的缺点是其RPC服务(gRPC)的实现性能较差。为此,ApacheThrift+Protobuf的RPC实现成为了很多公司的选择。3.ApacheThrift+Protobuf上文提到,利用Protobuf在数据定义灵活、高性能序列化/反序列化、兼容性等方面的优势,以及Thrift对传输的成熟实现,将两者结合起来是个不错的主意。众多互联网公司的选择。服务定义:serviceHelloService{binaryhello(1:binaryhello_request);}协议定义:messageHelloRequest{optionalstringuser_name=1;//访问该接口的用户optionalstringpassword=2;//访问该接口的密码optionalstringhello_word=3;//其他参数;}messageHelloResponse{optionalstringhello_word=1;//访问该接口的用户}认为对于纯粹的thrift实现来说,这种方式虽然看似繁琐,但在扩展性、可维护性、服务治理等方面可以带来很多便利。4、服务注册与发现Springcloud提供了服务注册与发现功能。如果需要自己实现,可以考虑使用ApacheZookeeper作为注册中心,使用ApacheCurator来管理Zookeeper链接。它实现了以下功能:监听注册表项的变化,一旦有更新,就可以重新加载注册表。管理到zookeeper的链接并在出现问题时重试。Curator的重试策略是可配置的,提供了以下策略:BoundedExponentialBackoffRetryExponentialBackoffRetryRetryForeverRetryNTimesRetryOneTimeRetryUntilElapsed一般采用指数延迟策略。例如,重试间隔为1s、2s、4s、8s……呈指数增长,以避免杀死服务器。对于服务注册,需要详细设计注册结构。一般registry结构会这样组织:机房区域-部门-服务类型-服务名称-服务器地址由于在zookeeper上注册和发现有延迟,所以在实现上还必须注意服务启动成功后才能注册到zookeeper;当服务下线或重启时,需要先断开与zookeeper的连接,然后再停止服务。5、连接池RPC服务访问类似于数据库。建立连接是一个耗时的过程,连接池是服务调用的标准配置。目前还没有成熟的开源ApacheThrift连接池。一般互联网公司都会开发自己的连接池供内部使用。自己实现可以基于JDBC连接池进行改进,比如参考ApachecommonsDBCP连接池,使用ApachePools来管理连接。在接口设计上,连接池需要管理的是RPC的Transport:publicinterfaceTransportPool{/***获取一个transport*@return*@throwsTException*/publicTTransportgetTransport()throwsTException;}实现连接池的难点主要在于如何从多个服务器连接选择的连接为当前调用服务。比如目前有10台机器提供服务,上次分配的是第四台服务器,这次应该分配哪一台?在实现上,需要收集每台机器的QOS和当前的负担,分配一个最优的。连接。6、API网关随着公司业务的增长,RPC服务越来越多,这也给服务调用带来了挑战。如果一个应用程序需要调用多个服务,那么对于这个应用程序来说,需要维护与多个服务器的链接。重启服务会影响连接池和客户端访问。因此,API网关在微服务中得到了广泛的应用。API网关可以看作是一系列服务集合的访问入口。从面向对象设计的角度看,它类似于外观模式,实现了对所提供服务的封装。网关的作用API网关本身并不提供服务的具体实现,它根据请求将服务分发给具体的实现。其主要功能:API路由:当收到请求时,将请求转发给具体实现的工作机。避免在消费者端建立大量连接。协议转换:原API可能使用http或其他协议实现,统一封装成rpc协议。注意这里的转换是批量转换。也就是说,这组的API本来是使用http实现的,现在需要转成RPC,所以引入了一个网关来统一处理。对于单个服务的转换,单独开发一个Adapter服务来执行。封装公共功能:在网关上封装微服务治理相关的功能,简化微服务的开发,包括熔断、限流、鉴权、监控、负载均衡、缓存等。分发:通过控制API网关的分发策略,易于实现访问的分布,特别适用于灰度测试和AB测试。在解耦RPCAPI网关的实现中,难点在于如何实现服务独立。我们知道,使用Nginx来实现一个HTTP路由网关是可以做到服务独立的。但由于RPC网关的实现不规范,很难做到与服务无关。统一使用thrift+protobuf开发RPC服务,可以简化API网关的开发,避免每个服务上线带来的网关调整,网关与具体服务解耦:每个服务实现的worker机器注册服务对动物园管理员;API网关接收zookeeper的变化,更新本地路由表,记录services和workers(连接池)的映射关系。当请求提交给网关时,网关可以从rpc请求中提取服务名称,然后根据名称找到对应的worker机器(连接池),调用worker上的服务,接收后返回结果结果给调用者。权限和其他Protobuf的一个重要特性是数据的序列化与名称无关,仅与属性类型和数字有关。这样就间接实现了类的继承关系。如下图,我们可以通过Person类解析Girl和Boy的反序列化流程:messagePerson{optionalstringuser_name=1;optionalstringpassword=2;}messageGirl{optionalstringuser_name=1;optionalstringpassword=2;optionalstringfavorite_toys=3;}messageBoy{optionalstringuser_name=1;optionalstringpassword=2;optionalint32favorite_club_count=3;optionalstringfavorite_sports=4;}我们只需要合理安排服务的入参,用固定的数字来表示常用的属性,就可以用通用的基础类来解析入参.比如我们要求所有输入的第一个和第二个元素必须是user_name和password,那么我们就可以使用Person来解析这个输入,从而实现服务的统一认证,并根据认证结果实现QPS控制等工作.7.熔断限流NetflixHystrix提供了很好的熔断限流实现,参考其在GitHub上的项目介绍。这里简单介绍一下熔断限流的原理。熔断器一般采用断路器模式(CircuitBreakerPatten)。当服务发生错误,每秒错误数达到阈值后,将不再响应请求,直接向调用方返回serverbusy错误。延迟一段时间后,尝试打开50%的访问,如果错误率仍然很高,继续熔断;否则恢复正常情况。限流是指根据访问者、IP地址或域名来限制对服务的访问。一旦超过给定的配额,就禁止访问。除了使用Hystrix,如果想自己实现的话,可以考虑使用GuavaRateLimiter8.服务演化随着服务访问量的增加,服务的实现也会不断演化以提升性能。主要方法有读写分离、缓存等。读写分离针对的是实体服务,读写分离是提高性能的第一步。实现读写分离一般有两种方式:在同构数据库上使用主从复制:一般的数据库,如MySQL、HBase、Mongodb等,都提供主从复制。数据写入主库,从从库进行读取、检索等操作,实现读写分离。该方法实现简单,不需要额外开发数据同步程序。一般来说,对写有事务性需求的数据库,读性能会很差。虽然可以通过增加从库来增加分片请求,但这也会导致成本增加。异构数据库的读写分离。利用不同的数据库,通过消息机制或其他方式将主库的数据同步到从库。比如使用MySQL作为主库来写,当数据写入时,将消息投递到消息服务器,同步程序收到消息后将数据更新到读库。可以使用Redis、Mongodb等内存数据库作为读库,支持基于ID的读;Elastic可以作为从库来支持搜索。缓存使用如果数据量很大,使用从库也会导致从库成本非常高。对于大部分数据,比如订单库,一般只需要一段时间,比如三个月以内的数据。较长时间的数据访问非常低。在这种情况下,不需要将所有数据加载到昂贵的读库中,即此时读库处于缓存模式。在缓存模式下,数据更新策略是个大问题。对于实时性要求不高的数据,可以考虑被动更新策略。即当数据加载到缓存中时,设置过期时间。一般的内存数据库,包括Redis、couchbase等,都支持这个特性。过期时间后,数据将失效,再次访问时,系统会触发从主库读写数据的过程。对于实时性要求高的数据,需要采用主动更新策略,即收到Message后立即更新缓存数据。当然,服务演化后,原有服务的实现也会受到影响。考虑到微服务的一个实现原理,即一个服务只管理一个repository,将原来的服务拆分成多个服务。为了保持用户的稳定性,将原来的服务重新实现为服务网关,作为各个子服务的代理提供服务。以上就是RPC和微服务的全部内容。下面介绍thrift+protobuf的实现规范。附件1.基础服务设计规范基础服务是微服务服务栈中最底层的模块。基础服务直接处理数据存储,提供数据的增删改查等基础操作。附录1.1设计规范文件规范rpc接口文件名称命名为xxx_rpc_service.thrift;protobuf参数文件的名称为xxx_service.proto。这两个文件都以UTF-8编码。命名规范服务名称的命名格式为“XXXXService”,其中XXXX为实体,必须为名词。以下是合理的接口名称。OrderServiceAccountService附件1.2方法设计由于基础服务主要是解决数据读写问题,所以从使用的角度,对外提供的接口可以参考数据库操作,规范增删改查等基础接口,查询、统计。接口以操作+实体命名,如createOrder。接口的输入输出参数按照接口名+Request和接口名Response的规范命名。这种方法使界面易于使用和管理。file:xxx_rpc_service.thrift/***这里是版权声明**/namespacejavacom.phoenix.service/***提供了对XXX实体的增删改查的基本操作。**/serviceXXXRpcService{/***CreateEntity*入参:*1.createXXXRequest:创建请求,支持创建多个实体;*输出参数*createXXXResponse:创建成功,返回创建实体ID列表;*例外*1。userException:输入参数错误;*2.systemExeption:服务器出错,无法创建;*3.notFoundException:需要的参数没有提供。**/binarycreateXXX(1:binarycreate_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)/***更新实体*输入参数:*1.updateXXXRequest:更新请求,支持同步更新多个实体;*输出参数*updateXXXResponse:更新成功,返回更新实体的ID列表;*Exception*1.userException:入参错误;*2.systemExeption:服务器创建失败,错误;*3.notFoundException:在服务器端找不到实体。**/binaryupdateXXX(1:binaryupdate_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)/***删除实体*输入参数:*1.removeXXXRequest:删除请求,根据id删除,支持一次删除多个实体;*输出参数*removeXXXResponse:删除成功,返回被删除实体的ID列表;*Exception*1.userException:输入参数错误;*2.systemExeption:服务器创建失败;*3.notFoundException:在服务器端没有找到实体。**/binaryremoveXXX(1:binaryremove_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)/***通过ID获取实体*传入参数:*1.getXXXRequest:获取请求,根据toid获取,支持一次获取多个实体;*输出参数*getXXXResponse:返回对应的实体列表;*Exception*1.userException:输入参数错误;*2.systemExeption:服务器出错,无法创建;*3.notFoundException:在服务器端找不到实体。**/binarygetXXX(1:binaryget_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)/***查询实体*入参:*1.queryXXXRequest:查询条件;*出参*queryXXXResponse:返回对应的实体列表;*Exception*1.userException:输入参数错误;*2.systemExeption:服务器出错,无法创建;*3.notFoundException:在服务器上找不到实体。**/binaryqueryXXX(1:binaryquery_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)/***统计符合条件的实体个数*输入参数:*1.countXXXRequest:查询条件;*输出参数*countXXXResponse:返回对应的实体数量;*Exception*1.userException:输入参数错误;*2.systemExeption:服务器出错,无法创建;*3.notFoundException:实体在服务器上不存在出现。**/binarycountXXX(1:binarycount_xxx_request)throws(1:Errors.UserExceptionuserException,2:Errors.systemException,3:Errors.notFoundException)}附录1.3参数设计每个方法的输入输出参数都用protobuf表示。file:xxx_service.protobuf/****这里是版权声明**/optionjava_package="com.phoenix.service";import"entity.proto";import"taglib.proto";/***请求创建一个entity***/messageCreateXXXRequest{optionalstringuser_name=1;//访问该接口的用户optionalstringpassword=2;//访问该接口的密码repeatedXXXXxxx=21;//实体内容;}/***创建实体的响应***/messageCreateXXXResponse{repeatedint64id=11;//创建成功的实体ID列表}附件1.4异常设计RPC接口不需要太复杂的异常,一般定义三种异常。fileerrors.thrift/***调用者导致的错误,如参数不符合规范、缺少必要的参数、没有权限等。*这种异常一般可以重试。***/exceptionUserException{1:requiredErrorCodeerror_code;2:optionalstringmessage;}/***是服务器端错误导致的,比如无法连接数据库。这也包括QPS超限的情况。此时rateLimit返回分配QPS的上限;***/exceptionsystemException{1:requiredErrorCodeerror_code;2:optionalstringmessage;3:i32rateLimit;}/***根据给定的ID或其他条件找不到对象。***/exceptionsystemException{1:optionalstringidentifier;}附录2.服务SDK当然RPC服务不能直接提供给业务方,需要提供一个封装好的客户端。一般来说,客户端除了提供代理访问服务器外,还需要封装一些常用的功能,包括服务发现、RPC连接池、重试机制、QPS控制等。这里首先介绍服务SDK的设计。直接使用Protobuf作为输入参数和输出参数,开发代码非常繁琐:123");GetXXXResponseresponse=xxxService.getXXX(request.build());if(response.xxx.size()==1)XXXxxx=response.xxx.get(0);如上,重复代码较多,使用起来不直观、不方便。所以需要使用客户端SDK做一层封装,让业务方调用:classXXXService{//根据ID获取对象publicXXXgetXXX(Stringid){GetXXXRequest.Builderrequest=GetXXXRequest.newBuilder();request.setUsername("用户名");request.setPassword("密码");request.addId("123");GetXXXResponseresponse=xxxService.getXXX(request.build());if(response.xxx.size()==1)returnresponse.xxx.get(0);returnnull;}}为所有服务端接口提供相应的客户端SDK,这也是微服务架构的最佳实践之一。封装完成后,调用者可以在不知道实现细节的情况下使用公共接口。【本文为专栏作者《凤凰牌老熊》原创稿件,转载请微信联系作者公众号《凤凰牌老熊》转载】点此阅读作者更多好文
