从0到1:轻松搞定从RPC到服务化框架的设计!

共 7923字,需浏览 16分钟

 ·

2021-11-24 08:40


导语 | 目前互联网系统都是微服务化,那么就需要RPC调用,因此本文梳理了从RPC基本框架协议到整个服务化框架体系建设中所包含的知识点,重点在于RPC框架和服务治理能力的梳理。


一、从RPC到服务化框架设计


(一)RPC基本框架


  • 理解RPC


RPC就是远程过程调用。我们本地的函数调用,就是A方法调B方法,然后获取结果,RPC就是让你像本地函数调用一样进行跨服务的函数调用。我们现在都在讲微服务,服务都拆分为微服务了,那么相关依赖的调用,就会变成跨服务之间的调用,他们的通信方式就是依靠RPC。


  • RPC基础结构(RPC协议)


Nelson的论文Implementing Remote Procedure Calls告诉我们, RPC协议包括5个部分:


  • Client

  • Client-stub

  • RPCRuntime

  • Server-stub

  • Server



当Client发起一个远程调用时,它实际上是调用本地的Stub。本地Stub负责将调用的接口、方法和参数,通过约定的协议规范进行编码,并通过本地的RPCRuntime进行传输,然后将数据包发送到网络上传输出去。当Server端的RPCRuntime收到请求后,交给Server-Stub进行解码,然后调用服务端的方法,服务端执行方法,返回结果,Server-Stub将返回结果编码后,发送给Client,Client端的RPCRuntime收到结果,发给Client-Stub解码得到结果,返回给客户端。


这里面分了3个层次:


  • 对于客户端和服务端,都像是本地调用一样,专注于业务逻辑的处理就可以了。


  • 对于Stub层,处理双方约定好的语法、语义、封装、解封装。


  • 对于RPCRuntime,主要处理高性能的传输,以及网络的错误和异常。



(二)RPC框架的重点


从RPC基础结构中,我们总结出RPC框架的重点,包括4部分,如下:


  • 数据序列化


序列化就是将数据结构或对象转换成二进制的过程,也就是编码的过程,序列化后数据才方便进行网络传输;反序列化就是在序列化过程中所生成的二进制转换成数据结构或者对象的过程,将二进制转换为对象后业务才好进行后续的逻辑处理。


常见的序列化协议如下:


  • protobuf(IDL)

  • json

  • xml

  • Hessian2 (JAVA系)


常见的RPC框架如gRPC、Thrift、Dubbo、RPCX 、Motan等会支持上述协议中的大部分,尤其是PB和json。目前从性能上和使用广泛度上来看,现在一般推荐使用PB,当然很多自研的框架里面他们也会自己实现他们自己的序列化协议。


  • 网络传输(网络通信)


在数据被序列化为二进制后就可以行网络传输了,网络传输就是我们的数据怎么传输到对方服务器上,目前来说,常见的通信传输方式包括:TCP、UDP、HTTP(HTTP2.0)、QUIC协议,TCP是大部分框架都会默认支持的,额外这里要说明一下,RPCX支持QUIC而gRPC支持HTTP2.0。


