微服务设计原则——高可用

本文详细阐述了微服务设计中的限流、熔断、降级、过载保护等概念,强调了简单可靠的原则,以及如何通过CAP定理和BASE理论实现分布式系统的高可用性。文章介绍了熔断器的工作原理,以及在面对故障时如何采取适当的策略,如幂等设计和故障自愈,以确保系统的稳定性和一致性。
摘要由CSDN通过智能技术生成

1.依赖故障

在分布式系统中,被依赖的服务发生故障,无法正常响应请求时常发生。为了保证高可用,需要采取一系列策略和措施来减少依赖故障对自身服务的影响。

1.1 降级

降级是一种在面对特殊业务或异常情况时保持系统可用的策略。当依赖的服务不可用时,退而求其次提供一些基本功能或返回预设的默认值,以确保系统依然能够提供有限的功能或服务;又或者某些特定活动场景(例如双十一)下优先保障计算资源投入到 业务倾向的服务,降级边缘服务。

大部分服务是如下结构,既要给上游使用,又依赖于下游提供的第三方服务,中间又穿插了各种业务逻辑,这里每一块都可能是故障的来源。

如果第三方服务挂掉怎么办?我们业务也跟着挂掉?显然这不是我们希望看到的结果,如果能制定好降级兜底的方案,那将大大提高服务的可靠性。

比如我们做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序,但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在本地 cache 里放置一份热门商品以便兜底。

又比如做一个数据同步的服务,这个服务需要从第三方获取最新的数据并更新到 MySQL 中,恰好第三方提供了两种方式:

  • 一种是消息通知服务,只发送变更后的数据。
  • 一种是 HTTP 服务,需要我们自己主动调用获取数据。

我们一开始选择消息同步的方式,因为实时性更高,但是之后就遭遇到消息迟迟发送不过来的问题,而且也没什么异常,等我们发现一天时间已过去,问题已然升级为故障。合理的方式应该两个同步方案都使用,消息方式用于实时更新,HTTP 主动同步方式定时触发(比如 1 小时)用于兜底,即使消息出了问题,通过主动同步也能保证一小时一更新。

1.2 冗余

通过在不同的地理位置或服务器上部署多个冗余组件,确保即使上游服务出现故障,系统也能继续运作。

可以使用多个数据源、服务器或服务实例,以便在一个出现故障时,其他实例仍然可以提供服务。

冗余需要配合故障转移

在被调出现故障时,自动或手动将流量转移到备用系统或服务,确保系统的持续运行。

配置负载均衡器和故障转移机制,使系统能够检测故障并将请求转移到健康的备份服务。

1.3 自研

在关键被调服务无法满足需求时,自主开发和维护关键组件,以降低对外部服务的依赖。

构建自有的服务或组件,以替代外部服务的功能,确保系统在被调服务不可用时能够独立运行。

2.超时时间

对依赖的内部、外部系统和各种基础组件的超时时间设置,需要一个合理的超时时间。

如果超时时间设置过长,系统长时间不返回,请求积压,可能会导致服务过载,系统挂掉。如果超时时间设置过短,可能会出现大量请求超时,系统的可用性会降低。

此外,如果你的服务超时时间设置得短,重试次数设置得多,会因为重试增加系统的整体耗时;如果超日时间设置得短,重试,次数设置得也少,那么这次请求的返回结果会不准确。

所以需要为请求设置合理的超时时间,根据业务需求和依赖服务的性能进行调整,防止请求无限等待或大于依赖服务自身的耗时时间

  • 【强制】主调用方必须设置超时时间,且调用链路超时从上往下递减。建议超时时间的设置如下:
服务类型建议超时时间(ms)
DB50-100
Redis50-100
MQ50-100
公司内部服务500
公司外部服务3000-5000
其他参考P99耗时
  • 【强制】服务间访问必须通过命名服务,方便服务发现控制下游节点和超时时间。
  • 【推荐】调研被依赖服务自身调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。

3.失败重试

被调失败了,一定要重试吗?

