vlambda博客
学习文章列表

服务化架构增加了哪些复杂度:微服务架构谈(5)


介绍微服务的东西比较多了,有趋之若鹜之势,但任何东西都是按需演进的,服务治理,注册中心由来已久。在SOA还很流行的时候,那会在电信做项目,ESB厂商是比较受宠的,找接口的A、B双方一起讨论接口定义。那会比较困惑的是,这层透传解决了那些问题,说好的服务组装,编排呢?开源的SOFA套件可以作为服务化框架的参加模型

SOFA 中间件是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,包括微服务研发框架,RPC 框架,服务注册中心,分布式定时任务,限流/熔断框架,动态配置推送,分布式链路追踪,Metrics 监控度量,分布式高可用消息队列,分布式事务框架,分布式数据库代理层等组件,也是在金融场景里锤炼出来的最佳实践。


SOFA文档: http://www.sofastack.tech/ 
SOFA: https://github.com/alipay

华为李林峰同学有一本书《分布式服务框架》仍有借鉴意义,找到了之前的一篇夹叙夹议的读后感,以供读者探讨。  


一、概论 

在SOA时代,大家都在讨论服务是什么,以及著名的ESB!而今微服务大行其道,仿佛分布式数据库一致性问题,服务注册与发现、服务治理都是新鲜事物。其实从SOA(ESB)时代到SOA服务化时代,绝大部分命题已经涉及到了,比如阿里开源框架Dubbo就由此而生,而那时微服务这个概念还不为人知。  


分布式服务涉及的几大件,参考Dubbo作者梁飞的博客,服务治理涉及到:

  • 服务注册与发现 

  • 软负载均衡与容错

  • 服务路由

  • 服务编排

  • 服务质量协定

  • 服务降级

  • 服务监控与统计

  • 服务容量管理 

  • 服务分层架构

  • 服务调用链跟踪

  • 故障传导分析

  • 服务测试

  • 服务健康检测

  • 服务容器

  • 服务自动部署

  • 服务资源调度 

  • 服务文档管理、审批流程 


下图为Dubbo服务质量的关系图:

服务化架构增加了哪些复杂度:微服务架构谈(5)


二、关于服务调用

SOA体系下的系统通常由多个相互依赖的应用(App)集群组成,它们之间通过Service进行交互:

服务化架构增加了哪些复杂度:微服务架构谈(5)

图2-1 应用依赖示例


应用与应用间分布式地部署,使得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异步调用

为了提升应用的对外服务执行期间的线程利用率,引入异步化的调用框架:一次服务的调用被映射为两次方法调用(请求调用和回调),两次调用分别映射成远程通信的请求和响应。

服务化架构增加了哪些复杂度:微服务架构谈(5)

图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时间所占整个服务的耗时比例有限),并不适用异步化。 


三、SLA限流的陷阱


如《分布式服务框架》一书第1414.6总结所说,流量控制是保障服务SLA的重要措施,也是业务高峰期故障预防和恢复的有效手段,分布式服务框架需要支持不同的流控策略,还要支持流控阈值、策略的在线调整。作者介绍了静态流控,动态流控的基本思想。 


具体到操作层而言,我们的单机服务器容量一般是针对服务接口粒度。比如A系统有咨询、支付、发放三个主要接口,那么一般而言,在压测的时候有不同的场景组合模型来支持三个接口组合的能力设定,QPS或者TPS。

 

场景组合\接口名

咨询

支付

发放

场景组合1

1000QPS

150TPS

400TPS

场景组合2

800QPS

170TPS

450TPS

场景组合3

600QPS

210TPS

500TPS


如果业务需求判定为重点保障咨询接口,则把咨询设置为最大值比如1000,而支付和发放就会调小,比如支付120TPS。但是如果咨询实际的值大于预期,支付实际又没有预期高,原本期望用于支付的资源可以部分挪用来做咨询,然而现有固定阈值状态下咨询会被限流。于是可以通过动态调整限流策略来做到这一点。期望限流策略同时做到:1保护系统资源 2 保证重点接口的服务能力 3 根据当前的各接口负载,动态调整限流。

 

再谈谈第二个问题,假设支付接口B的单机TPS测定为300TPS,可能商户m的流量占了60%,如果商户m的流量奇高(某日营销活动),那么商户m的流量就会打满300触发限流导致其他商户均支付不成功。解决方案1是在路由的时候就区分好大商户和普通商户,大商户有专门的服务集群,达到隔离的目标。解决方案2是在接口SLA上面做文章,扩展限流组件的能力,可以支持细粒度的限流,可以通过配置商户参数、表达式来实现。下图即是粗放式限流一类管理界面。


服务化架构增加了哪些复杂度:微服务架构谈(5)



四、参数传递的商榷


