不当愣头青、聊聊软件架构中的那些惯用的保命手段

大家好,我是vzn呀,又见面了。

前不久出了个有意思的事情:

某平台UP主发布了一段小米SU7碰撞的测评视频,表示碰撞后小米SU7的小电瓶出现故障导致车门打不开、紧急呼叫系统失灵等问题,引起不小轰动。就在大家都在吃瓜看小米如何应对时,小米官方抛出了一份内部调查报告,重点就一个:我还藏了个备用电源!我把过程上报到国家监控平台了!!你在黑我!!!。这剧情反转程度,像极了重生小说中的桥段,小米设计师似乎预料会有这么一出,提前藏了个小电瓶,就是为了等待这一刻的绝地反杀。

抛去吃瓜的爽文成分,深入其内核,我们不难发现,小米SU7备用电源的设计思路,在软件领域有着广泛的共鸣和深刻的意义。在软件开发与设计的广阔天地里,面对的是一个充满变数的世界,其中既包括普通用户的日常操作,也涵盖了网络故障硬件崩溃乃至恶意攻击的种种挑战。按照墨菲定律的阐述,只要概率大于0%的事情就很容易会发生(说的通俗点,就是怕什么就会来什么)。做好异常场景的应对,是一个成熟程序员的进阶必修课,也是一个软件系统线上平稳运行的内在基石。

说到这里,就不得不提到系统架构设计的一个深层哲学,它强调的是对未知风险的敬畏与思考。一个优秀的系统架构设计,应当能够预见并应对未来可能出现的各种挑战,要容忍并接受局部错误存在客观性并努力将局部错误控制在一定范围内,同时当系统出现不可逆灾难时可以尽量最大程度的保障系统核心业务的持续可用。

本篇文章,我们就来聊聊软件开发中针对系统容错能力以及灾难应对能力的考量。

1. 容错设计,再给一次机会

我们开发的一款软件,上线运行后面临的情况是极其复杂的,说不出错,几乎是不可能的:

上游输入异常参数怎么办

外部接口挂了怎么办

依赖出现故障怎么办

网络抖动导致请求失败了怎么办?

硬盘坏了怎么办?

...

前面也说过,异常是必定存在的客观事实。如果我们一味的追求绝对的0偏差,其实是自己为难自己。所以呢,为了提升系统应对异常情况的能力,考虑增加一些容错设计,便是一个不错的思路。允许有限范围的异常、尝试去包容这些异常,尽量保证最终符合预期的目标达成即可。

容错能力的实现,有很多种方式,典型的就是重试机制补偿策略

1.1. 重试:浪子回头金不换

重试是为了降低异常情况的出现给业务请求造成的影响,尽量保障请求按预期被处理的一种常用方式。那是否所有的失败场景都需要重试呢?显然不是,比如登录密码校验的时候,输入了一个错误的密码导致鉴权失败,这种不管重试多少次永远都依旧是失败。一般而言,只有受一些瞬时偶然因素干扰的失败场景,才需要考虑重试策略。比如:

  1. 某次对外网络请求的时候,因为网络抖动 等原因导致的请求失败,或者是请求处理超时,可以通过有限次数的重试,来提升对外交互处理的成功率
  2. 分布式系统中,某个节点服务异常,网关将请求重新分给另一个节点进行重新处理,提升整个集群的容错能力
  3. 抢夺分布式锁的排队处理场景中,某次没有获取到锁,等待一段时间后再次尝试获取锁

按照重试触发时机 的不同,重试策略可以分为立即重试延时重试

触发时机 适用场景
立即重试 适用于一些因为偶然因素导致的失败,比如请求的时候如果因为网络抖动导致的链接失败,可以尝试立即重试。
延时重试 适用于因为资源受限引发的失败场景。比如对外请求的时候,由于下游接口流量过大触发限流导致的失败,如果立即重试,大概率重试依旧失败,这种场景就可以考虑等待一定时间后再重试,以提升成功率。

而根据重试操作的具体实现逻辑,还可以分为原路重试差异重试

重试类别 场景举例
原路重试 (1)调用三方系统HTTP接口,出现响应超时或者网络不通等异常情况,重新发出一次请求 (2)获取分布式锁失败的时候,尝试重新请求获取
差异重试 (1)分布式系统中,一个节点的请求处理失败后,网关将请求分发到另一个节点进行重试 (2)从Redis中获取数据失败,尝试从MySQL中捞取数据