如果不管三七二十一直接重试,这样是不对的。比如有些业务返回的异常表示业务逻辑出错,那么你怎么重试结果都是异常;又如有些异常是接口处理超时,这个时候就需要结合业务来判断了,有些时候重试往往会给被调方造成更大压力,雪上加霜。所有失败重试要有收敛策略,必要时才重试,并做好限流处理。

注意接口重试的风险

当大系统被拆分成多个微服务,微服务之间大量RPC调用,经常可能因为网络抖动等原因导致RPC调用失败,这时候使用重试机制,可以提高请求的最终成功率,减少故障影响,让系统运行更稳定。

虽然重试能够提高服务的稳定性,但是一般情况下大家都不会转经易去重试,或者说不敢重试,主要是因为重试有放大故障的风险。

首先,重试会加大直接下游的负载。如下图,假设 A 服务调用 B 服务,重试次数设置为r(包括首次请求),当 B 高负载时很可能调用用不成功,这时 A 调用失败重试 B,B 服务的被调用量快速增大,最坏情况下可能放大到 r 倍,不仅不能请求成功,还可能导致 B 的负载继续升高,甚至被流量打挂。

在这里插入图片描述

更可怕的是,重试还会存在链路放大效应,结合下图说明一下:
在这里插入图片描述
假设现在场景是 Backend A 调用 Backend B,Backend B 调用 DB Frontend,均设置重试次数为 3 。如果 Backend B 调用 DB Frontend,请求 3 次都失败了,这时 Backend B 会给 Backend A 返回失败。但是 Backend A 也有重试的逻辑,Backend A 重试 Backend B 三次,每一次 Backend B 都会请求 DB Frontend 3 次,这样算起来,DB Frontend 就会被请求了 9 次,实际是指数级扩大。假设正常访问量是 n,链路一共有 m 层,每层重试次数为 r,则最后一层受到的访问量最大,为 n * r ^ (m - 1) 。这种指数放大的效应很可怕,可能导致链路上多层都被打挂,整个系统雪崩。

  • 【强制】与被调方沟通接口在失败(超时)情况下,能否重试,最好由被调在回包告知是否可以重试。
  • 【强制】重试需设置最大重试次数,一般是重试三次。
  • 【强制】重要操作进行重试时,梳理请求处理的整个链路,保证重试收敛在一个地方,不然可能造成指数级放大。
  • 【建议】重试策略可以考虑带有退避策略,如固定间隔重试、指数退避等,避免频繁重试导致被调负载变大。

4.幂等设计

所谓幂等(Idempotent),指对接口的多次调用和调用一次所产生的结果是一致的。

接口需要幂等设计的主要原因是确保在不同条件下的重复请求或意外的重复操作,不会出现意外结果或数据损坏。

重复请求很容易发生,比如用户误触,超时重试等。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果时网络异常(超时成功),此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

在设计幂等接口时,通常会使用一些标识符或令牌来确保相同请求不会被重复处理。这可以通过在请求中包含唯一标识符或使用状态管理来实现。幂等设计是分布式系统和服务设计中的一个重要原则,有助于提高系统的可靠性、稳定性和一致性。

  • 【强制】核心流程写接口,设计的时候需要考虑幂等性的需求,比如超时重试和异常的重复请求。
  • 【强制】对接口进行幂等性测试,确保在重复请求的情况下,接口的行为符合幂等性要求。
  • 【建议】使用唯一事务标识符:对于可能重复执行的操作(如下单操作、查看报表等),使用一个唯一的事务标识符(例如请求ID或幂等键)来识别重复的请求。服务器可以跟踪已处理的事务 ID,并对相同 ID 的重复请求返回相同结果。

5.限流

限流(Rate Limiting)是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。

现实生活中,处处都有限流的实际应用,就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。

限流可用于保护下游服务。

当下游服务的处理能力有限时,限流可以避免送自身服务发送过多请求至下游,防止下游服务(如数据库、微服务、第三方API)崩溃或出现严重性能下降。

限流亦可用于保护自身服务。

同样地,如果我们的服务资源有限、处理能力有限,就需要对调用我们服务的上游请求进行限制,以防止自身服务由于资源耗尽而停止服务。

常见的场景有:

(1)请求限制。下游有严格的请求限制。比如银行转账接口,微信支付接口等都有严格的接口限频。

