作者简介
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 门票活动查询引擎微服务治理前后对比
六、总结
微服务架构下服务拆分越细,调用关系越复杂,层级越深,性能损耗越大,开发效率越低(垂直域/小组内),所以服务不是越小越好,而是“合适的大小”。
在构建微服务的时候,要根据业务体量、团队规模、成本等因素综合考虑,按照合理的原则,构建出适合的大小,以达到预期的目标。
服务治理是一个长期的过程,制定目标持续优化,让系统更快更稳定,为业务赋能。
【推荐阅读】
上线效率提升8倍,携程门票活动直连平台实践
每分钟写入6亿条数据,携程监控系统Dashboard存储升级实践
携程海外MySQL数据复制实践
“携程技术”公众号
分享,交流,成长