在重试机制落地的时候,还有2个基础的原则不能忽略:

  1. 限定重试次数: 保证极端情况下,系统不会陷入无限循环重试。
  2. 重试次数要合理:避免过多的重试,浪费系统资源,不要为了重试而重试。

此外,在一个较长的处理链路上,如果涉及到重试的环节过多,还需要考虑引发请求风暴的风险。比如下图的场景,假定重试的最大次数限制为N:

所以重试手段并非是零成本的,它的使用也是有副作用的,尤其是在一些复杂链路场景中。为了规避连环重试可能导致的连环风暴隐患,还需要引入一些辅助手段来应对。

  • 打破链式重试

请求风暴的形成,是因为最末端的异常被无限制透传给了所有上游环节,然后触发了上游环节的反复重试,将请求数量指数级放大。但是实际上,仅仅是最后一个节点与DB之间的请求出现问题,其实只需要重试这个操作即可,上游节点并不需要重试。为了实现这一效果,需在请求交互层面进行规划,通过返回值、返回码等方式,告知上游节点是否需要重试,将重试的范围限定在故障发生位置,而非全链路的链式连锁反应。

  • 结合熔断策略

结合熔断机制,根据该路请求处理的失败率进行判断,达到一定阈值的时候,直接执行熔断操作。后续通过一定的探测机制,分配少量的试探性流量,如果成功率达到设定阈值,则恢复此链路的后续处理。

1.2. 补偿:亡羊补牢犹未晚

上面介绍的重试手段,主要目的是为了尽可能的提升当次操作的成功率。但是,总有一些异常场景不是即时重试就可以解决的。比如在一些大型的微服务分布式系统中,一个请求流程会跨越多个服务进行处理,且请求的处理往往是异步的,如果出现重试也无法解决的异常问题,就需要额外的补偿机制,对处理结果的最终一致性进行保障。

补偿机制经常被使用在分布式系统中,它的一个核心前提是,允许并接受过程中的暂时性数据问题,并通过补偿措施,保证最终的数据一致性。那么,如何知晓是否需要执行补偿操作、哪些数据需要执行补偿操作呢?这就需要"对账"了。

所谓"对账",就是定期将此前一段时间内的业务处理数据进行盘点比对一下,找到数据层面不符合预期的数据。基于对账发现的异常记录,再执行对应的补偿修正处理。

举个例子:

一个电商平台系统,其订单系统的设计,买家的订单和卖家的订单是分库存储的。一个订单创建并付款完成之后,订单信息会流转到下游消费服务中被各自处理,并分别写入到买家订单库和卖家订单库中。

在微服务化场景下,虽然可以通过一些分布式事务等手段来加以防范,但依旧可能会因为一些极端情况,导致一个订单没有被同时成功写入到买家订单库和卖家订单库中,这样就可能会用户的使用造成影响。这种情况下,就可以考虑搞个定时任务,定期扫描下一段时间内的订单数据,校准下两边的差异,然后针对异常数据进行处理修正。如下所示:

这样,基于事后对账+补偿的双重手段,保障了系统的"最终一致性"目标达成。

2. 顾全大局,舍小义而谋大利

还有一些业务场景,它可能是牵扯到多个并列的依赖方,并最终诉求是将多个依赖方的结果混合在一起。这种情形下,某一个依赖方出现问题,对最终用户的使用体验而言影响很有限、甚至是无感的。一损俱损显然不是最优解,弃卒保车会更为合理些。

举个例子, 一款新闻资讯类的软件,首页的内容流列表由多路数据源汇总而成:

  1. 即时突发新闻
  2. 热门时政要文
  3. 关注账号发文
  4. 可能感兴趣内容
  5. 付费推广内容
  6. xxx

最终多个来源的数据,会被混合成一个列表内容流展示给用户。这个过程中,如果其中某一路(比如:即时突发新闻)出现异常未获取到数据,对用户而言其实是无感的,因为用户也不知道究竟是系统出问题了、还是确实没有即时突发新闻。但是因为某一路数据的获取失败,直接给用户报错异常、或者给用户一个白屏显示,反而是将用户给放大了。

在实际的项目中,当故障的出现已经不可避免且无法规避或者重试解决的时候,为了避免问题的进一步扩大,通过一定程度的"妥协"与"舍弃",以尽量小的损失、避免故障影响面的放大,也是一种常规操作,实现手段有很多,主流的有降级、限流、熔断、放通、隔离等。

2.1. 降级