林峰在12章总结参数传递策略。包括业务内部参数传递、服务框架和业务之间的参数传递。其中业务流程编排涉及参数传递总结了三种方式:


  • 硬编码

  • 通过抽象的编排上下文进行传递

  • 通过专业的BPM流程引擎进行业务逻辑编排,参数通过流程上下文传递


4.1 线程上下文传参规范

在上下文参数传递中,经过使用ThreadLocal进行参数传递。要特别注意的是ThreadLocal参数清理,以及使用ThreadLocal还存在多线程情况下数据混乱的风险。因此在一些编码规范中约定:

  • 本系统内, ThreadLocal变量使用完成后,必须在本系统显式remove。

  • 跨系统,接口服务提供方,不能直接或者间接通过ThreadLocal参数提供给服务消费者使用。

4.2 参数透传风险

透传参数也是我们经常使用的一种方案。透传参数看似简单,也有一些容易犯的错。透传参数最大的问题是没有定义那些参数应该在那些系统消费。比如A>B>C>D…E,A、B、C、D系统存在同步调用关系,D发消息给E。一些上下文的构造在A系统创建,本身应该在D系统消费的,结果在C系统不小心被修改了,那么就发生了超出预期的风险。

 

4.3 非合理使用共享变量

我们来看一个使用扩展字段的例子:

A系统调用B系统的服务,B系统的处理逻辑伪代码如下:

 

//检查checkInfoMap是否有payInfo对象匹配的内容

List<CheckInfo>  pAssertCheckInfos=matchCheckInfos(payInfo,checkInfoMap);

//匹配结果不为空

if(pAssertCheckInfos.isEmpty())

{

    //代码省略

    return;

}

//清空

payInfo.getInfoMap().clear();

这段代码的问题在于进入if分支后,直接返回;没有执行map的清空逻辑。


A系统继续使用了decisionInfo对象,处理逻辑是拿到大字段map以后直接putAll追加到原有入参map中,导致在某种情况下返回map追加到多个入参map中,而产生超出预期的结果。对象共享有几种方式:本地缓存、静态对象、单例对象、单例bean服务的成员变量等。以下介绍一个本地缓存风险介绍的案例。


在大型并发系统设计中,基于现有分布式编码的经验总结出如下原则:任何对象共享涉及都需要杜绝外部变更风险,有如下几种方式基于自身使用场景去考虑哪种更适合:采用深克隆的方式,采用不变对象模式。


五、服务规模的把握


经常有人问,服务拆多大合适?有人说微服务的微究竟是多”微”?林昊在前一段有一篇文章《大部分公司并不需要微服务》,一个单一应用的复杂度远比N个应用组成的分布式系统简单、快速的多;一旦进入分布式的坑,在技术上就不得不有比较大的投入,而对于一些处于中小规模的公司而言,完全没有必要。


我觉得在几十名研发的公司,可能还真的不需要做服务化或者微服务。在支付宝几百研发人员的时候,应该还是2个主要系统,一个前台业务,一个后台系统。


回到多大服务规模合适,可以看看康威定律,系统架构往往和组织架构相关,反之亦然。我们回头看3人一组的小team能cover多少system?如果把system拆分为service(服务),可以再次计算。比如1个人cover 1-2个系统,如果平均1个系统10个服务的话,那么单人管理的服务在20以内的级别;3个小组管理的服务应在100个之内。


服务本身的拆分粒度也要在管理复杂度上做权衡,业务领域的内聚上做权衡。服务变多,链路变长,研发效率反而会下降。我们应尽量降低依赖。


六、服务调用跟踪

众所周知,trace架构基本都源自Google Dapper

服务化架构增加了哪些复杂度:微服务架构谈(5)


下图为高德在基于trace基础上做的场景应用,比如服务状态自我诊断、监控追溯等。


服务化架构增加了哪些复杂度:微服务架构谈(5)


服务化架构增加了哪些复杂度:微服务架构谈(5)

上图为支付宝app通过无线网关的trace示意图,包括应用链路,文件存储。应用链路包括rpc调用和消息服务。《分布式服务框架》一书,林峰特别对服务调用链价值进行了汇总,体现了对于不同角色,服务调用链路跟踪的价值所在。


开发:

架构优化

消除不合理依赖

性能优化

还可以补充容量评测、设计变更分析等


测试:

识别调用流程

优化测试用例

关键路径覆盖率

还可以补充自动化端到端测试


运维:

故障定界

故障定位

提前预警

易故障点识别

 

七、分布式事务

todo

 

声明:

本文部分插图引用网络公开资料,包括高德稳定性架构实践(雷娃)、支付宝无线-从前端到后端的服务治理 以及 阿里妈妈全景业务监控平台Goldeneye。



往期推荐:







   ……


技术琐话 



以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。本号由坐馆老司机技术团队维护。