作者简介

HongLiang,携程高级技术专家,专注系统性能、稳定性、承载能力和交易质量,在技术架构演进、高并发等领域有丰富的实践经验。

一、背景

微服务架构在中大型互联网公司中被广泛应用,随着业务的发展,应用数越来越多、调用关系也越来越复杂。中台化后,交易系统要支持业务线多,系统复杂性高,原系统虽然能支撑业务量的持续增长,但在稳定性、吞吐力和资源利用率上面,还存在优化空间。

分享的目的

本文站在业务开发角度介绍开发在微服务架构下遇到的相关问题(微服务架构的优缺点这里不再赘述),以门票活动预订流程查询引擎为例,分享微服务治理的实战经验,希望能给遇到同样问题的同学提供一些借鉴思路。

如下图所示,蓝色部分为本文的重点

图1 微服务架构关注点

在微服务治理之前,我们先简单了解一下微服务历史和陷阱。

二、微服务简史

微服务概念在2005年被提出,2011年用来代表架构风格。

定义:微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成,每个微服务仅关注于完成一件任务。

2.1 微服务与SOA的关系

大家在使用SOA (Service-Oriented Architecture)的时候往往分不清和微服务有什么关系,总结如下:

  • 微服务是SOA的实现方式

  • 微服务是去掉ESB后的SOA

  • 微服务是一种和SOA相似但是两种不同的架构设计风格

图2 微服务与SOA的关系

了解微服务与SOA关系后,再对比一下功能差异:

表1 微服务与SOA对比

2.2 携程SOA从1.0到 2.0演进

图3 携程SOA演进

携程微服务:携程SOA2.0是微服务架构,推荐单机、单应用、单服务。

三、微服务的陷阱

微服务这个话术会将关注点错误的聚焦在“微”上,大家会误以为服务越小越好,实际上大小并不是第一考虑因素。接下来我们来看看开发微服务应用的时候容易踩到的陷阱。

下图可以看出,服务拆分越细,调用关系越复杂。

调用链路理论上有 n * (n-1) 条:

图4 服务粒度越细调用关系越复杂

应用粒度拆分过细容易带来以下几个问题:

3.1 重复调用

调用路径 C – >D ->E 和 C ->E, 对于E的一次请求,可能会被调用了多次。

图5 一次请求中服务E被重复调用

3.2 循环依赖

一条链路出问题,导致其他链路故障。当服务B1或B2 性能变差时,最终导致链路A/B都会被影响,严重情况下导致宕机。

图6 循环依赖

3.3 链路太长

服务层级过深,一次请求链路太长会导致性能下降,每层网络延时和序列化反序列化时间都有性能损失,层级越深,下游性能越差。

链路太长,定位问题困难(效率低),当服务F出现故障时,下游A~E 应用 owner 需要排查原因。

图7 链路越长,性能损失越大

以上这些问题,在日常开发中容易遇到,下面我们看看怎么解决这些问题。

四、微服务治理

从下图中可以看到应用之间调用关系复杂,并且有严重的循环依赖问题。

图8 应用调用关系图(双黄线表示循环依赖)

循环依赖是微服务里面容易忽视的问题,系统稳定的情况下不会出现问题,由于某些原因,当系统从稳定变成非稳定状态时,循环依赖容易导致更严重的故障。我们先看1个生产案例:

案例:发布过程中下游超时,订单下跌

刚接入流量的机器因线程初始化、类加载锁、JIT等会产生慢请求。

图9 发布过程中的慢请求

当流量接入时,请求在刚拉入的机器中多次来回调用,因多次慢请求叠加,导致接口越来越慢,机器资源耗尽,一台一台被拖垮,最终整个服务不可用,产生雪崩(如下图)。

图10 发布过程中循环依赖导致应用雪崩

当然如果应用间循环依赖QPS很小,例如单机QPS在10以内,少量慢请求无法将资源耗尽,一般不导致故障,但是这种“坏味道”会给系统埋下隐患,严重的时候会演变为接口级的循环依赖,导致死循环,并且这种死循环可能在测试环境由于命中缓存没有被发现,发布到生产后有些缓存穿透的请求就会导致循环调用,直到超时;如果单机QPS上百,产生的慢请求短时间内耗尽资源,阻塞后续请求,导致性能下降,产生故障。