降级 作为一种兜底策略,通常是在故障场景下从业务层面作出的一种妥协策略。 一般是遇到局部功能障碍、或者资源负载层面问题的时候的一种应对方案。当出现某些突发情况,导致系统资源不足以支撑全量业务功能的正常开展时,为了将有限资源集中起来保障核心功能的可用,而主动将一些非核心的功能停用的思路。

降级的使用场景很多,比如:

  1. 电商每年的618或者双11等大促时节,为了保障抢购下单的正常推进,将订单评价、历史订单查询等非核心功能先降级停用,所有资源全力支撑商品的浏览、下单、付款等操作
  2. 互动社交平台,突发超级流量明星的大瓜新闻时,降低一些非核心功能(推广、关注流)的更新频率,将更多资源用以支撑爆炸性话题的访问与互动浏览操作
  3. 对于即时通信IM类场景,如果出现网络故障原因导致机房带宽承压有限,那就降级让视频和语音类服务不可用,尽力保障文字消息功能依旧可用

降级的本质,就是一个取舍的过程,舍弃不在乎的部分,保住最在乎的部分。舍弃谁、保全谁,需要根据自身业务的特征来判断。一般而言,有几个维度:

降级维度 场景举例
降低用户体验 界面刷新不及时、不展示动效、不展示高清图、不显示系统推送通知
舍弃部分功能 不允许查看历史订单、不允许数据导出操作、不允许上传文件操作
安全性让步 不做复杂二次校验、跳过风控判断、不记录操作日志
降低准确性 列表数据更新不及时、统计报表更新不及时
降低一致性 列表显示的评论数与点击进去正文显示的评论数不一致,已经删除的文章依旧出现在列表中
降低数据量 订单中心只显示最近100条记录,仅可以查询最近1年数据

实施降级操作的前提,需要系统业务规划层面进行配合,要做好系统业务功能的SLA规划,划分出核心功能与非核心功能。同时,在系统的架构层面要做好核心功能与非核心功能的解耦与隔离。

2.2. 限流

一般在春节、五一、或者国庆等节假日,一些热门的景区都会限制进入景区的客流量,以此保证游客的游览体验与人身安全。同样道理,软件系统受限于自身实现、业务规划以及硬件资源承载能力等诸多限制,其承压能力也是有上限的。如果请求流量突增且明显超出系统规划的可承受范围的时候,可能会引发系统宕机等事故。为了保障系统安全,避免突发流量对系统的正常运行造成冲击,就需要对进入系统的流量进行限制管控。

限流一般可以依据两个维度进行实施:

限制维度 场景举例
限制并发数 比如限制连接池的连接数、线程池的线程数等。
限制QPS 限制每秒进入的请求量。

限流操作的实现,离不开限流算法,主流的有漏桶算法令牌桶算法

  1. 漏桶

漏桶算法的原理很简单,它不限制流入的请求量,但是会以一个相对受限的速度从漏桶中获取请求进行消费处理,如果流出速度小于流入速度,请求就会在漏桶中积压暂存等待顺序被处理,一旦漏桶容量被积压的请求撑满,便会发生溢出,无法进入漏桶的请求将被丢弃。

正如其名字一般,漏桶的原理,像极了生活中使用的漏斗。这也是一个示例,再次印证了软件架构设计中的很多实现与处理策略,都是来源于最质朴的生活。

  1. 令牌桶

令牌桶的逻辑与漏桶略有不同,它会有个令牌发放模块负责匀速生成令牌并放入到令牌桶中,然后每次请求处理前先尝试获取一个令牌,只有获取到令牌了才会去处理对应的请求。

值得注意的一点是,虽然令牌是设计成匀速生成并放入到令牌桶中的,但这并没法保证请求一定会被匀速处理。极端情况下,可能会出现短暂请求量突破限速值的情况(比如:大部分时候请求量小于令牌生成量,导致桶内蓄满令牌,突然来波大流量,会一口气消耗掉令牌桶中全部的存量令牌),所以需要根据系统设计的承压负载情况,合理设定限流的阈值。但这一设计也有其优势,偶尔短暂的脉冲波动可以尽量消化掉,同时又保证长期整体处理速率处于一个受控状态。

还有一种简陋的基于计数器的"伪限速"方案,这一思想很简单,每个计数周期维护一个计数器,然后来一个请求计数器就累加1次,计数满阈值后便拒绝后续请求,直到下一周计数器重新计数。这种本质上只能控制流量、无法控制过程流速,极端情况下的一些请求峰值,极有可能会击垮系统,要尽可能将流控计数周期设置的短一些,尽量避免在核心重要系统中使用此方案。

