4

微服务架构下如何通过弱依赖原则保障系统高可用

 1 month ago
source link: https://blog.51cto.com/u_15714439/10511587
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

微服务架构下如何通过弱依赖原则保障系统高可用

精选 原创

京东云开发者 2024-04-18 15:35:13 ©著作权

文章标签 数据 解耦 链路 文章分类 架构 后端开发 阅读数130

当我初次接触高可用这个概念的时候,对高可用的【少依赖原则】和【弱依赖原则】的边界感模糊,甚至有些“傻傻分不清楚”。这两个原则都关注降低模块之间的依赖关系,但它们之间的确存在某些差异。

那么,「少依赖原则」和「弱依赖原则」它们之间本质的区别究竟是啥?

少依赖原则和弱依赖原则都是旨在提高系统的可靠性和稳定性,但是它们之间的本质区别在于对依赖关系的管理控制

1.少依赖原则(Less Dependency Principle) :这一原则强调系统设计阶段的模块独立性,目的是从源头上降低故障传播的风险,通过降低模块之间的耦合度,让各个模块独立完成特定功能,减少不必要的依赖。

2.弱依赖原则(Weak Dependency Principle) :弱依赖原则关注的是在系统运行过程中,如何管理和控制模块之间的依赖关系,以便在某个模块出现故障时,其他模块仍能正常运行。通过实现弱依赖,使得系统具备更好的容错能力高可用性

当然,这两个原则并不是绝对的,两者之间有一定的关联性,我们可以根据系统的复杂性和实际需求,灵活地调整模块之间的依赖关系。在实际应用中,少依赖原则和弱依赖原则也可以相互配合,共同提高系统的高可用性。

一、基于弱依赖原则的架构策略

弱依赖原则:一定要依赖的,尽可能弱依赖,越弱越好 事物a强依赖事物b,一旦b出问题时,那么a也会出问题,一损俱损。 所以任何强依赖都要尽可能的转化成弱依赖,可以直接降低出问题的概率。

1、微服务架构

模块拆分:微服务架构将复杂的应用程序拆分为多个独立的、可组合的服务模块。每个服务具有明确的功能边界和职责,相互之间具备松耦合的特点。这样,当某个服务出现故障时,其他服务可以继续独立运行,保证系统的整体可用性。

独立部署:各个模块有自己独立的代码仓库,可以独立进行分组部署和升级,无需其他模块的配合。当某个服务发生故障或需要升级时,不会影响到其他服务的正常运行。特别针对黄金交易链路上的系统,有条件的话尽量考虑提供独立的数据资源(DB、redis),并进行垂直分组部署。

需要注意的是:部署隔离需要充足的部署资源,以及上下游配合,尽量提前做好该工作。当然,模块隔离必须建立在低耦合的基础上进行才有意义。如果组件之间的耦合关系千头万绪、混乱不堪,模块隔离只会让这种混乱雪上加霜。

2、异步通信

异步通信可以认为是在模块隔离基础上的进一步解耦,将物理上已经分割的模块之间的强依赖关系进一步削弱,使故障无法传播扩散,提高系统可用性。异步在架构上的实现手段主要是使用消息队列,当一个模块发生故障时,另一个模块可以继续处理任务,不会受到太大影响。

以我正在做的职能架构升级项目为例,其中创建员工账号的场景:新员工在PC页面提交创建账号的请求后,需要将员工信息进行数据持久化,并发送创建成功的短信和邮件给员工,此外,还需将员工信息同步给人资系统。

如果用微服务同步调用的方式,那么后续操作任何一个故障,都会导致业务处理失败,员工无法创建成功。我们通过使用消息队列的异步架构,新员工创建时发mq后就立即响应「创建成功」,后续的操作通过消费消息来完成,即使某个操作发生故障,后续再补偿也不会影响员工的创建流程。

微服务架构下如何通过弱依赖原则保障系统高可用_解耦

图1.1 创建员工业务流程图

3、接口抽象

耦合度过高是软件设计的万恶之源,也是造成系统可用性问题的罪魁祸首。一个高度耦合的系统,可谓“牵一发而动全身”,任何微小的改动都可能会引发意想不到的bug和系统崩溃。连最基本的功能维护都已经勉为其难,更不用奢谈什么高可用了。

我们可以通过定义抽象的策略接口,这个抽象接口通常是从多个具有共同特征行为的类中抽象出来的,而具体实现类的指定都交给工厂类去完成,从而实现模块之间的松耦合。这样,当某个模块发生变化时,也不会影响其他模块的正常运行。接口抽象的方式,我将在后文的「实际场景分析」,针对具体案例展开详细的讨论。

4、故障切换与容错

设置完善的故障处理机制,包括故障检测、故障切换和故障恢复。当检测到故障时,系统可以快速切换到备用组件或恢复服务,保证系统的可用性。

数据分片:存储数据时,将其分布在多个存储节点上。当某个存储节点发生故障时,数据可以从其他节点恢复,从而提高数据的可用性和容错性。

