RPC 微服务框架设计

本文依旧主要参考沈剑大佬的微服务设计相关的五篇博文及其精彩评论,还参考其他数位网友的优秀分享,文末是完整参考。

1、SOA 和 微服务

SOA(Service-Oriented Architecture,面向服务的架构):更多是一种架构模式思想。

微服务架构:从 SOA 架构衍生出的一种更具体的变种架构模式。

刚接触没必要把两种概念梳理的特别清楚,有个大致的印象就行,因为两者确实十分类似,没有实践经验时很难真正区分两者。

参考:SOA架构和微服务架构的区别

2、集群架构 VS 微服务架构

2.1 集群架构的痛点

  • 系统高可用性差:所有业务代码都运行在一个进程里,不同业务的线程互相抢占资源,相互影响,sql 慢查询,cpu 100%
  • 代码量大,升级部署时间长
  • 抑制了迭代周期:频繁上线可能影响服务的可用性,降低用户体验;控制上线频率则会影响迭代速度。多个需求合并上线,必将导致上线风险的提高
  • DB耦合,垂直分库困难
  • 限定编程语言

2.2 微服务架构的优势

  • 各个模块独立部署发布,缩短迭代周期,每个需求都上线一次,降低上限风险。
  • 各个服务模块职责更清晰
  • 大部分服务都是无状态的,更容易扩展性能
  • 不限定编程语言,增加了技术的可选择性,比如对性能要求高的模块可以用 c++ 编写,需要调用算法模型的可以用 python 编写,其他可以用 Java 或者 Golang 编写。

2.3 带来的新问题

  • 本地调用变成远程过程调用,超时重试,被调用的服务需要考虑接口幂等。
  • 提升了系统复杂度,分布式事务,join必须移到 service 实现
  • 需要考虑服务治理,监控,限流,鉴权等问题。
  • 服务数量增加后,需要考虑使用 devops 来进行自动化部署

3、微服务拆分的粒度

纵向拆分

  • 业务处理层:所有子业务线一个服务、每个子业务线一个服务、
  • 数据访问层:一个数据库一个服务、一张表一个服务。

横向拆分:通用的功能,比如 sso 登录、服务发现、监控、鉴权等。

拆分的粒度主要考虑:不同公司,不同业务,不同发展阶段 -> 决定拆分粒度。

3.1 细粒度拆分的优点

(1)服务都能够独立部署

(2)无状态,扩展性更好,扩容和缩容方便,有利于提高资源利用率和应对高流量。

(3)拆得越细,耦合相对会减小,容错相对会更好,一个服务出问题不影响其他服务。

3.2 细粒度拆分的不足

(1)拆得越细,系统越复杂,服务之间的依赖关系也更复杂,从而可能导致出问题时定位问题更难

(2)运维、治理、监控复杂度提升

4、RPC 框架设计

屏蔽远程调用实现细节。让调用方“像调用本地函数一样调用远端的函数(服务)。屏蔽序列化/反序列以及socket 数据发送和传输细节。RPC框架是架构微服务化的首要基础组件,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节

整体过程:

  • 将请求体按照固定协议序列化成字节流,
  • 发送数据到目标服务,
  • 接收方接收二进制字节流数据,
  • 根据固定协议反序列化字节流成请求体和函数名
  • 本地调用函数得到结果
  • 序列化执行结果成二进制流
  • 将结果字节流发送给调用方
  • 调用方收到结果字节流
  • 反序列化结果字节流得到调用结果

4.1 框架组成

client(提供给调用方使用,屏蔽远程调用复杂度)、server(提供给被调用方使用,屏蔽远程调用复杂度)、以及用来生成 client 和 server 的工具。

client 和 server 都需要的功能有:序列化/反序列,socket 数据发送和传输。

client 还需要的功能:连接池管理、负载均衡、故障转移、队列管理,超时管理、异步管理

server 还需要的功能:服务端组件、服务端收发包队列、io线程、工作线程、上下文管理器、超时管理、异步回调,等

4.2 序列化/反序列化

序列化:将“对象”形态的数据转化为“连续空间二进制字节流”形态数据的过程。

反序列化:序列化的逆过程。将“连续空间二进制字节流”形态数据转化为“对象”形态的数据的过程。

4.2.1 序列化协议

指导序列化和反序列化的规则称之为序列化协议。比如规定报文的哪些字节段用来存储对象名、属性类型、属性值等,分别占几个字节。常见的序列化协议有 xml、json。

4.2.2 序列化协议要考虑的因素

序列化协议需要考虑的最重要的三个因素:转换解析效率、压缩率(转换后数据大小)和可读性。