此外,对于一些集群化多节点部署的场景,规划限流的时候,还需要关注是单机 的流量限制,还是集群整体的流量限制,选择适合自己业务的实现方案。

2.3. 熔断

熔断在现实世界中最直观的应用,就是家里强电箱里空气开关中的保险丝了。当电流超载的时候,保险丝就会断开,以此来保护家里的整体电路不会烧掉,以及各种电器不受损坏。

同样道理,在软件实现中,也有类似电路保险丝一般的设计思路,通过在服务的对外请求调用处增加熔断器,当符合预设条件时,就会将对应依赖的服务从自身的请求链路中剔除,来避免自身节点耗费大量的资源在等待一个大概率错误的响应。

熔断,是自身的一种自保手段,目的是防止外部节点异常将自己耗死。熔断的策略一般有两种:

  1. 按照请求的失败率进行熔断

短期内发往某个目标服务的请求失败率高于某个阈值则执行熔断策略,不再继续调用此目标服务。然后通过定期的心跳探测机制,或者少量试探流量的方式,决定继续熔断还是恢复请求。

  1. 按照请求响应时长进行熔断

对于一些高并发量的处理场景,如果调用的目标服务的请求时延过大,势必会拖累整体系统的吞吐量。这种情况下,为了保障自身节点的处理性能,也可以按照请求响应时长,决定是否触发熔断操作。

此外,在集群部署环境下,网关节点也经常会将熔断作为基础功能进行提供,实现比服务熔断更细粒度的一种控制,当服务集群中某个节点出现故障时,直接将该节点剔出集群,待其恢复之后再加入到集群中。

具体应用的时候,可以直接使用一些成熟的开源方案,比如Hystrix或者Sentinel等。需要强调的一点是,熔断一般针对的目标是一些非核心、非必须的依赖服务,本质上,熔断也是降级的一种实现形式。

2.4. 隔离

隔离 作为一种故障控制手段, 其设计思想是通过将资源分隔开,互不干扰,这样系统出现故障的时候,就可以将故障限定在一定的传播范围内,避免出现滚雪球效应、波及全局。常见的隔离措施,有数据隔离机器隔离线程池隔离以及信号量隔离等等。

  • 数据隔离

最直观的表现,就是分库分表了。比如对系统的数据,按照业务维度进行分库存储,或者按照业务的重要度进行识别,将数据识别为重点数据/非重点数据,亦或是保密数据/非保密数据,然后按照细分后的结果实施差异化的数据存储保障策略。比如,对于非重点数据,简单的搞个一主一从双副本即可, 而重点数据,可能得考虑异地多副本可靠存储与备份。

  • 机器隔离

不同的业务使用不同的机器,从硬件资源层面进行隔离。通过将机器分组的方式,针对重点服务或者是高危服务实现专机专用,而对应一般普通服务,则可以多个业务混用同一套机器,从而实现了差异化的隔离处置。

  • 线程池隔离

隔离的思想,不仅仅是体现在数据层面或者是进程机器节点等宏观层面,该思想同样适用于对单个进程内部的实现。因为同一个进程内处理很多不同的逻辑,如果某个处理逻辑无限制的创建执行线程,占据了全部的系统CPU资源,则整个进程中其余的逻辑就会受到影响。

为了应对这种情况,可以基于线程池进行隔离设计,为主要业务处理方法指定对应的执行线程池,限定具体业务方法仅可以按照分配线程池提供的线程资源进行调度与使用,禁止业务方法自行无度占用系统的线程与CPU执行资源。这样一来,即使某个业务占用了自己全部线程池资源,依旧不会影响到其余线程池的正常处理,保障了其余业务的正常开展。

因为线程池的维护也会占用额外的资源,所以隔离的粒度的把控也要做到适可而止,遵循适度原则。

3. 硬件灾备:钞能力带来的超能力

软件的顺畅运行与硬件的稳健支撑密不可分。尽管软件层面通过巧妙的容错设计、灵活的降级策略以及精准的限流机制等手段,能够显著提升其自恢复能力和可用性,但在面对硬件故障这一硬性挑战时,单纯依赖软件手段就显得力不从心。所以,在设计规划建设一套可靠的软件服务的整体架构时,硬件部署规划时的可靠性设计,也是无法回避的话题。

相较于软件层面的各种容错策略,硬件层面的应对就显得简单且粗暴------------堆资源 !即通过资源的冗余部署来增强系统的容错能力。当然,这一策略的实施不可避免地会增加经济成本,所以具体实施与规划的时候,还需要结合预算情况,在成本许可范围内实现可靠性的最大化保障能力。