故障恢复期间,由于调用关系复杂,分不清上下游关系,无法根据调用关系来限流,导致定位困难,恢复时间长。

上述案例主要是由循环依赖引起,像一颗炸弹,为系统埋下隐患。

除了循环依赖,还有下面几类问题可以优化:

1)层级太深:

  • 透传字段要改多个应用,需求迭代效率低

  • 每层网络延时、序列化和反序列化都有性能损失,导致终端体验差

2)重复缓存:同一个DB不同应用重复构建缓存

3)流量大:

  • 重复调用,直接调用或者间接调用,末尾服务压力大

  • 离线任务峰值波动太大

4)未隔离:核心、非核心流量未隔离

5)效能低:人均应用多/资源使用率低

针对上面的几类问题,我们制定了微服务治理目标、原则和治理策略。

4.1 治理目标

1)稳定:故障隔离,提升系统稳定性

2)交付:独立迭代、独立扩展、快速交付

横向拆分:减少耦合,独立迭代。

纵向拆分:减少应用层级,提高开发效率,缩短交付周期。

3)重用:相同功能复用

不同系统重复功能复用,减少重复开发,提升一致性。

4.2 治理原则

1)避免跨团队维护一套代码。

2)服务粒度要与团队规模匹配,人均应用数在3个以内。

根据历史经验,一个人在超过3个应用之间来回切换开发,开发效率会降低,日常处理告警繁琐,业务和性能优化也无法聚焦。

3)应用分层:上一层可以依赖任意下一层级(不可反向依赖)。

4)层级深度:垂直域/小组内,应用层级控制在5层以内。

这里的“5层”是我们根据实际业务实际情况来定的。一个垂直域/小组内应用层级超过5层,一个需求上下游依赖太多,开发效率会降低。

4.3 构建原则

1)业务领域拆分:单一职责,业务建模(对人员要求高)

2)数据存储:独立的数据读写API

3)复用性:功能复用(比如基础数据提供能力,提供给不同小组使用)

  • 可靠性

  • 核心与非核心隔离

4)稳定规则与易变动规则隔离

5)快速失败:设置合理的熔断规则

6)异步通信:将与此次请求无关的操作/调用异步化

4.4 治理策略

1)去除循环依赖

问题:服务B和服务C 循环依赖

策略

  • 应用分层与定位:第一步划分应用层级(分层工具有传统三层架构、泛领域分层等),将应用定位划分到不同的层级。

  • 确认依赖关系:每一层内如果有多个应用,确认上下游关系。这个根据业务场景来,根据父子关系,包含关系,依赖关系,确认每一层内的依赖关系和应用职责。

图11 循环依赖治理

2)缩短调用链路

问题:服务BCD 链路太长(垂直域/小组内)

策略

  • 领域细分:将粗粒度的应用按照业务领域垂直划分,不同层级负责不同的职责,让系统更独立。

  • 减少透传:每个层级职责清晰,减少不必要的透传,让开发效率更高。

图12 缩短调用链路

3)复用性

问题:服务BCD 对相同数据重复缓存(存在一致性问题)

策略

下沉基础服务,提供基础数据:将相同的功能下沉为基础服务,例如:基础数据服务提供缓存,翻译等功能。避免不同的使用方重复缓存,重复接入翻译。

图13 重复功能下沉

效果:下沉基础数据服务,统一缓存,翻译等功能,提供给不同的开发组使用。

4)流量治理

a) 重复调用

问题:一次请求,服务C同一个接口被重复调用

策略

功能内聚:将同一个功能对下游的依赖放到同一个服务内调用。由于系统自身迭代导致的不合理调用,可以按照上述方法优化。如果为了解耦将功能拆开,可以根据实际情况评估影响和收益。

图14 功能内聚合并重复调用

效果:功能内聚,多次调用合并为一次调用。

b) 降低调用量

问题:一个服务中,不同的接口功能拆分太细,下游使用的时候都需要调用多个接口组装结果。例如:一次请求服务B的a、b、c、d、e接口都被调用,下游为实现一个功能,需要调用太多小接口。