QUIC(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP传输层协议,同时兼具TCP、TLS、HTTP/2等协议的可靠性与安全性,可以有效减少连接与传输延迟,更好地应对当前传输层与应用层的挑战。QUIC在应用程序层面就能实现不同的拥塞控制算法,不需要操作系统和内核支持,这相比于传统的TCP协议,拥有了更好的改造灵活性,非常适合在TCP协议优化遇到瓶颈的业务。


  • RPC调用方式


网络传输只是数据传输非常基础的一方面,从业务上来看,我们发起RPC 调用,那么还需要RPC的调用方式


  • 同步RPC:最常用的服务调用方式,开发比较简单,比较符合编程人员的习惯,代码相对容易维护些。


  • 异步RPC:客户端发起服务调用之后,不同步等待响应,而是注册监听器或者回调函数,待接收到响应之后发起异步回调,驱动业务流程继续执行,实现起来相对复杂,但是高并发场景下性能会更好。


  • 并行RPC:并行服务调用,一次I/O操作,可以发起批量调用,然后同步等待响应;


  • 服务治理


RPC协议只规定了Client与Server之间的点对点调用流程,包括Stub、通信协议、RPC消息解析等部分。但是在实际应用中,远程过程调用的时候还需要考虑服务的路由、负载均衡、高可用等问题,而保障服务之间的调用就需要进行服务治理,服务治理基本就涵盖:服务注册和发现、限流、降级、熔断、重试、失败处理、负载均衡等。



(三)常见RPC框架


RPC框架就是在RPC协议的基础上,来完善一些偏向业务实际应用的功能,从而满足不同场景的业务诉求。综合来看,目前的RPC框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。


  • 服务治理型RPC框架


服务治理型的RPC框架有Dubbo、DubboX、Motan、RPCX等。


  • Dubbo是阿里开源的分布式服务框架,能够实现高性能RPC调用,并且提供了丰富的管理功能,是十分优秀的RPC框架。


  • 序列化Protobuf 、JsonRPC、Hessian2。


  • 兼容协议:gRPC、Thrift。


  • Triple协议是Dubbo3的主力协议,完整兼容gRPC over HTTP/2。


  • DubboX是基于Dubbo框架开发的RPC框架,支持REST风格远程调用,并增加了一些新的feature。


  • Motan是微博开源的一套轻量级、方便使用的Java语言的RPC框架


  • 使用对java更友好的hessian2进行序列化。


  • 通讯协议支持Motan、http、tcp(默认TCP)。


  • RPCX: 一个用Go实现的类似Dubbo的分布式RPC框架


  • 支持多种编解码协议,如Gob、Json、MessagePack、gencode、ProtoBuf等。


  • 支持常见的服务治理策略。


  • 支持TCP、HTTP、QUIC和KCP。


这类的RPC框架的特点是功能丰富,提供高性能的远程调用以及服务发现及治理功能,适用于大型服务的微服务化拆分以及管理,对于特定语言的项目可以十分友好的透明化接入。但缺点是语言耦合度较高,跨语言支持难度较大。


  • 跨语言调用型RPC框架


跨语言调用型的RPC框架有:


  • gRPC是Google开发的高性能、通用的开源RPC框架。


  • 支持ProtoBuf


  • 基于HTTP2


  • 支持多中语言


  • 业界很多基于gRPC来开发自己的RPC框架(美图、华为)


  • Thrift是Apache的一个跨语言的高性能的服务框架,也得到了广泛的应用。


  • Hprose是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。


这一类的RPC框架重点关注于服务的跨语言调用,能够支持我们常见的大部分的语言进行语言无关的调用,非常适合于为不同语言提供通用远程服务的场景,但这类框架没有服务发现、服务治理等机制,应用时需要我们自己来实现服务发现、服务路由等策略。


跨语言指的是,客户端和服务端可以在各种环境中运行和相互通信,并且可以用框架支持的任何语言编写(gRPC如下:)



对比



二、通用的服务化框架设计


我们一般讲的微服务框架包含了RPC框架,微服务体系中最重要的就是RPC框架,并且是一般是偏向服务治理的RPC框架。微服务需要提供的核心能力包括:微服务架构中通讯的基础协议RPC、服务发现与注册、负载均衡、容错、熔断、限流、降级、权限、全链路日志跟踪


(一)微服务框架的核心能力(服务治理策略)


  • 服务注册与发现


微服务后,服务大量增加,因此我们一定要能够有一个合适的方案能够发现对方的所有服务,业界比较常见的服务发现的组件如zookeeper、etcd、consul等,基本原理就是先将自己的服务列表注册,然后提供服务发现能力。


服务发现机制有服务端发现客户端发现两种实现方式。


  • 服务端发现模式(server-side):可以通过DNS或者带VIP的负载均衡实现。


优点是对客户端无侵入性,客户端只需要简单的向负载均衡或者服务域名发起请求,无需关系服务发现的具体细节,也不用引入服务发现的逻辑。


缺点是不灵活,不方便难异化处理;并且同时需要引入一个统一的负载均衡器。


  • 客户端发现模式(client-side):需要客户端服务注册中心中查询服务地址列表,然后再决定通过哪个地址请求服务。


其灵活性更高,可以根据客户端的诉求进行满足自身业务的负载均衡,但是客户端需要引入服务发现的逻辑,同时依赖服务注册中心。


常见服务注册组件包括:zookeeper、Etcd、Consul。java系的一般选择zookeeper,而Golang的一般选择consul或etcd,这个也就是各自选择对应的语言。etcd相比而言,是用的较多的,K8s系统里面也基于是etcd。


  • 服务路由&负载均衡


服务路由和服务发现紧密相关,服务路由一般不会设计成一个独立运行的系统,通常情况下是和服务发现放在一起实现的。


服务路由核心的功能就是路由算法,也就是负载均衡,从业界来看,负载均衡的实现方案一般可以分为三类:


  • 服务端负载均衡:


负载均衡器在一台单独的主机上,可以采用软负载,如Nginx,LVS等,也可以采用硬负载,如F5等。


实现简单单存在单点问题,所有的流量都需要通过负载均衡器,如果负载均衡器存在问题,则直接导致服务不能正常提供服务;中间经过负载均衡器做代理,性能也有一定损耗。


  • 客户端负载均衡


解决了服务端负载的单点问题,每个客户端都实现了自己的负载功能,负载能力和客户端进程在一起。


负载均衡要求每个客户端自己实现,如果不同的技术栈,每个客户端则需要使用不同的语言实现自己的负载能力。


目前业界主流的微服务框架都是采用客户端负载均衡方案


  • 客户端主机独立负载均衡


服务发现和负载的能力从客户端进程移出,客户端进程和负载均衡进程是2个独立的进程,在同一个主机上。也就是SideCar模式。


没有单点问题,如果一个主机的负载均衡器出问题,只影响一个节点调用,不影响其他的节点,负载均衡器本身负载也较小,性能损耗较低。


常见的负载均衡算法有:随机路由、轮询路由、hash、权重、最小压力路由、最小连接数路由、就近路由等。


  • 服务容错


负载均衡和容错是服务高可用的重要手段。服务容错的设计有个基本原则,就是“Design for Failure”。常见的服务容错策略如请求重试、流控、隔离


  • 超时与重试


超时是一种最常见的服务容错模式,在微服务调用的场景中,它主要解决了当依赖服务出现建立网络连接或响应延迟,不用无限等待的问题,调用方可以根据事先设计的超时时间中断调用,及时释放关键资源(如连接数),避免整个系统资源耗尽出现拒绝对外提供服务这种情况。常见的超时里面,一般如网络连接超时时间、RPC的响应超时时间等。


重试一般和超时模式结合使用,适用于对于下游服务的数据强依赖的场景(不强依赖的场景不建议使用!),通过重试来保证数据的可靠性或一致性,常用于因网络抖动等导致服务调用出现超时的场景。在重试的设计中,我们一般都会引入,Exponential Backoff的策略,也就是所谓的“指数级退避”,每一次重试所需要的sleep时间都会指数增加,否则可能会导致拖累到整个系统。


  • 服务限流


限流和降级用来保证核心服务的稳定性;限流是指限制每个服务的最大访问量、降级是指高峰期对非核心的系统进行降级从而保证核心服务的可用性


限流的实现方式:


  • 计数器方式(最简单)


  • 队列算法:常规队列,FIFO;优先级队列;权重队列。


  • 漏斗(漏桶)算法Leaky Bucket。


  • 令牌桶算法Token Bucket。


  • 基于响应时间的动态限流:参考TCP协议中算法,TCP使用RTT来探测网络的延时和性能,从而设定相应的“滑动窗口”的大小。


分布式限流和单机限流:


单机限流:有多种限流算法可供选择,最主要的是两种,漏桶算法及令牌桶算法。如果要对线上并发总数进行严格限定的话,漏桶算法可能会更合适一些,这是单机限流机制。


分布式限流(集群限流):集群限流的情况要更复杂一些,一般是中心化的设计。


个可选的方案是先在各个微服务节点上实现一个计数器,对单位时间片内的调用进行计数,计数值会被定期的汇总到日志中心,由统计分析器进行统一汇总,算出这个时间片的总调用量,集群限流分析器会拿到这个总调用量,并和预先定义的限流阈值进行比对,计算出一个限流比例,这个限流比例会通过服务注册中心下发到各个服务节点上,服务节点基于限流比例会各自算出当前节点对应的最终限流阈值,最后利用单机限流进行流控。


简单的基于Redis来做,但是方案的缺点显而易见,每取一次令牌都会进行一次网络开销,而网络开销起码是毫秒级,所以这种方案支持的并发量是非常有限的。


分布式限流业界常用的框架包括Hystrix、resilience4j。


  • 容错降级


容错降级可以分为三大类,从小到大依次是:


接口降级:对非核心接口,可以设置为直接返回空或者异常,可以在高峰期减少接口对资源如CPU、内存、磁盘、网络的占用和消耗。


功能降级:对非核心功能,可以设置该功能直接执行本地逻辑,不做跨服务、跨网络访问;也可设置降级开关,一键关闭指定功能,保全整体稳定;还可以通过熔断机制实现。


服务降级:对非核心服务,可以通过服务治理框架根据错误率或者响应时间自动触发降级策略;还可以通过断路器实现。


  • 熔断


熔断设计来源于日常生活中的电路系统,在电路系统中存在一种熔断器(Circuit Breaker),它的作用就是在电流过大时自动切断电路。熔断器一般要实现三个状态:闭合、断开和半开,分别对应于正常、故障和故障后检测故障是否已被修复的场景。


闭合:正常情况,后台会对调用失败次数进行积累,到达一定阈值或比例时则自动启动熔断机制。


断开:一旦对服务的调用失败次数达到一定阈值时,熔断器就会打开,这时候对服务的调用将直接返回一个预定的错误,而不执行真正的网络调用。同时,熔断器内置了一个时间间隔,当处理请求达到这个时间间隔时会进入半熔断状态。


半开:在半开状态下,熔断器会对通过它的部分请求进行处理,如果对这些请求的成功处理数量达到一定比例则认为服务已恢复正常,就会关闭熔断器,反之就会打开熔断器。


熔断设计的一般思路是,在请求失败N次后在X时间内不再请求,进行熔断;然后再在X时间后恢复M%的请求,如果M%的请求都成功则恢复正常,关闭熔断,否则再熔断Y时间,依此循环。


在熔断的设计中,根据Netflix的开源组件hystrix的设计,最重要的是三个模块:熔断请求判断算法、熔断恢复机制、熔断报警:


  • 熔断请求判断机制算法:根据事先设置的在固定时间内失败的比例来计算。


  • 熔断恢复:对于被熔断的请求,每隔X时间允许部分请求通过,若请求都成功则恢复正常。


  • 熔断报警:对于熔断的请求打异常日志和监控,异常请求超过某些设定则报警。


  • 隔离


隔离,也就是Bulkheads隔板的意思,这个术语是用在造船上的,也就是船舱里防漏水的隔板。


在服务化框架的隔离设计中,我们同样是采用类似的技术来让我们的故障得到隔离。因此这里的重点就是需要我们对系统进行分离。一般来说,有两种方式,一种是以服务的类型来做分离,一种是以用户来做分离。


  • 以服务的种类来做分离的方式:比如一个社交APP,服务类型包括账号系统、聊天系统,那么可以通过不同系统来做隔离。


  • 以用户来做分离的方式:比如通过策略来实现不同的用户访问到不同的实例。


  • 集群容错


在分布式场景下,我们的服务在集群中的都是有冗余的,一个是为容错,一个是为了高并发,针对大量服务实例的情况下,因此就有了集群容错的设计。集群容错是微服务集群高可用的保障,它有很多策略可供选择,包括:


  • 快速失败(Failfast):快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。


  • 失败转移(Failover):失败自动切换,当出现失败,重试集群其它服务实例 。通常用于读操作,但重试会带来更长延迟。一般都会设置重试次数。


  • 失败重试(Failback):失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。


  • 聚合调用(Forking):并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。一般会设置最大并行数。


  • 广播调用(Broadcast):广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。



(二)微服务框架的基础能力


  • 服务监控和告警


开源代表作:Prometheus+Grafana,他们都遵循OpenMetrics,基本数据格式分为Gauge、Count、Summary、Histogram。


  • 分布式服务Tracing跟踪系统


目前有两种协议规范:


  • OpenTracing:链路跟踪领域的标准,目前业界系统支持最多的标准,开源代表作:jaeger、zipkin。


  • OpenTelemetry:可观测性领域的标准,对Trace,Metrics,Log统一支持的唯一标准。


  • 配置中心


配置中心用来管理大量微服务之间的业务配置,并且是中心化的统一配置中心来进行管理。


  • 远程日志


远程日志组件的代表作是ELK系统:Elasticsearch、Logstash、Kibana。


在微服务架构中,一个客户端请求的接入,往往涉及到后端一系列服务的调用,如何将这些请求串联起来?业界常用的方案是采用全局流水号【traceID】串联起来。通过全局流水号【traceID】,从日志里面可以拉出整条调用链路。


这里关于整体链路又和分布式服务Tracing跟踪系统关联起来,Tracing可以知道整体链路的请求质量,远程日志+traceID可以知道整体链路的日志详情。



(三)微服务框架依托的自动化运维能力


微服务框架建设好后,那么大量服务怎么运维,这就依托自动化运维能力,包括如下几个方面:


  • 自动化测试


  • 自动化部署


  • 生命周期管理


业界目前一般采用容器平台,微服务框架+K8s容器平台 是当今互联网业务的黄金标准。们在123上部署的容器,其实就是运行在K8s平台上的。123只是一个管理页面,我们服务部署的容器是运行在K8s平台上,一个实例就是一个Pod,K8s就用来管理这些Pod的生命周期。



三、小结:自己搭建一个服务化框架的思路


自己搭建一个服务化框架的思路:


首先,要确定好基本的RPC通信协议,一般会选择开源方案,重点关注:功能需求的满足度、多语言的支持、性能和稳定性、区活跃度、成熟度。


其次,基于开源的RPC框架来搭建而不是完全从0开始。可选的框架包括Dubbo、Motan、gRPC、Thrift。


关于方案的对比,这里不再陈述,网上可以搜索得到,想要表达的是,每个公司的情况不一样,开发人员的能力和语言也不一样,因此方案选型需要根据自身情况而定,没有最好,只有最合适!


最后,Go语言方面,gRPC是业界公认的比较好的RPC框架,基于gRPC+一些服务治理策略可以实现一个服务化框架。这些服务治理的策略,很多也都可以用一些开源的组件。



 作者简介


吴德宝

腾讯PCG后台开发工程师

腾讯PCG后台开发工程师,目前负责腾讯PCG电商业务开发。擅长IM、后台基础服务,容器等技术。



 推荐阅读


是什么让AIoT开发更便捷?—TencentOS

超专业解析!10分钟带你搞懂Linux中直接I/O原理

碳中和的入口与出口,数字化建设该如何完成?

基于Protobuf共享字段的分包和透传零拷贝技术,你了解吗?






浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报