常见的硬件层冗备的实践,一个是保障业务应用高可用的多活机制,另一个是为了保障数据可靠存储的多副本存储机制

3.1. 多活

随着越来越多的生活场景被搬到线上处理,互联网白热化时代,对与业务的7*24小时持续可用提出了严峻的挑战。但对于一个软件服务来说,不管架构多么完美、代码多么优雅,最终程序都得运行在硬件基础之上,而硬件层面的风险,是代码无法左右的。那么如何应对硬件的各种损坏或者不可用风险呢?很简单,**花钱消灾!**多花点钱,多搞点硬件资源,多部署几套服务就行咯。但是这个部署多套,实际也是有讲究的。

为了应对不同层级的风险,也引申出了多种不同的堆硬件的方式:

  • 集群化

为了应对单台服务器硬件的损坏、比如硬盘损坏、电源烧毁等, 单个机房内部署多个节点,由多个不同的机器,共同组成一个集群,这样其中一个节点故障,其余节点依旧可以正常处理业务,有效避免了单点故障的出现概率,提升了业务的可靠性。

  • 同城双活

上述在同一机房内利用多个节点组成集群的方式,虽然能应对单台机器的故障场景,但如果机房出现整体故障,比如停电、着火、光缆被挖断等情况,依旧会导致全军覆没。为了应对这一可能的风险,自然而然的解决方案就是再建一个机房,这样两个机房互为备份,风险就大大降低了。通常而言,两套机房之间会涉及到数据的同步,所以对机房之间的网络传输速度与时延有极高要求,这就要求两个机房不能离得太远,最好在同一个城市。 ------ 这便形成了常说的同城双活架构。

  • 两地三中心

基于同城双活的模式,其可靠性已经可以满足大部分普通业务场景对于系统可靠性的诉求了。但若业务系统极其重要,尤其是一些金融、社交、基础服务提供商等牵扯到国计民生的领域,对系统的可靠性与数据的安全性有更加苛刻的要求。在同城双活架构中,为了控制机房间网络时延,两个机房的距离都不会太远,万一出现某些不可抗力的自然灾害(比如地震)造成两个机房全部损坏,依旧会导致业务或数据受损。所以如何应对?答案已经呼之欲出了,跨不同城市多建一些机房呗!于是乎,两地三中心三地五中心等等解决方案应运而生。

看到没?系统的可靠程度,一定程度上,取决于堆的钞票的厚度。

3.2. 多副本

冗余备份 ,又叫做多副本。本质上就是为了防止单点故障造成数据层面的丢失,而采取的将同一份数据分散在多个位置存储多份的一种方式。这种方式会造成额外的资源成本支出,但其所带来的数据可靠性与高可用性,是"孤本"无法比拟的。

多副本的策略,广泛的被应用在各种数据存储组件中。比如:

  1. 本地缓存多副本
  2. Redis多副本
  3. MySQL的多副本
  4. Kafka多副本

最常见且最简单的多副本策略,就是Master-Slave这种架构,类似MySQL的一主多从架构。在这种架构中,通常由Master节点负责数据的写操作,然后通过内在的数据同步机制,将数据变更同步更新到各个Slave节点进行数据的多副本存储。为了提升硬件的利用率,Slave节点除了用于数据内容的多副本可靠存储,还可以对外提供只读查询操作,在必要场景下支撑业务的读写分离诉求。

Master-Slave这种主从架构的多副本策略有个致命的问题,就是每台节点存储的都是全量的数据文件,这使得数据总量受限于单机存储,存在瓶颈。对于超大数据量场景,还会需要更加复杂的多副本方案,对总体数据进行切片,对每个切片数据进行多副本支持,进而可以支持容量的水平扩展。像Redis集群或者是kafka所采用的便是这种策略。

  • 分片存储形态1:分散在不同机器上存储
  • 分片存储形态2: 多集群承载分片模式

这种对数据进行分片并分散在多台物理存储节点的方式,打破了单机容量 的限制,但是也增加了数据读写与数据同步的复杂度。因为数据分散在多个节点上,所以在读写的时候,需要支持将请求路由分发到数据分片所在的节点上,比较常见的是使用一致性Hash算法来进行分片。此外,各节点上分片数据的同步与一致性保障也需要更加复杂的处理逻辑来支撑,比如Kafka就专门设计了ISR算法来处理多副本之间的数据同步。