读写分离:对于接受弱一致性的场景,将读操作分配给从数据库,写操作分配给主数据库,以提高系统的性能和稳定性,并支持主从切换;数据量大时,也可以进行分库分表处理。

兜底降级:当一个系统中的强依赖服务数量较少时,其整体基础稳定性便会越高。对于那些特殊数据依赖较多而逻辑依赖较少的系统,我们可以采取去依赖的架构设计策略。具体来说,就是将依赖服务数据持久化异构到自己的数据库,并通过异步方式进行同步更新维护,从而降低对其他系统的依赖程度,进一步提升系统的稳定性。然而,这种方法也存在一定的弊端:数据冗余可能导致在特定时间窗口内出现数据不一致的情况。

5、松耦合的业务逻辑

将业务逻辑进行解耦,使其相互独立。例如:目前线下店仓系统中专卖店、大商超、超级大店三个业态是公用一套代码,各个业态之间设计上是松耦合的,不同业态扩展点的实现相互隔离,这样当某个业务逻辑出现故障时,其他业务逻辑可以继续运行,降低故障影响范围。

二、实际场景分析

1、case 1:中间件弱依赖

i、消息队列弱依赖

日常开发中经常会遇到分布式事务的场景,同一个事务内涉及「RPC调用、写DB、对外消息发送」等一系列原子操作,可能存在某一个环节请求异常的情况,为了保证事务的最终一致性,需要采用失败重试策略。对一个简单的应用流程来说,抛出异常业务中断回滚即可;但是对于复杂业务流程是不可行的,发生请求异常时,上游应用可能已经执行完毕,尤其是多个异步流程组合一个整体流程的场景,其他前置的流程可能已经执行完毕,无法回滚。

以店仓生产缺货取消单据的场景为例,店仓拣货环节,若商品全部缺货、或者用户选择“缺货取消订单”的发货策略时,此时拣货缺货,会调用下游接口取消订单。经常会出现前置操作(如RPC调用、写DB等)完成后,调用订单取消接口失败或者异常的情况。调用失败会触发UMP报警,需人工干预进行处理。

微服务架构下如何通过弱依赖原则保障系统高可用_链路_02

图2.1 生产缺货取消回写opc流程

为了避免以上问题,保障数据的最终一致性,初期优化采用mq自产自消的方式,进行重试。

微服务架构下如何通过弱依赖原则保障系统高可用_数据_03

图2.2 JMQ解决回调取消接口失败

这样一来,业务系统的稳定性与JMQ中间件的稳定性就强关联了,自然对JMQ的稳定性有较高要求。为了降低对JMQ的强依赖,保证业务的顺利执行,通过技术手段提升用户体验,减轻研发值班人员压力,最终形成了任务重试工具。

其核心思想是将分布式事务拆分成本地事务进行处理,具体实现方式是:将任务落库,保证业务操作表与纯任务表在同一个数据库,通过数据库事务保证业务操作与任务持久化的强一致性。在一定程度上,将业务操作与中间件依赖解耦。

采用回调函数的机制实现调用者和底层驱动的解耦,提升了组件的灵活性,对业务侵入性小。

微服务架构下如何通过弱依赖原则保障系统高可用_数据_04

图2.3 任务重试组件工作流程

ii、数据库弱依赖

第二个涉及中间件弱依赖的场景是数据库弱依赖,在日常开发中,数据库操作异常的情况屡见不鲜,比如网络链路问题、慢SQL导致的性能下降,以及引发的故障等。这些问题往往会导致交易黄金链路在短时间内无法正常工作,给业务带来不小的损失。为了应对这些情况,我们考虑引入灾备机制,以确保在异常情况下仍能维持较高的交易成功率,保障订单履约时效。

该方案的核心思路是:在DB操作出现故障的时间段内,通过其他存储介质(如redis)临时存储数据。然后,通过MQ异步补偿还原DB操作,从而保障数据的最终一致性。

微服务架构下如何通过弱依赖原则保障系统高可用_解耦_05

图2.4 数据灾备方案

这个方案显著提升了我们应对数据库操作异常的处理能力,确保了黄金交易链路的平稳运行。通过实施灾备策略,我们在确保数据最终一致性的同时,也有效减小了故障对业务的影响。

2、case 2:依赖倒置解耦业务逻辑

微服务架构下如何通过弱依赖原则保障系统高可用_解耦_06

图2.5 定义抽象接口进行解耦

代码层面的依赖优化案例,是基于依赖倒置的设计原则,将业务模块进行解耦。在需求迭代的过程中,我们经常为了图方便,将具体类直接依赖于具体类,也就是所谓的高层模块依赖于低层模块。但是这样是极其不利于扩展的,随着新功能的不断追加,系统的功能会越来越臃肿,核心功能也会越来越模糊,这种情况下,系统的高可用性会受到影响。