(2)保护下游。调用的下游不是为高并发场景设计。比如提供异步计算结果拉取的服务,并不需要考虑各种复杂的高并发业务场景,提供高并发流量场景的支持。每个业务场景应该在拉取数据时缓存下来,而不是每次业务请求都过来拉取,将业务流量压垮下游。

(3)突发流量控制。例如在电商促销活动、抢购、票务抢购等场景,用户请求量会在短时间内急剧增加,导致服务器负载骤增。限流可以防止服务器因高并发而崩溃。

常用的限流算法有漏桶算法和令牌桶算法。必要的情况下,需要实现分布式限流。

关于限流的实现方式参见:服务高可用利器 —— 限流算法介绍与示例

6.熔断

何为熔断?

如果依赖的服务出现故障:一直超时或者一直抛出异常,那在一段时间内就没有必要调用这个服务了,这就是熔断。

熔断即停止对下游访问或切换到备用服务,可以减少对下游施加的压力,也可以防止下游故障向上游传导。

为何要熔断?

在分布式架构中,一个服务通常会与多个外部服务进行交互,这些外部服务可能是RPC接口、数据库、第三方 API 等。例如,在支付过程中,可能需要调用银联提供的 API;而查询某个商品的价格,则可能需要进行营销活动查询。然而,除了自身服务外,依赖的外部服务不可能 100% 可靠。

当依赖的第三方服务出现异常情况时,例如第三方服务过载,会导致调用第三方服务的响应时间变长,甚者形成级联效应。这样一来,服务自身的请求可能会积压,最终可能耗尽自身资源,导致自身服务不可用。

如何应对这种情况?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的服务也需要装上“保险丝”,以防止非预期请求压垮系统。

服务的保险丝就是熔断器(Circuit Breaker)。

熔断器可以帮助系统在依赖的下游服务出现问题时保证整个系统的高可用,减少对下游的压力,防止故障进一步扩散。同时也能在一段时间后重新尝试恢复正常操作。避免局部不稳定因素导致整个分布式系统雪崩。

如何实现熔断?

熔断的实现通常包括监控、熔断器、降级和自动恢复等机制。

重点说一下熔断器的工作原理。

熔断器分为三个状态:关闭状态、开启状态和半开状态。

  • 关闭状态(Closed)

允许调用远程服务,记录调用的状态和延迟等信息。

  • 开启状态(Open)

当调用失败率达到一定阈值后,熔断器会进入开启状态,停止调用远程服务,直接返回失败的响应。

  • 半开状态

开启状态下,每隔一段时间,熔断器会进入半开状态,允许部分请求通过,以测试远程服务的可用性。

如果测试成功,则熔断器进入关闭状态,否则重新进入开启状态。

一般不需要自己实现一个熔断器,可以使用常见的熔断组件,主要有奈飞的 Hystrix 以及阿里的 Sentinel,两种互有优缺点,可以根据业务的实际情况进行选择。

7.过载保护

限流(Rate Limiting)是通过控制请求的处理速率,限制系统在特定时间窗口内接收和处理的请求数量,以避免系统过载。

当系统已经过载时,那么需要做过载保护,防止服务过载引发雪崩。

相信很多做过高并发服务的同学都碰到类似事件:某天 A 君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。

当流量过大,服务过载时,可以采取「拒绝」或者「引流」等措施。

过载保护的具体做法:

  • 请求等待处理超时

比如把接收到的请求放在指定的队列中排队处理,如果请求等待处理超时了(假设是 100ms),这个时候直接拒绝超时请求。再比如队列满了之后,清除队列中一定数量的排队请求,保护服务不过载,保障服务高可用。

  • 服务过载及早拒绝

根据服务当前指标(如 CPU、内存使用率、平均耗时等)判断服务是否处于过载,过载则及早拒绝请求并带上特殊错误码,告知上游下游已经过载,应做限流或熔断处理。

  • 引流

当系统过载时,自身只做轻量的处理逻辑,然后将流量转发给性能更高的系统去做处理,或者完全不处理,只做一个代理转发。

8.快速失败

遵循快速失败原则,一定要设置超时时间。