4. 人工干预,保证对系统的控制权

我们按照业务场景与业务诉求进行功能实现的时候,会预先设想好各个场景的处理与应对,也会考虑一些可能异常场景的代码层面自动兼容与应对。但可能会有一些场景,它就是突破了我们预先对系统设定的一切合理规划,或者系统出现一些未曾预料到的场景无法自恢复的时候、亦或是自动恢复或回滚处理的影响面太大的时候,都可能需要人工介入处理。所以,系统在规划与实现的时候,很有必要构建一些人工干预手段与能力。

这种人工干预能力,有很多实际的应用场景,可以用来提升运维人员对系统的高度掌控力从而更好的应对各种突发场景,也可以作为运营人员的一种高权限后台处置权进行预留。

4.1. 人工介入应急处置能力

先看个例子:

背景:

某个业务需要从远端数据源获取数据并进行业务逻辑处理,由于业务本身属于特别核心且重要的服务,远端数据源数属于外部依赖,数据准确性与服务可用性不可控。

实现:

  1. 业务处理的时候,定期从远端进行数据源拉取更新,优先使用远端数据源的数据。
  2. 为了应对远端数据源不可控风险,定期更新的时候都会将远端获取到的数据写一份到本地磁盘中进行备份,本地磁盘保存最近N个的备份。
  3. 如果远端服务数据拉取失败,则业务自动尝试从本地读取最近的备份文件以支撑自身业务的继续运行。如果最近文件处理失败或数据异常,则自动加载前一个备份文件,以此类推,直到重试完本地所有备份文件后,如果依旧处理失败,则系统不可用,放弃挣扎。

结合上述背景,可以看出实现应对策略想的还算周到,做到了使用本地备份进行远端数据请求失败情况下的兜底处理,还考虑到了数据加载异常的情况增加了自动重试机制,自动往前加载直到尝试加载到一份可用的历史备份文件为止。但是考虑一种场景:假设远端服务接口正常,返回的数据响应格式也正确,但是由于远端数据源的服务开发人员昨天夜里升级了个版本,导致下发的数据内容本身存在严重的错误,这导致下游业务使用该数据的时候业务受损。这种情况下,前面规划的实现中的所有自恢复与自保手段都是失效的。

所以呢,如果在规划阶段,在上述实现的几点保障措施的基础上,再额外规划一条人工指令干预通道,在紧急情况下,可以人工下发指令,强制要求系统断开与服务端的实时更新逻辑,并强制加载第X份本地备份文件,便可以快速让自身服务摆脱远端数据源的故障影响,等到其故障修复后,再下发指令恢复对远端数据源的实时更新。改造后的示意图如下所示:

4.2. 非预期场景的人工处置权

人工干预能力,也可以算作是管理端系统的一个"特权功能",为后台人员提供更高的操作权限,解决某些看似不合理、却极可能出现的业务层问题,比如处理某些难缠的客诉问题。

举个简单的例子:

一个证券公司开发了一款炒股APP,并提供了一个投顾付费功能,用户付费之后就可以使用对应的高级功能。业务规划的策略是用户购买之后不允许退订,并且界面以及用户协议里面也明确提示了购买之后不允许退订。

A用户购买之后就非要退订退款,然后不停地缠着客服并威胁不处理就去监管机构、证监会等地方去投诉。

理想情况下,我们预期是用户按照产品规划的策略进行购买,并且也已经尽到告知义务,不支持用户退订。但面对客户的胡搅蛮缠,本着维护公司形象的角度出发,为尽快平息争议,客服部门会私下同意后台操作为这个用户退单退款。如果系统设计与实现的时候,没有规划构建对应的后台人工退单退款能力,处理起来就会很被动了------------正所谓:可以不用、但不能没有

5. 监控预警,防患于未然

前面提及的容错设计以及一些灾备方案,其面向的是故障已经发生的情况下,如何去应对故障来保障系统业务的可用性。而更为稳妥的一种预期,是能够在问题刚暴露一点苗头的时候,就能被发现并及时化解掉 ,这里就需要在系统实现的时候进行一些必要的数据埋点指标采集监测,及时将系统的预警信息告知具体维护人员,提醒维护人员及早介入处理。

并非"看不到的问题就是没问题、看不见的故障就是没故障",作为系统的负责人员而言,应该是要知晓系统的整体运行状态以及系统的健康度,通过状态监控、指标监测等手段,让线上系统的运行状态从黑盒变为白盒。

5.1. 监控告警