策略

  • 合并服务B中同一领域功能:将相同的功能合并到一个接口,减少调用量。

  • 一个接口提供模块参数,按需调用:

  • 支持按需使用,对不同业务场景非必须的功能,提供模块参数,按需传参。

  • 对于独立的前端页面接口,对外透明,内部封装对应场景需要的模块参数,例如前端首屏请求。

图15 请求合并

效果:聚合相同功能,合并小接口,多次调用合并为一次调用。

c) 流量隔离

问题:非核心流量(例如:Job调度)大于用户流量

策略

流量隔离:一套代码,隔离部署,将核心和非核心流量隔离。核心流量承载用户请求,保证交易的稳定性,非核心流量承载离线任务调度和非核心场景调用。

图16 流量隔离

效果:总成本不变,核心链路稳定性得到提升,非核心链路CPU使用率得到提升。

d) 离线调度流量消峰

问题:单位时间内调度过于集中(Job)

策略

合理的延长调度时间:适当延迟调度时间,降低每分钟的调用峰值,让每分钟内调用量更加平稳。

图17 离线调度流量消峰

效果:调度总时间在可接受范围内,调度时间拉长,单位时间内调用总量降低,降低服务端峰值压力。

问题:每秒内调度不均衡(Job),导致服务稳定性差或为了能承载请求需要冗余更多服务器资源。

图18 客户端调度QPS不均衡

策略

客户端削峰填谷:调度波动太大,会导致请求到了服务端被限流或者服务端扩缩容。对于调度不均衡的离线任务,我们在客户端控制每秒内发送的请求量,让每秒内请求更加平稳,任务调度总时间不变。

图19 客户端调度从不均衡变为均衡

效果:分钟内总的调用量不变,服务端调用量从波动变为平稳。

5)降低人均应用数/提升CPU使用率

问题

  • 人均应用过多,开发效率降低

  • CPU使用率6%以下应用数占比超过50% 且总核数占比超过30%

策略

  • 短期:缩容,将单边服务器数缩容到SRE标准最小配置。

  • 长期:合并拆分过细的应用,参考历史、现状和将来的规划,将拆分过细、CPU使用率长期小于6%的应用做合并。

图20 应用合并

五、实施效果

1)循环依赖(应用分层,解除应用间循环依赖)

  • 去掉65条循环依赖链路,消除雪崩的风险

  • 超时类告警降低99%

  • 排障效率提升至分钟级别

2)链路长(减少应用层级):调用链深度缩短 40%

3)复用性(下沉基础数据服务,减少重复功能)

  • 新增基础数据服务,缓存统一,解决一致性问题

  • 缓存容量减少60%

4)流量治理(降低水位线)

  • 重复调用:功能内聚,去除重复调用

  • 调用量大:合并小接口、消除调用峰值;离线任务削峰填谷,降低峰值调用量

  • 核心应用调用量减少73%,核心系统峰值降低50%

5)开发效率(解耦&减少中间层)

  • 水平拆分独立功能,减少耦合,独立开发

  • 垂直领域减少3层,开发效率提升

6)查询引擎性能提升65%,QPS从8w提升至24w

  • 减少了系统不稳定导致的服务变慢

  • 领域划分,垂直优化系统,专注用户端到底层的优化

7)人均应用:人均应用数控制在2个以内

8)资源使用率(应用合并,提升CPU使用率)

  • 40+个应用CPU使用率(加权平均)从18%提升至32%

  • 治理前后查询引擎链路对比:

图21 门票活动查询引擎微服务治理前后对比

六、总结

微服务架构下服务拆分越细,调用关系越复杂,层级越深,性能损耗越大,开发效率越低(垂直域/小组内),所以服务不是越小越好,而是“合适的大小”。

在构建微服务的时候,要根据业务体量、团队规模、成本等因素综合考虑,按照合理的原则,构建出适合的大小,以达到预期的目标。

服务治理是一个长期的过程,制定目标持续优化,让系统更快更稳定,为业务赋能。

【推荐阅读】

  • Islands Architecture(孤岛架构)在携程新版首页的实践

  • 上线效率提升8倍,携程门票活动直连平台实践

  • 每分钟写入6亿条数据,携程监控系统Dashboard存储升级实践

  • 携程海外MySQL数据复制实践

“携程技术”公众号

分享,交流,成长