假设一个第三方接口正常响应时间是 50ms,某天该第三方接口出现问题,大约有15%的请求响应时间超过 2s。没过多久我们的服务负载飙生到 10 倍以上,响应时间也变得很慢,即第三方服务将我们的服务拖垮了。

为什么会被拖垮?没设置超时!我们采用的是同步调用方式,使用了一个线程池,该线程池里最大线程数设置了50,如果所有线程都在忙,多余的请求就放置在队列里中。如果第三方接口响应时间都是 50ms 左右,那么线程都能很快处理完自己手中的活,并接着处理下一个请求,但不幸的是如果有一定比例的第三方接口响应时间为 2s,那么最后这 50 个线程都将被拖住,队列将会堆积大量请求,从而拖垮整服务。

正确的做法是和第三方商量确定一个较短的超时时间比如 200ms,这样即使他们服务出现问题也不会对我们服务产生很大影响。

9.无状态

尽可能地使微服务无状态。

无状态服务,可以横向扩展,从而不会成为性能瓶颈。

状态即数据。如果某一调用方的请求一定要落到某一后台节点,使用服务在本地缓存的数据(状态),那么这个服务就是有状态的服务。

我们以前在本地内存中建立的数据缓存、Session 缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。

10.最少依赖

能不依赖的,尽可能不依赖,越少越好。

减少依赖,便可以减少故障发生的可能性,提高服务可靠性。

任何依赖都有可能发生故障,即使其如何保证,我们在设计上应尽可能地减少对第三方的依赖。如果无法避免,则需要对第三方依赖在发生故障时做好相应处理,避免因第三方依赖的抖动或不可用导致我们自身服务不可用,比如降级兜底。

11.简单可靠

可靠性只有靠不断追求最大程度的简化而得到。

乏味是一种美德。与生活中的其他东西不同,对于软件而言,“乏味”实际上是非常正面的态度。我们不想要自发性的和有趣的程序;我们希望这些程序按设计执行,可以预见性地完成目标。与侦探小说不同,缺少刺激、悬念和困惑是源代码的理想特征。

因为工程师也是人,他们经常对于自己编写的代码形成一种情感依附,这些冲突在大规模清理源代码的时候并不少见。一些人可能会提出抗议,“如果我们以后需要这个代码怎么办?”,“我们为什么不只是把这些代码注释掉,这样稍后再使用它的时候会更容易。”,“为什么不增加一个功能开关?”,这些都是糟糕的建议。源代码控制系统中的更改反转很容易,数百行的注释代码则会造成干扰和混乱;那些由于功能开关没有启用而没有被执行的代码,就像一个定时炸弹等待爆炸。极端地说,当你指望一个Web服务7*24可以用时,某种程度上,每一行新代码都是负担。

法国诗人 Antoine de Saint-Exupéry 曾写道:“不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美”。这个原则同样适用于软件设计。书写一个明确、简单的 API 是接口可靠的保证。我们向 API 消费者提供的功能和参数越少,这些 API 就越容易理解。在软件工程上,少就是多!一个很小很简单的 API 通常也是一个对问题深刻理解的标志。

软件的简单性是可靠性的前提条件。当我们考虑如何简化一个给定的任务的每一步时,我们并不是在偷懒。相反,我们是在明确实际上要完成的任务是什么,以及如何容易地做到。我们对新功能说“不”的时候,不是在限制创新,而是在保持环境整洁,以免分心。这样我们可以持续关注创新,并且可以进行真正的工程工作。

12.分散原则

鸡蛋不要放一个篮子,分散风险。

比如一个模块的所有接口不应该放到同一个服务中,如果服务不可用,那么该模块的所有接口都不可用了。我们可以基于主次进行服务拆分,将重要接口放到一个服务中,次要接口放到另外一个服务中,避免相互影响。

再如所有交易数据都放在同一个库同一张表里面,万一这个库挂了,此时影响所有交易。我们可以对数据库水平切分,分库分表。

13.隔离原则

控制风险不扩散,不放大。

不同模块之间要相互隔离,避免单个模块有问题影响其他模块,传播扩散了影响范围。

比如部署隔离:每个模块的服务部署在不同物理机上;

再如 DB 隔离:每个模块单独使用自身的存储实例。