(1)解析效率:这个应该是序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率就很高

(2)压缩率,传输有效性:同样一个对象,xml/json传输起来有大量的xml标签,信息有效性低,二进制自定义协议占用的空间相对来说就小多了

(3)扩展性与兼容性:是否能够方便的增加字段,增加字段后旧版客户端是否需要强制升级,都是需要考虑的问题,xml/json和上面的二进制协议都能够方便的扩展

(4)可读性与可调试性:这个很好理解,xml/json的可读性就比二进制协议好很多

(5)跨语言:上面的两个协议都是跨语言的,有些序列化协议是与开发语言紧密相关的,例如dubbo的序列化协议就只能支持Java的RPC调用

(6)通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,上面自定义的二进制协议虽然能够跨语言,但每个语言都要写一个简易的协议客户端

4.2.3 业内常见的序列化方式

(1)xml/json:解析效率,压缩率都较差;扩展性、可读性、通用性较好

(2)protobuf:Google出品,必属精品,各方面都不错,强烈推荐,属于二进制协议,可读性差了点,但也有类似的to-string协议帮助调试问题

(3)thrift:比较常用的二进制协议,性能跟 protobuf 差不多。比 protobuf 支持更多的语言

4.3 同步调用/异步调用

这部分沈剑大佬实在是总结的太好了,节选一小部分,强烈推荐去看原文RPC-client异步收发核心细节?

客户端调用又分为同步调用与异步调用

同步调用的代码片段为:

Result = Add(Obj1, Obj2);// 得到Result之前处于阻塞状态

异步调用的代码片段为:

Add(Obj1, Obj2, callback);// 调用后直接返回,不等结果,比同步调用,需要多传一个回调函数
# 处理结果通过回调回调函数得到:
callback(Result){// 得到处理结果后会调用这个回调函数
…
}

4.3.1 同步调用

所谓同步调用,在得到结果之前,一直处于阻塞状态,会一直占用一个工作线程,上图简单的说明了一下组件、交互、流程步骤。

上图中的左边大框,就代表了调用方的一个工作线程。

左边粉色中框,代表了RPC-client组件。

右边橙色框,代表了RPC-server。

蓝色两个小框,代表了同步RPC-client两个核心组件,序列化组件与连接池组件。

白色的流程小框,以及箭头序号1-10,代表整个工作线程的串行执行步骤:

1)业务代码发起 RPC 调用,Result=Add(Obj1,Obj2)

2)序列化组件,将对象调用序列化成二进制字节流,可理解为一个待发送的包packet1

3)通过连接池组件拿到一个可用的连接connection

4)通过连接connection将包packet1发送给RPC-server

5)发送包在网络传输,发给RPC-server

6)响应包在网络传输,发回给RPC-client

7)通过连接connection从RPC-server收取响应包packet2

8)通过连接池组件,将conneciont放回连接池

9)序列化组件,将packet2范序列化为Result对象返回给调用方

10)业务代码获取 Result 结果,工作线程继续往下走

RPC框架需要支持负载均衡、故障转移、发送超时,这些特性都是通过连接池组件去实现的。

4.3.2 异步调用+回调

一般异步调用都直接用消息队列来实现。所以异步的 RPC 调用似乎很少使用。

所谓异步回调,在得到结果之前,不会处于阻塞状态,理论上任何时间都没有任何线程处于阻塞状态,因此异步回调的模型,理论上只需要很少的工作线程与服务连接就能够达到很高的吞吐量。

上图中左边的框框,是少量工作线程(少数几个就行了)进行调用与回调。

中间粉色的框框,代表了RPC-client组件。

右边橙色框,代表了RPC-server。

蓝色六个小框,代表了异步RPC-client六个核心组件:上下文管理器,超时管理器,序列化组件,下游收发队列,下游收发线程,连接池组件。

白色的流程小框,以及箭头序号1-17,代表整个工作线程的串行执行步骤:

1)业务代码发起异步 RPC 调用,Add(Obj1,Obj2, callback)

2)上下文管理器,将请求,回调,上下文存储起来

3)序列化组件,将对象调用序列化成二进制字节流,可理解为一个待发送的包packet1

4)下游收发队列,将报文放入“待发送队列”,此时调用返回,不会阻塞工作线程

5)下游收发线程,将报文从“待发送队列”中取出,通过连接池组件拿到一个可用的连接connection

6)通过连接connection将包packet1发送给RPC-server

7)发送包在网络传输,发给RPC-server

8)响应包在网络传输,发回给RPC-client

9)通过连接connection从RPC-server收取响应包packet2

10)下游收发线程,将报文放入“已接受队列”,通过连接池组件,将conneciont放回连接池

11)下游收发队列里,报文被取出,此时回调将要开始,不会阻塞工作线程

12)序列化组件,将packet2范序列化为Result对象

13)上下文管理器,将结果,回调,上下文取出

14)通过 callback 回调业务代码,返回 Result 结果,工作线程继续往下走

如果请求长时间不返回,处理流程是:

15)上下文管理器,请求长时间没有返回

16)超时管理器拿到超时的上下文

17)通过timeout_cb回调业务代码,工作线程继续往下走

5、问题

问:web层和service层分开部署的好处是什么? 如果有个user web层和 user service层,放到一个project部署,1.减少了rpc网络开销 2.部署机器减半 而且做到无状态,也能水平扩展

功能复用,把通用功能部署成一个单独的服务后,其他 web服务 也可能调用 user service。

问:如何在保证业务快速向前的同时进行服务化改造

继续维护迭代老系统,新功能尽量按照拆分规范来设计开发,逐步把老系统迁移到新架构。

问:被调用方怎么知道调用方调用的哪个接口

RPC 接口实现的。RPC 通信协议里约定好报文的某几个字节用来存接口名,被调用方识别报文后根据报文里指定的接口路由到指定的接口。

问:restful 和 rpc 各自的优势和适用场景

内部服务接口调用:RPC

尤其适用于需要进行大量数据交互的服务(thrift提供高效的压缩协议,交互更加简洁,吞吐量更大)、 高频率交互的服务,减少通信数据传输大小,提高效率,特别是并发高的时候,性能差异尤其明显。

对外提供的服务 :restful HTTP

简单,更安全。更加规范、通用、易扩展、易维护,具有较高的安全性(https)。

问:内部服务可以使用 http 来进行服务间的通信吗

可以,但是效率不如 RPC。

问:微服务如何保证高可用

rpc-server 被调用方集群冗余部署,做好限流,幂等,rpc-client 实现负载均衡、故障转移、超时重发。据此来保证系统的可用性。

问:同步调用和异步调用分别如何实现负载均衡?

都是通过连接池来实现。连接池中建立了与一个RPC-server集群的连接,连接池在返回连接的时候,需要具备随机性。

同步调用跟异步调用的不同在于,同步连接池使用阻塞方式收发,需要与一个服务的一个ip建立多条连接;异步收发,一个服务的一个ip只需要建立少量的连接(例如,一条tcp连接)。

问:同步调用和异步调用如何实现故障转移?

连接池中建立了与一个RPC-server集群的连接,当连接池发现某一个机器的连接异常后,需要将这个机器的连接排除掉,返回正常的连接,在机器恢复后,再将连接加回来。

问:同步调用和异步调用如何实现发送超时?

同步阻塞调用,拿到一个连接后,使用带超时的 send/recv 即可实现带超时的发送和接收。即等待一定时间发送,以及等待多久没收到结果自动返回。

异步调用,需要借助超时管理器来实现,定时扫描上下文管理器中保存的上下文,如果超时则直接执行回调函数,并将这个上下文从上下文管理器中删除,如果超时回调执行后,正常的回包又到达,通过req-id在上下文管理器里找不到上下文,就直接将请求丢弃(因为已经超时处理过了)。

问:如何检测到连接池中的连接发生异常

(1)连接断开,可以捕获到事件;

(2)可以用正常请求包试探;

(3)也可以用单独的线程保活。

问:异步回调方式,工作线程直接把请求传递给下游收发线程就返回,以及下游收发线程拿到响应后立刻发送给工作线程执行回调函数不就行了,为什么还要经过一个接收队列

通过队列将 io 线程和 work 线程需要解耦开来。

问:异步调用里的异步收发队列是每个下游服务所有连接共用一个吗

是的,同一个下游服务所有接口都共用一个下游收发队列。

问:下游收发队列 trace 链路追踪怎么弄

通过 logid 来串联。

问:核心设计出来后,编码实现过程中,作者印象深刻的坑是哪方面

功能方面bug易揪,性能方面bug难找,框架类编码符合这个特点

6、完整参考

互联网架构为什么要做服务化?

微服务架构多“微”才合适?

为什么说要搞定微服务架构,先搞定RPC框架?

微服务架构之RPC-client序列化细节

RPC-client异步收发核心细节?

极客时间胡忠想老师的《从0开始学微服务》课程第一讲“01 | 到底什么是微服务?

猜你喜欢

转载自blog.csdn.net/qq_41767116/article/details/129484203