一般而言,监控平台都是独立于业务进行构建,且提供Push或者Pull两种指标数据获取机制。在监控内容方面,可以涵盖资源使用情况、系统状态、业务运行数据等各维度。

监控告警是开发与运维人员知晓线上系统异常状态的一个重要手段,实施的时候需要注意不要滥用告警通道。告警消息的发送最好支持分组聚合消息抑制等能力,避免出现无用告警消息的狂轰滥炸,麻痹接收人员的神经、淹没真正重要的"求救"信号。同时,在构建监控告警平台的时候,考虑尽量独立于业务,让告警相关的逻辑从业务中解耦,降低监控对业务逻辑的侵蚀性。

关于如何设计与规划构建监控告警平台,有兴趣的可以看下我此前的一篇文章《搭建一个通用监控告警平台,架构上需要有哪些设计》

5.2. 实时Dashboard

既然要防患于未然,首先是要对系统整体的健康状态有个清晰的认知。这个时候,系统健康监测相关的能力的价值就会显现出来。这就像是一份系统的实施体检报告,基于这份体检报告,可以发现系统中潜在的压力点位、风险环节、可疑趋势,然后可以提前介入进行应对,将故障消灭在萌芽阶段。

5.3. 灾备演练

如前所述,系统在构建的时候规划了一系列高大上的异常应对与灾难恢复手段,但如何确保这些手段在异常出现的时候能达到预期效果呢?这就得通过灾备演练来检验了。如同和平年代的军事演习,灾备演练也是很多大型项目的定期"军演"。通过模拟一些可能的灾难故障场景,去验证系统的容错与异常保障手段的有效性,发现应急方案中存在的问题并及时修复。

对于一些大型系统而言,其整个业务流程的处理会牵扯到上下游以及周边等众多依赖,一些灾备预案的实施也是上下游联动触发的。所以定期灾备演练的另一个目的,也是锻炼开发运维人员的应急预案实施的默契度。

6. 意识养成,保持对风险的敏锐识别力

如前所述,在实现层面有很多种成熟且可落地的方案可以将系统的异常应对与灾难恢复等场景变为现实,但这些其实都是具体的" ",是我们已知有这个风险或者诉求的前提下,为了应对这些已知可能场景而作出的具体应对之法。而身为一名IT从业者,一方面要经历将业务诉求变为现实的落地过程,同时也是与各种异常情况博弈的历险之旅。对风险的敏锐洞察力 ,应该是一个优秀程序员刻在骨子里的品质。这种品质,不仅体现在编码层面、亦非局限于架构设计,而是各个方面的,它是一种思维模式、是一种本能的条件反射

保持对风险的敏锐识别力,才能让自己看见潜在风险,才能让各种风险防备之术得以落地。

举个简单的、非技术实现层面的例子。

线上系统临时有个问题,需要紧急手动换个包并重启下进程修复。
  • 头铁勇士的梭哈

多简单的一件事,进程停掉,删掉旧包,上传新包,然后进程已启动,完美解决。

也许,大部分情况,的确也没出现过问题。但对于有一定资历的人员而言,看到对线上环境的这一操作,往往会有点"心惊胆战"的感觉。比如:

万一上传的包有问题,启动失败了,这个时候线上包也删了,服务也没法启动了,线上服务直接就报废了

  • 吃过小亏之后的低头

结合上述操作可能存在的风险点,改良版的做法,自然就是旧包不删、改为重命名备份起来,这样新包万一有问题,可以直接用旧包回滚恢复线上服务即可。

这种改良之后的操作方法,从可靠性层面而言,的确有很大改进,给自己留了充足的回滚与回退的余地。但是仔细审视一下,依旧有改进的余地:

  1. 上传新包的操作,由于要走网络传输、受网络波动的影响较大,存在失败风险。
  2. 如果包的体积很大、或者上传的时候要走vpn、堡垒机等层层关卡,可能速度会比较慢,整个传输过程的耗时会很长。这种情况下,可能会导致线上进程停机时间太久。
  • 受尽毒打而幸存后的谨慎

进一步的优化上述操作的步骤,可以将整个动作分为前置准备环节和线上操作环节两部分,将一些比较耗时且风险比较大的操作,放在前置准备环节中预先完成。

这样一来,在正式线上操作的环节中,仅需要执行一些确定性较高的动作,这样既可以保证执行动作的快速结束,也可以降低执行动作的不确定性。通过风险前置操作,降低了整个操作过程中出现问题的概率。