古代赤壁之战就是一个典型的反面例子,铁锁连船导致隔离性被破坏,一把大火烧了80W大军。

隔离是有级别的,隔离级别越高,风险传播扩散的难度就越大,容灾能力越强。

例如:一个应用集群由N台服务器组成,部署在同一台物理机上,或同一个机房的不同物理机上,或同一个城市的不同机房里,或不同城市里,不同的部署代表不同的容灾能力。

例如:人类由无数人组成,生活在同一个地球的不同洲上,这意味着人类不具备星球级别的隔离能力,当地球出现毁灭性影响时,人类是不具备容灾的。

14.故障自愈

没有 100% 可靠的系统,故障不可避免,但要有自愈能力。

人体拥有强大的自愈能力,比如手指划破流血,会自动止血,结痂,再到皮肤再生。微服务应该像人体一样,当面对非毁灭性伤害(故障)时,在不借助外力的情况下,自行修复故障。比如消息处理或异步逻辑等非关键操作失败引发的数据不一致,需要有最终一致的修复操作,如兜底的定时任务,失败重试队列,或由用户在下次请求时触发修复逻辑。

15.版本兼容

向旧兼容

向旧兼容(Backward Compatibility),也称为向后兼容,是指在软件系统或接口更新时,确保新版本的系统或接口能够正常运行旧版本的功能或与旧版本的客户端进行交互。向旧兼容性设计对于保持系统的稳定性和用户体验至关重要,尤其是在大规模分布式系统中,向旧兼容性可以防止新版本导致的业务中断或用户操作失败。

以下是关于向旧兼容性设计的一些原则和方法:

1)接口兼容性

新增功能:在接口更新时,尽量以增加新功能的方式而不是修改或移除旧功能。例如,在REST API中可以通过新增API路径或增加新的请求参数来实现新功能,而不影响现有接口。

默认值支持:如果必须增加新的参数或字段,确保它们具有合理的默认值,使旧版本的客户端无需更改即可继续使用。

版本控制:引入版本控制机制,如在API路径中包含版本号(如 /api/v1/resource),确保旧版本的客户端可以继续访问原有的API版本,而不会受到新版本更改的影响。

2)数据格式兼容性

结构化数据:在数据格式(如JSON、XML)中新增字段时,确保旧版本的客户端可以忽略未识别的字段而不出错。例如,在JSON响应中增加新字段时,旧版本的客户端应能忽略这些新字段。

数据格式转化:如果数据格式发生了变化,提供兼容性的转换层,确保旧版本的客户端仍能正确解析数据。

保持字段的语义:避免改变已有字段的语义和类型。例如,不要将字符串字段改为数字字段,这样可能会导致旧客户端的解析失败。

3)数据库兼容性

数据库迁移:在进行数据库结构变更时,采用迁移脚本逐步演进数据库结构,确保新旧版本的代码都能正常操作数据库。避免一次性大规模修改数据库表结构。

保持旧字段:新增字段或表时,尽量保留旧字段或表,直到所有旧版本的代码都不再依赖这些旧结构为止。可以在代码中标注即将废弃(deprecate)的字段,但暂时不要移除。

兼容性索引:在数据库中创建新的索引时,考虑到旧版本代码对索引的依赖,避免移除旧的索引结构。

4)配置文件兼容性

配置文件版本化:在配置文件中增加版本号,并且在新版本中保持对旧版本配置文件的兼容解析。例如,如果增加了新的配置项,确保在没有配置该项时使用默认值。

配置项保留:保留旧的配置项,即使新版本不再使用,也不要直接移除,确保旧版本系统能够继续使用相同的配置文件。

5)兼容性测试

回归测试:在新版本发布前,执行回归测试,确保新版本的代码不会破坏旧版本的功能。

自动化测试:建立向后兼容性的自动化测试,模拟旧版本客户端的行为,验证其在新版本环境下的兼容性。

灰度发布:在新版本发布时,采用灰度发布的方式,逐步将流量引导到新版本,并监控兼容性问题。

6)文档与沟通

版本发布说明:在发布新版本时,提供详细的版本发布说明,明确新版本的变更内容以及对旧版本的兼容性。

Deprecation 通知:当某个功能或接口即将被废弃时,提前通过文档或公告通知用户,给出合理的迁移周期。

开发者指南:为开发者提供向后兼容性设计的最佳实践和指南,确保在系统设计与开发过程中保持兼容性意识。

7)向旧兼容的实际应用

API 开发:在开发REST API时,通过版本控制、默认值、保持字段语义等手段,确保API更新不会影响旧版本客户端。

数据库结构调整:在数据库结构调整中,通过增量式迁移、保留旧字段、创建兼容性索引等方法,确保旧版本代码能够正常运行。

软件升级:在企业系统中进行软件升级时,通过灰度发布、自动化兼容性测试等手段,减少升级对业务的影响。

8)小结

向旧兼容性设计确保了在系统不断演进的过程中,旧版本的功能和用户体验能够得到维护。通过版本控制、数据格式兼容、数据库迁移、配置文件管理等多种手段,可以有效地避免新版本引入的问题对现有系统的影响,从而提高系统的稳定性和用户的满意度。

向新兼容

向新兼容也称为向前兼容(Forward Compatibility),是指指系统、接口或协议在设计时考虑到未来的版本更新,使得当前版本能够与未来的版本兼容。这意味着即使未来的版本发生变化,当前的系统或客户端仍然能够正确地理解或处理这些变化,而不需要进行重大修改。

  • 【强制】兼容上游扩展。账户类型、品类、字段取值等,自身服务需要做到兼容,并在非法输入时,编写防御性代码来处理这些情况,做好容错。
  • 【建议】兼容性字段:在数据库设计中保留一些扩展字段或列,用于未来可能的功能扩展。

16.数据一致性

数据同步与对账

在分布式系统中,数据一致性是指不同节点之间的数据在一段时间内保持同步和一致的状态。由于分布式系统中存在网络延迟、节点故障等问题,如何保证数据的一致性成为一个重要的挑战。

根据CAP理论,分布式系统必须在一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)之间进行权衡。

  • 【强制】凡是数据同步,必须有对账机制。建立自动化巡检工具,定期执行对关键模块的审计和回顾。
  • 【强制】数据需要需要满足最终一致,可通过兜底脚本来实现。
  • 【强制】关键数据状态要集中化管理,避免多个系统分别维护相同状态(如使用容器本地缓存导致不一致),推荐使用中央数据库或分布式数据管理系统统一管理关键数据状态。
  • 【强制】采用可靠的消息队列(如 Kafka、RabbitMQ)或事件驱动架构,确保关键数据状态的实时同步。
  • 【建议】数据最终一致,需要根据业务场景,考虑业务容忍度,比如容忍数据不一致的时间长度和数据一致性对用户体验的影响。
  • 【建议】实施异常监控和预警机制,及时发现和处理关键数据状态未更新/更新失败的问题。

数据时效性

数据时效问题通常指的是如何确保系统中数据在各个组件和系统之间保持最新和一致,同时在延迟、数据同步等方面不会影响用户体验。

  • 【强制】数据缓存或兜底数据,一定要有更新机制。如基于时间的 TTL(Time-To-Live)和基于事件的失效机制,确保数据不过时。
  • 【建议】在系统启动或服务恢复时刷新缓存或兜底数据,使数据有机会重新初始化。
  • 【建议】时效性监控。有主动发现数据已过期的机制,并设置告警阈值,及时发现和解决时效性问题。

17.防御性编程

防御性编程(Defensive Programming) 是软件开发中的一种编程思想和方法,旨在通过在代码中引入额外的防护措施来提高软件的稳健性和可靠性。

通过防御性编程,开发者可以预防或减少因意外输入、错误条件或不符合预期的环境引发的错误,确保系统在各种情况下都能正常运行。

防御性编程的核心原则和实践:

1)输入验证

检查输入的有效性:对所有外部输入(用户输入、文件、API调用等)进行严格验证,确保输入数据的格式、范围和类型符合预期。
避免注入攻击:通过验证和清理输入,防止SQL注入、XSS攻击等安全漏洞。

2)异常处理

捕获并处理异常:使用try-catch块捕获可能的异常,并提供合理的处理方案,如记录日志、返回友好错误信息或执行补救操作。

定义自定义异常:为特定业务场景定义自定义异常,使错误处理更加清晰和具有针对性。

3)边界条件检查

边界检查:在处理数组、集合、字符串等数据结构时,检查边界条件,防止越界访问。
避免溢出:在处理数值计算时,检查是否可能发生溢出或下溢,并采取相应措施防止意外结果。

4)容错设计

使用默认值:当输入或外部资源不可用时,使用合理的默认值,确保系统继续运行。
降级处理:在关键功能无法执行时,提供简化版本或降级服务,避免系统完全失效。

5)断言和预条件

使用断言:在关键路径或重要逻辑中使用断言来验证代码的前置条件,确保程序状态符合预期。
明确前后条件:在函数或方法中,确保输入参数(前条件)和输出结果(后条件)符合函数的设计约定。

6)设计时假设最坏情况

悲观思维:假设外部系统可能返回错误、不可靠的数据或在关键时刻不可用,并设计系统以应对这些情况。
资源管理:确保对资源(内存、文件句柄、网络连接等)的正确分配和释放,防止资源泄漏或死锁。

7)接口与契约
明确接口契约:定义和遵守API的输入、输出和异常处理契约,确保调用方和实现方有一致的理解。
封装和隔离:通过封装和模块化设计,减少各组件之间的依赖,防止错误在系统中传播。

8)日志记录
记录关键操作:在程序中记录重要的操作、输入数据和异常情况,以便后续调试和问题排查。
日志级别控制:合理设置日志级别(如INFO、WARN、ERROR),避免生产环境中记录过多无关信息,影响系统性能。

9)慎用外部依赖
验证外部依赖:对第三方库、服务和组件进行严格验证,确保其行为符合预期,并能够处理其可能的错误情况。
降级或备用方案:当外部依赖不可用时,提供降级方案或备用实现,确保系统关键功能继续运行。

10)测试与验证

单元测试:通过全面的单元测试覆盖所有代码路径,验证代码的正确性和鲁棒性。
模拟与仿真:通过模拟各种错误场景(如网络故障、数据库不可用),测试系统在异常情况下的行为。

防御性编程的优点:
提高系统稳定性:通过主动防范可能出现的错误和异常,系统在各种情况下都能保持稳定。
增强安全性:防御性编程有助于防止常见的安全漏洞,如注入攻击和未授权访问。
便于维护和扩展:通过清晰的契约、输入验证和异常处理,系统的维护和扩展变得更加容易。

防御性编程的挑战:

代码复杂性:过度的防御性代码可能导致代码臃肿,难以阅读和维护。因此,需平衡防御性和代码简洁性。

性能开销:额外的检查和验证可能增加系统的性能开销,需在设计时权衡性能与鲁棒性。

防御性编程是一种思维方式,它要求开发者在编写代码时始终考虑到系统可能遇到的各种异常情况,并提前设计好应对措施,以确保系统的稳定性和安全性。

18.异常处理

在系统开发中,处理接口的异常分支是确保系统稳健性和用户体验的关键部分。异常分支处理不仅需要识别和响应错误,还要确保系统能恢复正常状态,避免数据不一致和其他问题。以下是接口异常分支处理的关键策略和步骤:

  • 【强制】技术方案中要有依赖接口异常场景的处理预案,明确接口字段异常的处理情况,以便事前确认和事后对照。
  • 【强制】编码上采用不信任编码,对依赖服务返回的数据进行校验,确保数据的完整性和正确性,包括检查空数据、未识别的枚举值、数据格式错误(尽量)等。常见的操作:if分支一定要有else,switch一定要有default。
  • 【强制】实现错误重试机制,设置合理的重试次数和间隔时间,避免频繁重试引发依赖服务更大的负载。注意,重试的对象必须是幂等支持重试的接口,且需要在系统中最靠近数据上游的地方重试,以免造成重试风暴。
  • 【强制】建立监控系统,配置异常告警,实时监控请求的成功率、失败率、响应时间等指标,增加关键指标的电话告警,增强处理及时性。
  • 【强制】错误链路等异常分支必须至少有一条错误日志的详细信息打印,包括异常类型、时间、请求参数等信息。
  • 【强制】对于接口使用的固定流程或特殊场景,应当在协议中明确指出,告知使用方,可减少不必要的沟通和风险。
  • 【强制】数据兼容性问题,应当在设计时充分考虑。防止需要使用或处理到历史数据时,逻辑对应不上需要额外处理。
  • 【建议】实现降级兜底策略,当依赖服务异常时,根据接口是否为主流程强依赖,决策当前请求的回包逻辑。如提供替代方案(如兜底返回缓存数据)、或默认值、或默认文案(通知用户系统繁忙稍后重试)等。注意,默认值不适用于千人千面的接口(如用户获取自己的持仓信息)。
  • 【建议】实现熔断策略,当依赖服务频繁出现异常时,通过命名服务暂时停止对其请求,避免对下游服务造成更大压力。
  • 【建议】定期演练依赖服务接口异常的各项场景,增加对外部依赖不可用情况的故障注入回归验证流程。
  • 【建议】外部依赖变化时应主动检查自身应用的异常分支处理是否适配。