请看下面这个案例:历史代码中,执行发货单取消逻辑十分复杂,不同类型、不同来源的单据在不同生产环节取消处理逻辑存在差异,这里的doCandel方法就是高层模块,而调用取消接口和发送取消消息是低层模块,这是一个典型的高层模块依赖于低层模块的编码形式。

ii、优化前的实现方式

以下一个代码片段是系统中的历史代码负债,耦合性强,可读性和可扩展性差。

微服务架构下如何通过弱依赖原则保障系统高可用_数据_07

图2.6 历史代码

iii、优化后的实现方式

弱依赖原则强调应该尽量让模块之间的依赖关系变得弱化。这意味着模块之间的相互作用应该尽量简单,避免复杂的依赖关系。我们优化的核心思想是采用 工厂模式+模板模式 去抽象接口,实现不同环节单据取消后续处理逻辑差异。

a.首先,我们通过定义一个抽象的策略类AbstractDoCancelNodeStrategy,将取消单据后的核心流程进行拆解,最终将拆解的四个步骤定义为四个抽象方法。

微服务架构下如何通过弱依赖原则保障系统高可用_数据_08

图2.7 抽象策略类定义

b.然后,我们创建了4个具体实现策略类,分别用于处理不同环节取消单据的逻辑。主要是提供相同行为的不同实现,业务上可以根据不同条件选择进入不同的实现类。

微服务架构下如何通过弱依赖原则保障系统高可用_链路_09
微服务架构下如何通过弱依赖原则保障系统高可用_解耦_10

图2.8 不同策略实现

c.其次,创建获取不同生产环节取消单据的策略工厂:采用启动时加载策略的方式,在项目启动时,把接口的实现类的实例放在Map里,系统运行过程中,可以通过取消节点对应的key,找到这个实现类的标识,进行相应的逻辑处理。

微服务架构下如何通过弱依赖原则保障系统高可用_数据_11

图2.9 策略工厂

d.这样一来,高层模块可以依赖于这个策略类,而不是具体的策略实现。这样,当业务需求发生变更,需要对新的单据类型进行取消消息广播,只需实现一个新的处理策略类,而无需修改高层模块的代码。

微服务架构下如何通过弱依赖原则保障系统高可用_解耦_12

图2.10 高层模块代码

这样优化后,相当于把高层模块和具体的RPC调用的底层模块逻辑进行了间接解耦。并且提供了对开闭原则的完美支持,可以在不修改主流程代码的情况下,灵活增加新算法。总之,以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此咱们在日常开发中,要多尝试面相接口编程,采用先顶层设计再细节的去设计代码结构。

三、强弱依赖治理

服务依赖是决定系统复杂度的一个重要因素,随着业务的不断迭代,服务依赖可能会变得越来越复杂,导致系统难以维护和扩展。在没有明确强弱依赖的前提下,我们很难进行熔断、降级、限流的相关操作,也不能有效的对系统进行相关优化改造、持续推进系统稳定性提升。因此,服务依赖治理变得至关重要。我们需要定期检查我们的依赖模型是否合理,识别不合理的依赖将其合理化。具体的治理流程包括:

1、依赖标记:通过人工梳理代码的形式,对系统核心链路上的所有依赖进行梳理,分析并标注依赖关系及强弱。

2、强弱依赖验证:采用混沌工程等方式模拟链路故障,核心思路就是不断给系统“找麻烦”来验证系统能力,模拟某个依赖服务出现故障的场景,从而验证人工标注的有效性。

3、依赖治理:依赖治理的目标体现在如下几个方面:

•筛选出那些不是真的强依赖的部分,将其转换为弱依赖,实现强依赖最少化。

•对强依赖进行解耦合,建立核心链路的降级预案,并对预案持续保活。

•对弱依赖做合理的异常捕获逻辑,配置合理的超时、熔断以及限流。针对具体的业务场景,以场景为最小单位,编写止损可控的兜底逻辑,并配置相应的动态切换开关,当异常发生时,可一键切换至兜底逻辑。

•弱依赖支持平滑停用,支持突发场景下舍军保帅。

当然,除了通过以上措施去构建遵循弱依赖原则的高可用系统,还有一些高可用的架构方案:

比如,在架构设计时,我们还需要考虑到不同层级的异常监控(业务层、应用层、中间件层、基础层),数据采集包含日志、埋点、链路追踪等,数据告警通过电话、短信、邮件、京me等方式通知到值班人员。通过建立完善的监控体系,实时收集系统运行状态,并进行预警。这样,当系统出现潜在故障时,可以及时发现并采取措施进行修复。

此外,对于改造量比较大的新业务上线后,可以通过ducc控制灰度切流的方式,降低软件编码错误带来的影响。观察没有问题,再全量切流,保证即使程序有Bug,也可以流量切回,产生的影响也控制在较小的范围内。这样的系统在面对故障时,具有更强的容错能力和抗故障能力,才能确保系统整体运行的稳定性和可用性。

  • 收藏
  • 评论
  • 分享
  • 举报

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK