介绍微服务的东西比较多了,有趋之若鹜之势,但任何东西都是按需演进的,服务治理,注册中心由来已久。在SOA还很流行的时候,那会在电信做项目,ESB厂商是比较受宠的,找接口的A、B双方一起讨论接口定义。那会比较困惑的是,这层透传解决了那些问题,说好的服务组装,编排呢?开源的SOFA套件可以作为服务化框架的参加模型。
SOFA 中间件是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,包括微服务研发框架,RPC 框架,服务注册中心,分布式定时任务,限流/熔断框架,动态配置推送,分布式链路追踪,Metrics 监控度量,分布式高可用消息队列,分布式事务框架,分布式数据库代理层等组件,也是在金融场景里锤炼出来的最佳实践。
SOFA文档: http://www.sofastack.tech/
华为李林峰同学有一本书《分布式服务框架》仍有借鉴意义,找到了之前的一篇夹叙夹议的读后感,以供读者探讨。
一、概论
在SOA时代,大家都在讨论服务是什么,以及著名的ESB!而今微服务大行其道,仿佛分布式数据库一致性问题,服务注册与发现、服务治理都是新鲜事物。其实从SOA(ESB)时代到SOA服务化时代,绝大部分命题已经涉及到了,比如阿里开源框架Dubbo就由此而生,而那时微服务这个概念还不为人知。
分布式服务涉及的几大件,参考Dubbo作者梁飞的博客,服务治理涉及到:
- 服务注册与发现
- 软负载均衡与容错
- 服务路由
- 服务编排
- 服务质量协定
- 服务降级
- 服务监控与统计
- 服务容量管理
- 服务分层架构
- 服务调用链跟踪
- 故障传导分析
- 服务测试
- 服务健康检测
- 服务容器
- 服务自动部署
- 服务资源调度
- 服务文档管理、审批流程
下图为Dubbo服务质量的关系图:
二、关于服务调用
SOA体系下的系统通常由多个相互依赖的应用(App)集群组成,它们之间通过Service进行交互:
应用与应用间分布式地部署,使得Service通常以RPC的形式表现。RPC的底层网络通信与Service的方法调用间的相互转换构成了服务调用框架的主要职责。
2.1同步调用
同步服务调用是最常见的一种服务调用方式:Service的调用一一对应为方法调用,方法调用又一一映射为底层网络的一次请求响应。以图2-1的一次完整调用中,AppCurrent所在进程的处理过程示例:
- 对外发布自己的服务SerivceA。该服务的实现为AppCurrent的一个方法,服务的参数、结果映射为方法的形参和返回值;
- 接收到上游AppUp发起的服务A调用,底层通信组件反序列化之后得到对应的方法、实参,此时在一个执行线程上开启对该方法的调用;
- 方法内部实现逻辑又包含了对下游两个分布式服务B和C的先后两次调用,同样通过通信组件序列化为发送给AppDown1/2的两次网络请求/响应;
- 处理完服务A对应方法的内部逻辑之后,方法返回,其结果由通信组件将方法返回结果对象序列化为AppCurrent到AppUp的响应包。
该过程中可以发现,AppCurrent从接收客户端发起远程服务调用请求,到服务A实现方法返回整个过程,至少占用一个执行线程。这个线程占用期间,部分工作量为服务实现方法的本地处理逻辑,但也经历了两次对下游系统的调用,等待下游返回期间该线程处于阻塞状态,阻塞期间该线程不能被别的服务调用复用,只能“专心地”等待下游。对于很多路由型的服务,其ServiceB/C的耗时占据了ServiceA耗时的很大比例,使得ServiceA的有效线程利用率极低(大多数时间都在等待下游)。
2.2异步调用
为了提升应用的对外服务执行期间的线程利用率,引入异步化的调用框架:一次服务的调用被映射为两次方法调用(请求调用和回调),两次调用分别映射成远程通信的请求和响应。
如图2-2的一次完整调用,ServiceA被拆解成了methodA和callbackA两次跨线程的调用,ServiceB亦然。AppCurrent所在进程的处理过程示例:
a. AppCurrent接收到来自上游AppUp的服务调用,通信层框架将其反序列化为AppCurrent的方法(e.g. methodA)以及实参,获取一个执行线程(线程1)发起methodA的方法调用;b. methodA在执行线程上完成方法内部逻辑,此时需要调用ServiceB;
c. 异步调用框架将ServiceB的请求调用(methodB)序列化为发送给AppDepend1的网络请求,此时请求调用返回,不等待响应结果;
d. 由于appDown1此时未返回,AppCurrent对ServiceB的请求调用虽然返回,但无法获知SerivceB的结果,所以其请求方法调用的返回结果为null;
e. 随着对ServiceB请求调用的返回,上层的methodA也随之返回(结果仍为null),线程1释放;
f. 但此时SerivceA的执行过程仍在继续(等待AppDown1返回结果进行后续的逻辑),其结果无法返回给AppUp,所以methodA的返回并不会触发AppCurrent到AppUp的通信层响应,此时(等待AppDown1的过程)没有占用执行线程;
g. 等待一段时间之后,AppDown1返回了ServiceB的调用结果。AppCurrent的通信框架层将该结果反序列化,获取一个执行线程(线程2),该线程上触发ServiceB的回调callbackB,将ServiceB的返回结果通过回调的实参传入;
h. callbackB由AppCurrent实现,其逻辑包含了ServiceB完成后的后续逻辑,典型的逻辑如结果转换、DB持久化等;
i. 线程2随后将ServiceA的结果通过callbackA传入调用框架,后者将其序列化为ServiceA的响应包通过通信框架发送给AppUp,线程2释放。
整个异步化调用过程:
- AppUp =(SerivceA)=>AppCurrent =(ServiceB)=> AppDepend1
被拆解成了:
- AppUp -(methodA)->AppCurrent -(methodB)->AppDepend1
- AppUp <-(callbackA)-AppCurrent <-(callbackB)-AppDepend1
两个独立线程的操作,两者间的AppDepend1处理过程(ServiceB等待响应过程)不消耗执行线程。对于ServiceB耗时比例高的场景,异步化调用极大地提升了执行线程有效使用率。但异步化策略也带来了复杂度,体现在以下方面:
- 服务的处理逻辑被拆解,下游调用前后的逻辑在同步调用实现下由相同的functionScope变成了不同的scope,原有的方法本地变量需要应用层保存以及在回调时匹配。
- 应用自身的aop相关功能失效:原有同步调用的很多方法被拆解成了空返回值调用+跨线程回调,拦截器不再能够获取到方法的返回值。
- 原本单线程传递的服务治理相关信息(e.g. traceId)被拆解到了多个线程,框架需要跨线程地维护这些治理相关信息
对于下游rpc密集型应用如网关,这些复杂度是值得的。但对于大多数的DB密集型/cpu密集型应用,异步化的引入的复杂度所能提升的线程利用率有限(下游rpc时间所占整个服务的耗时比例有限),并不适用异步化。