19.CAP 定理

2000 年,加州大学伯克利分校的计算机科学家 Eric Brewer 在分布式计算原理研讨会(PODC)上提出了一个猜想,分布式系统有三个指标:

一致性(Consistency)
可用性(Availability)
分区容错性(Partition tolerance)

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标最多只能同时实现两点,不可能三者兼顾,这便是著名的布鲁尔猜想。

在随后的 2002 年,麻省理工学院(MIT)的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,使之成为一个定理,即 CAP 定理。

CAP 定理告诉我们,如果服务是分布式服务,那么不同节点间通信必然存在失败可能性,即我们必须接受分区容错性(P),那么我们必须在一致性(C)和可用性(A)之间做出取舍,即要么 CP,要么 AP。

如果你的服务偏业务逻辑,对接用户,那么可用性显得更加重要,应该选择 AP,遵守 BASE 理论,这是大部分业务服务的选择。

如果你的服务偏系统控制,对接服务,那么一致性显得更加重要,应该选择 CP,遵守 ACID 理论,经典的比如 Zookeeper。

总体来说 BASE 理论面向的是大型高可用、可扩展的分布式系统。与传统 ACID 特性相反,不同于ACID的强一致性模型,BASE 提出通过牺牲强一致性来获得可用性,并允许数据段时间内的不一致,但是最终达到一致状态。同时,在实际分布式场景中,不同业务对数据的一致性要求不一样,因此在设计中,ACID 和 BASE 应做好权衡和选择。

20.BASE 理论

在 CAP 定理的背景下,大部分分布式系统都偏向业务逻辑,面向用户,那么可用性相对一致性显得更加重要。如何构建一个高可用的分布式系统,BASE 理论给出了答案。

2008 年,eBay 公司选则把资料库事务的 ACID 原则放宽,于计算机协会(Association for Computing Machinery,ACM)上发表了一篇文章Base: An Acid Alternative,正式提出了一套 BASE 原则。

BASE 基于 CAP 定理逐步演化而来,其来源于对大型分布式系统实践的总结,是对 CAP 中一致性和可用性权衡的结果,其核心思想是即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。BASE 可以看作是 CAP 定理的延伸。

BASE 理论指:

  • Basically Available(基本可用)

基本可用就是假设系统出现故障,要保证系统基本可用,而不是完全不能使用。比如采用降级兜底的策略,假设我们在做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序。但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在本地 cache 里放置一份热门商品以便兜底。

  • Soft state( 软状态)

软状态指的是允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

  • Eventual consistency(最终一致性)

上面讲到的软状态不可能一直是软状态,必须有时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性,因此所有客户端对系统的数据访问最终都能够获取到最新的值,而这个时间期限取决于网络延时,系统负载,数据复制方案等因素。


参考文献

微服务的4个设计原则和19个解决方案
博客园.如何健壮你的后端服务?
高可用的本质
CAP 定理的含义 - 阮一峰的网络日志
CAP理论该怎么理解?为什么是三选二?为什么是CP或者AP?面试题有哪些?
Base: An Acid Alternative
如何优雅地重试 - 字节跳动技术团队

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值