在面对单体架构存在的问题并决定进行微服务架构改造时,车贷系统必须应对一系列技术和非技术挑战。微服务架构的引入通常伴随着一些门槛,这对整个团队的技术水平和协作能力都提出了一定要求。在接下来的讨论中,我们将重点探讨微服务改造中的各种技术挑战以及可能的对策。
确切而合理的服务边界的设定是确保微服务架构成功的关键。在考虑服务划分时,以下是一些关键原则:
- 符合团队结构: 服务的实施和维护依赖于执行团队,包括业务、产品、技术、测试和运维团队。因此,服务的设定应该与团队结构相协调。不同的执行团队可能提出不同但合理的服务划分方案。
- 业务边界清晰: 每个服务都应该有清晰的责任和边界,即一个服务对应一个具体的业务。服务之间的依赖关系应该是单向的,确保责任的清晰分离。
- 最小化地变更: 新增或变更业务应该有明确的服务对应。服务的设计应该避免摸棱两可的情况,确保在满足前述条件的前提下,受到影响的服务尽可能少,以降低变更的风险。
- 最大化地复用: 服务的设计要考虑到复用的可能性,最大程度地实现服务复用。这是微服务的一个重要优势,可以通过定义通用的服务来促进跨团队和系统的复用。
- 性能稳定简洁: 除了业务导向的考虑外,服务的设定还需关注技术方面。这包括对性能的影响、稳定性、架构的简洁性以及是否引入额外的中间件等。确保服务设计不仅满足业务需求,还具备良好的技术特性。
综合来看,服务划分不仅仅是技术决策,还需深刻理解业务需求并与团队结构相协调。合理的服务划分将有助于提高团队的协作效率,降低变更的风险,并最大程度地发挥微服务架构的优势。
那么我们应该怎么做才能满足这些要求呢?下文笔者就带领各位一同探讨下这个问题。
1. 以业务、技术和团队的角度来规划服务
我们需要清楚地认识到,服务的划分不是追求细粒度的划分越好,而是首先以业务域为基础进行拆分,然后结合技术视角,考虑团队规模和能力来明确定义服务之间的关系和边界。
image.png对车贷系统来说,尽管上图业务服务单元的相对原子化可能是理想的,但在实际拆分时需要考虑业务和团队的复杂性。以下是对这两个方面的问题的更深入探讨:
- 业务上的考虑:
- 一需求一个服务原则: 确实,维持一个需求影响一个服务的原则有助于实现快速版本迭代。然而,在贷款流程中,贷款申请、审核、放款是相互依赖的,一个需求可能需要跨足这三个服务。这可能导致服务之间的高度耦合,增加了处理变更的复杂性
- 领域边界的重新考虑: 可以重新审视贷款流程的领域边界,尝试在领域边界上找到更合理的划分,以减少一个需求对多个服务的影响。这可能需要一定的业务流程优化和重新设计,以更好地匹配微服务的原则。
- 团队上的考虑:
- 成员规模和技能水平: 考虑到团队规模和技能水平的限制,过于细粒度的服务拆分可能导致开发和维护的困难。团队成员可能需要同时涉及多个服务,增加了团队的协作成本。
-
合理权衡: 在拆分服务时,需要权衡服务的粒度。可以采用渐进式的拆分策略,先从业务较为独立的领域开始,逐步细化。确保每个服务都是可管理和可维护的,并适应团队的规模和技能水平。
我们重新划分下边界:
image.png
我们划分成四个服务:基础服务包含客户信息管理、合作商渠道管理及用户权限管理,所有贷款流程相关的都放在贷款服务,还款、催收等划到贷后服务,数据分析独立成一个服务。这样服务边界更清晰了,但是有如下几个问题:
- 所有服务都会依赖用户权限管理
- 登录、注册、催收等都需要发短信
- 所有系统都要文件上传
可见存在一些服务是公共的,针对这个情况我们可以再以技术视角做垂直拆分:
image.png
尽管我们已经在业务域和技术域上做了简单的服务边界划分,看似符合要求,但从架构的全局观上仍存在较大问题。主要问题在于对业务规划的理解不够深刻。随着业务的成熟,存量客户的维系和新客户的拓展将成为关键焦点。通常情况下,会引入配套的精确化营销系统(或类似系统),同时数据化经分系统将成为核心,为各类决策提供数据支持。然而,当前的架构并未清晰体现出业务系统的边界。因此我们再次修正下:
image.png
团队的整体能力是要考虑一个重要因素,一般而言团队的整体能力与服务的数量成正比,反之极容易导致架构失控。
2. 领域检查
在当前看来,我们已经取得了一定的进展,系统的构成以及各系统内的服务边界更加清晰。然而,我们需要谨慎行事,因为在实际复杂的业务环境中,涉及多条产品线、多个项目团队协作研发时,某些功能可能在不同产品中都显得可行。在这种情况下,业务边界的划分变得相当棘手。
为了解决这一问题,我们可以引入领域模型。利用领域模型为服务的业务划分提供指导是一个很好的起点。在国内,有一些优秀的实践,例如COLA等。
领域驱动设计:DDD(Domain-driven design)是一套综合软件系统分析和设计的面向对象建模方法。详细链接
领域建模对服务的划分有非常重要的指导意义,即使我们不用DDD也应该多少了解领域模型设计,举一个例子:某国内知名的垂直电商公司的CTO公开讲他们订单与优惠券的设计演进,第一个版本核心结构如下:
image.png
这一版很简单也很好理解,问题在于它将优惠券与店铺铺绑定,但优惠券有针对商品的、店铺的和平台的,比如X商铺A商品5折、X商铺满100减20,平台满200减20等,上面的结构明显不符合要求,所以他们改成如下结构:
image.png
做法是将订单拆分成针对商品、店铺及平台(支付)的三类,一个支付订单可以包含一个或多个店铺订单,一个店铺订单可以包含一个或多个商品订单,问题解决了吗?的确从功能上看是满足了,但这种做法的后患是订单与优惠券完全绑定了,订单被优惠券绑架了,如果业务上又出现了针对不同类目的优惠(这很常见)那是不是又要加入类目级订单?
image.png
引入一张订单优惠券的关联表(Order Coupon)即可,核心域只关注订单,各类活动的处理在运营活动域中操作。
就这么简单?是的,从领域处理上就是这样,但要支持我们的需求需要有些特殊的处理,在关联表要引入事务ID,同一次操作事务ID相同。
领域检查可以为我们的服务划分提供方向性的指导,使服务划分更明确、各有规划性。
3. 依赖DAG检查
image.pngDAG在数学上是有向无环图,指从任何一点出发都不会回到这个点,即不存在环路,我们服务的依赖也应如此。服务间要尽量避免双向或循环依赖,否则可能会导致灾难性的后果。
上图提到的是服务架构中的一种层次结构,其中服务4作为公共服务,为上层的业务服务(服务1、服务2、服务3)提供支撑。在良好的服务架构中,确实应该避免公共服务对业务服务的直接依赖,以确保服务的独立性和可维护性。
image.png
在微服务架构中,同层服务之间的相互依赖是一个容易被忽视但又非常重要的问题。当服务之间存在双向依赖时,一个服务的变更可能会波及到多个服务,导致修改、测试和部署的复杂性急剧增加。这种情况尤其容易发生在服务之间形成复杂的依赖网络时。
image.png
上图是在项目改造前的服务依赖图中,存在严重的双向依赖问题,直接导致了自动化部署困难、需求响应缓慢等多方面的挑战。一个设计精良的系统通常会采用服务分层的方式,将业务层与基础能力层分离。在这种设计中,业务层位于系统的顶层,而基础能力服务位于底层。上层服务可以调用底层服务,但底层服务不依赖于上层服务,因此各服务之间不存在相互依赖的问题。
错误处理依赖关系可能导致以下问题:
- 响应速度变缓慢: 当服务之间存在双向依赖时,一个需求的变更可能波及多个服务,导致对变更的响应速度变得缓慢。
- 系统可用性下降: 由于无法有效制定降级方案,双向依赖可能使一个服务的问题影响整个系统的可用性。缺乏有效的容错机制可能导致系统无法在部分服务故障时正常运行。
- 架构难以扩展: 双向依赖可能导致整体架构变得难以扩展。一个服务的变更可能引发系统中多个服务的变动,使得公共服务难以抽象,业务服务难以进行重构。
具体到车贷系统可能存在的情况是贷款和贷后服务中存在一定双向依赖:
image.png
贷款申请时先做前置判断:如果该顾客尚有未完成的贷款则拒绝本次申请,在催收巡检时需要获取已放款的订单来确定要处理的数据,很不幸的是它们彼此存在于两个服务中。解决的方法有三种:
- 直接查询数据库而不是调用服务 这是最直接暴力的做法,但带来的问题也很明显,如果操作的业务很复杂会导致代码冗余,即两个服务都要存有相同的代码,另外从服务隔离的角度看也不很合理
- 做数据冗余 以上例而言,在贷中服务放款完成后将这一数据同步给贷后服务,在贷后服务中冗余一份,这样后续做催收巡检时就不需要再请求贷中服务了,当然这会带来数据同步处理的需求
!比较有争议的问题:服务间的数据库是否需要独立?
主张独立的观点认为所有调用都应该是服务接口调用,不应该直接查询非本服务所属的数据表,这样才能最大化地实现能力复用,也最能保证服务解耦,反对的观点认为这带来过多的资源消耗及精力投入,现实情况存在比较多的跨服务查询,比如贷后服务中查询用户还款列表时就需要用到贷款服务的贷款信息表,过分地强调隔离会使系统设计更为复杂,得不偿失。
笔者在此不评论两个观点的是非,在实践中,笔者一般采用系统间数据隔离(分库),系统内各服务数据共享(同一库)。
-
增加抽象层 架构设计中绝大部分问题都可以通过增加一层解决,这也笔者认为最优雅的方法。如下图,我们可以增加公共业务服务这一层,在其上实现订单服务,这样就上述的需求而言通过订单服务实现了完全的解耦
image.png
完成DAG检查后我们的服务划分演进成了如下形式:
image.png
我们为信贷系统增加了数据服务层,添加了三个数据服务,分别提供对金融产品、申请用户、订单的数据操作。
⚠ 增加抽象层从单纯的架构层面上无疑是最优雅的,但这也增加了服务维护难度,必须要结合团队能力综合考虑,如果团队配置相对单薄那么直接查接口方案短期内可能更为适合。
4. 分布式事务检查
-
分布式事务成本高昂: 在分布式系统中,由于各种原因,实施跨服务的分布式事务通常会导致性能和复杂性上的负担。因此,服务拆分时应尽量避免跨服务事务,而是考虑其他替代方案。
-
合并服务的可能性: 在服务设计中,应当优先考虑服务的合并,以避免不必要的跨服务事务。合并服务有助于简化系统架构,减少分布式事务的需求。
-
TCC和基于MQ的柔性事务: 在无法避免分布式事务的情况下,可以考虑采用 TCC(Try-Confirm-Cancel)或基于消息队列的柔性事务。这些方法可以降低事务的一致性要求,减少对性能的负面影响。
-
TCC替代2PC: TCC是一种替代2PC对性能影响很大的的方案,尽管它提供了更灵活的事务管理方式,但开发成本相对较高。在采用TCC时,需要确保各方服务能够同步支持Try、Confirm和Cancel操作。
-
异步消息和补偿性事务: 针对无法控制的第三方服务,采用基于消息队列的异步消息和补偿性事务是一种有效的策略。这种方式通过将事务操作转换为消息,并在需要时执行补偿操作,实现了松耦合的分布式事务管理。
总体而言,这样的设计考虑更加注重系统的弹性和性能,通过选择合适的事务模型以及采用异步消息机制,有助于在分布式环境中降低事务的复杂性和开发成本。车贷系统目前基本基于MQ和钉钉告警结合的补偿性事务。
5. 性能分布检查
对于特别耗资源的操作应尽量独立。比如上文提到车贷系统使用了bcrypt(一种基于Blowfish算法的散列函数,类似MD5,但Hash时极为消耗CPU),导致系统注册服务的TPS严重下降,这时就应该考虑把这个签名操作对独立成服务,为这一服务部署更多节点,并且可以为其独立购买计算优化型云主机。
车贷系统需要近实时地同步GPS追踪器厂商的GPS轨迹数据,这一操作本应归属于贷后服务的贷后数据采集,但由于此操作对TPS、IO要求极高,会占用贷后服务绝大部分的资源,可重要性却次之,即使此功能临时下线对主体业务也不会有太大影响,所以有必要独立成服务,因此我们的服务划分又有新的变化:
image.png
6. 稳定性检查
一个服务中如存在稳定和不稳定的模块,应该将两者拆分。笔者曾为某个母婴电商首页配置做改造优化。此系统有很多运营活动,在不同时期,需要整个首页根据运营策略进行变化。之前方案是每个版本变化,投入大量开发改造首页,导致成本高不能急时响应,运营活动无法快速开展。配置变化中寻找不变的点,就是组件大部分可重用,比如类目块,商品块,活动块,轮播条等。为此为此在设计把其独立成Widget服务,它的变更不会影响核心服务。而是运营通过配置页面的组合搭配就可以完成运营策略首页配置。
车贷系统在运营过程中,我们注意到一些接入的服务商,特别是短信服务商和三要素验证供应商,响应速度较慢且不够稳定。现代软件研发中,我们有许多第三方服务可供选择,这使得我们可以快速高效地构建服务。然而,随之而来的问题是如何有效地管理和监控这些服务。
举例来说,对于短信服务,国内存在许多供应商,它们提供了各种各样的服务。由于政策和运营商策略的影响,没有一家短信供应商可以保证100%的触达率。因此,在项目中我们常常会选择接入多个供应商,并采用一定的策略来确保最大化触达率。
对于其他关键流程,比如电子合同和人脸比对,它们在贷款流程中扮演着不可或缺的角色。由于这些服务的可用性对于我们的业务至关重要,我们采取了多供应商备份的策略,并保留了自己的调用记录。这样一来,我们可以在一个供应商出现问题时迅速切换到备用供应商,确保服务的连续性,尽量减少对业务的影响。
解决方案
- 独立封装服务: 将三方服务独立封装,使其对外提供统一的接口。这有助于隔离服务变化,提高服务的可替换性和可维护性。
- 内部处理差异: 在服务内部处理不同三方服务之间的接口和规则差异。这可以通过适当的抽象和封装来实现,确保上层业务服务不受底层三方服务的影响。
- 自动切换备用服务: 实现自动切换到备用三方服务的机制。这可以通过实施健康检查、监控服务可用性,并在主服务不可用时切换到备用服务来实现。
-
服务与业务解耦: 将这些服务设计为与业务无关的公共服务。这意味着它们提供的功能是通用的,可以在多个业务场景中重复使用,提高了服务的通用性和可重用性。
所以这些三方服务都应该独立,服务对外提供相对统一的接口,服务内部去对接不同的三方服务,消化不同三方服务在接口、规则上的差异,确保一个三方服务不可用时可以自动切换到备用三方服务上。并且这些服务与业务无关,非常适合封装成公共服务,故我们的服务划分又可修改成:
image.png
7. 调用链检查
服务间调用有IO消耗且不易追踪,应控制调用链路的长度。以笔者的经验,一般的请求—响应类操作应该在4层以内比较合适,比如:应用服务网关——>业务服务——>(业务)数据服务——>公共服务。
当然调用链路的长短也要看情况,比如风控系统(见下图),它的一次风控决策最多就需要6层调用,请求从网关Gateway路由到流量控制器(Flow Controller),流量控制器负责在合适的时机发送请求事件到决策处理器(Decision Processor),决策处理器根据决策规则要求发布因子获取事件给因子服务(Factor),因子服务返回对应的因子数据给决策处理器以进行规则运算并异步回调业务请求方。但这一流程中如果因子数据不存在则因子服务要先请求数据采集分发器(Collect Dispatcher),数据采集分发器分发要采集的数据给对应的采集器,采集完成后逐级返回。这一系统通过MQ实现完全异步化处理,多层调用是为服务解耦,符合上述服务划分的要求,同时又因为是异步回调的方式,对实时性要求不高,所以这样的调用链路是可接受的。
image.png
服务的划分是微服务设计的第一步,也是成功实斥的关键。架构设计不应该仅仅关注技术层面,人的因素,包括团队和项目的特定因素,往往更为重要。原则为我们提供了大的方向,但具体的实践需要根据实际情况来调整。这就需要架构师在设计过程中具备灵活性和判断力,根据具体的情况进行度的把握。架构师的能力和经验将在如何平衡原则和实际需求之间进行权衡时得到考验。
因此,微服务设计不仅仅是技术的问题,也是一个关乎人、团队、项目和业务的问题。通过灵活机动的方法,并在实践中不断学习和调整,我们能够更好地应对变化和挑战,构建出适应性强、可维护的微服务架构。
网友评论