说到这里,也许有的小朋友会反驳,觉得公司带宽很高、传输文件很快,不需要这么麻烦,直接梭哈干就行了。这其实就是一个意识层面的共识问题,也是一种对风险的应对策略问题。其实还是之前的那句话,能意识到的风险并非真正的风险,往往是那些看似不可能的风险才是真正的风险。所有行为的出发点其实就一条:这个动作操作失败的后果,是不是你能够承担的。如果可以,那可以直接梭哈,否则的话,就要三思。

7. 再论本心:扁鹊三兄弟的故事

最后,讲个故事吧。

传说扁鹊周游到魏国的时候,魏文王接待他并问他:你家兄弟三人都是学医的,那么你们三个人中谁的医术最高呢?扁鹊回答说:"我大哥医术最高,二哥次之,我医术最差"。魏文王很困惑:"为何世人皆尊你为神医、却不曾听闻你大哥二哥?",扁鹊解释道:

  • 我大哥的医术最好,是因为他能够在你没有发病之前就能看出你是否有病。那个时候,病人是不会觉得自己患病了的,我大哥就在病人发现之前就将病给治好了。这是因为这个缘故,大哥的医术一直不被他人认可,也没有什么名气。

  • 二哥是家中医术第二好的,因为他能够在病人发病初期就看出来,然后将病人给治好,这样一来,病人们都认为我二哥只擅长治疗一些小病症。

  • 病人找我治病时,已经到了中晚期,病情已经十分的严重了。我将那些患了重病的病人给医治好后,我就更加出名了。但从根本上来讲,我的医术比不上我的两位哥哥。

放到当前日益内卷的IT行业,扁鹊大哥、扁鹊二哥这种人,也许是属于技术高超的一类人,他们默默守护自己的代码、不给异常爆发的机会。于是呢?始终稳定的线上服务,让人慢慢淡忘了相关开发人员的存在,使其反而成为被边缘化的透明人。真正可以有机会崭露头角、博得领导青睐的,往往都是团队里面的救火队员般存在的人,这些人,不停的在前线冲锋陷阵,去解决线上那些按起葫芦起了瓢的问题,久而久之便成为领导心中信赖的柱石,相关的机会与资源也向其倾斜。

技术之外的事情,虽然发人深省,却也似乎无解。正所谓圣人治未病,不治已乱、治未乱,反观我们自身,如何抉择,主动权在个人、遵从本心最重要。但是相信的是,时间会证明一切,一切的坚守与技术上的追求,最终一定会被看见(有点鸡汤的味道)。所以呢,技术上有点追求,永远是个正解。

8. 小结

好啦,关于软件开发与设计过程中的异常应对与灾备能力的探讨,暂且告一段落。这里所提到的容错应对灾难应对能力,其重要性犹如生活购买的保险------平日里或许显得默默无闻,甚至让人有些"成本浪费"的错觉,但在关键时刻,它们却能成为抵御风险的坚固防线,其价值无可估量。

正如小米SU7配备的备用电池,这一设计在紧急情况下为用户多了一层求生保障。在软件开发与设计的世界里,是否需要构建类似的备用方案灾备系统,取决于业务对潜在损失的容忍度。若灾难性后果是业务所无法承受的,那么增加一些额外的成本,构建一套完善的灾备兜底、异常保障以及监控告警机制,就显得尤为重要了。

亦如古语云:未雨绸缪,有备无患

我是vzn呀,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【是vzn呀】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

相关推荐
听我对云说6 分钟前
Java语言程序设计 选填题知识点总结
java·开发语言
苹果酱056711 分钟前
浅谈vue3 和 vue2的区别
java·spring boot·毕业设计·layui·课程设计
好看资源平台30 分钟前
网络爬虫——分布式爬虫架构
分布式·爬虫·架构
forestsea37 分钟前
【Java 解释器模式】实现高扩展性的医学专家诊断规则引擎
java·人工智能·设计模式·解释器模式
陈小于40 分钟前
springboot集成shiro和前后端分离配置
java·spring boot·后端
_Matthew43 分钟前
学习Zookeeper
分布式·zookeeper·云原生
不修×蝙蝠1 小时前
数据结构--数组实现栈和队列
java·数据结构·数组··队列
信息化未来1 小时前
odoo17 档案管理之翻译2
java·服务器·前端
清山博客1 小时前
Java将PDF保存为图片
java·开发语言·pdf
life10241 小时前
pdf文档动态插入文字水印,45度角,旋转倾斜,位于文档中央,多行水印可插入中文
java·pdf·水印