开发者社区 > 博文 > 探讨打造「高可用架构」秘籍
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

探讨打造「高可用架构」秘籍

  • fe****
  • 2024-07-10
  • IP归属:北京
  • 100浏览

    背景

    高可用性的文章多如牛毛,看得人眼花缭乱。今天,咱们换个花样,以终为始,来聊聊如何实现系统业务的高可用性这个宏伟目标。本文覆盖高可用架构设计、常见架构模式、高可用开发运维、大促高可用保障、业务高可用、COE复盘等方面的理念和思考。

    高可用性是个宏大的主题,覆盖的领域广泛。我们要聊的,都是实实在在的团队实践经验和教训

    本文不包含异地多活等议题。现在,让我们一起踏上这场关于高可用性的探索之旅吧!

    为了您的阅读体验,请先查看大纲,注意本文篇幅较长,涵盖了大量细节和分析,需要您花费更多的时间来阅读。谢谢!

    一、高可用概念

    先来介绍下高可用到底多高算高哈,wiki 上对高可用(High Availability)的定义:

    High availability (HA) is a characteristic of a system which aims to ensure an agreed level of operational performance, usually uptime, for a higher than normal period.

    高可用(High Availability)是系统所能提供无故障服务的一种能力。业界衡量系统可用性的方式主要有2种:

    时间纬度的系统可用性。
    请求纬度的系统可用性。

    1、时间纬度的系统可用性

    谈可用性不需要绕来绕去,大家只谈 SLA 即可。业界度量高可用能力也有统一标准:判断宕机时间,并以此计算出每年系统可用时间达到几个9,来判断高可用架构是否健壮。具体如下表所示:

    一般来说,我们的观念里一个服务至少要做到 99.9% 才称为基本上可用,是合格性产品。否则基本很难被别人使用。一般大家都在谈年 SLA,但是年 SLA对研发来说一般没有任何实际工程指导意义。我们更应该看重的是季度 SLA,甚至月 SLA,甚至周 SLA。这所带来的挑战就更大了。

    这里为什么要加99.95%呢?第2章节会说明

    2、请求纬度的系统可用性

    任何一家互联网公司,都是有流量的低峰期和高峰期,在低峰期停机1分钟和高峰期停机1分钟,对业务影响的结果完全不同。因此,可以基于一种更加科学的度量方式来评估,即基于一段时间(比如1年)的停机影响的请求量占比进行评估。这也是为什么要求每个团队在业务低谷期进行上线发布的原因,同时也是为什么大促期间问题等级更严重的原因。

    系统高可用性 =  成功请求数 / 总的请求数。比如系统可用性99.9%:表示1000个请求中允许1000 * (1- 99.9%) = 1个请求出错。

    二、高可用目标

    企业给用户提供能力,需要满足用户的诉求。如下图:

    从用户体验角度出发,第一需要确保服务稳定性,第二需要确保功能正确性。
    从企业防资损角度出发,第一需要确保应用高可用,第二需要确保业务高可用。

    那我们应该如何来系统的分解“提升 SLA”这一个难题呢。我们不能仅仅从系统结构(主备架构、集群架构等)的角度出发,而应该从业务的视角来考虑高可用的架构设计,高可用最终还是回归到系统稳定性的建设目标(“降发生”和“降影响”)。

    1、稳定性建设的最终目标

    这里我引入两个工业级别的概念 MTBF 和 MTTR。

    1. MTBF就是Mean Time Between Failures的缩写, 名为平均失效间隔, 它是指系统有多长时间坏一次。
    2. MTTR 就是 Mean Time To Repair 的缩写,名为平均修复时间,它是指修复系统并将其恢复到完整功能所需的时间量。

    降发生,即降低故障发生的概率,也即对应上面提高MTBF。通过冗余设计的思想来实现应用架构的高可用能力保障,同时通过可靠的基础设施组件,来将应用的高可用能力转移到基础设施来提供。

    降影响,即降低故障发生后的影响范围,也即对应上面降低MTTR。早感知,快定位,急止损,优改进。

    有了这两个概念, 我们就可以提出:

    一个服务的可用度,取决于 MTBF 和 MTTR 这两个因子。从这个公式出发,结合实际情况,就很好理清高可用架构的基本路数了。那就是: 要么提高 MTBF, 要么降低 MTTR。除此之外别无他法。

    2、高可用需要考虑的因素

    回复下第一章节高可用为什么要加99.95%这个问题。因为3个9(宕机8.76小时)对于物流生产环境的影响比较大,但4个9(宕机52分钟)对大部分系统来说又要求太高,基于成本和业务容忍度考虑,故提出了一个折中的99.9N(N=5,6,7,8)%的可用性概念。

    1. 成本

    系统可用性越高,对你的系统要求也越高,你付出的硬件和人力成本和代价也会越高。需要思考比如从99.95%提升到99.99%成本是多少,收益是多少,性价比如何?还不如花时间降低MTTR,否则MTTR平均修复时间延长反过来导致可用率降低。

    而对于一些服务并不需要达到那么高的可用性,因此就可以为这些服务设置较低的可用性目标。

    1、稳定比省钱更重要,所有省钱的前提都是需要保证系统稳定性。但也需要杜绝过度浪费。 2、稳定性压倒一切,稳定性是1,其他的是0,如果没有稳定性,就什么都没有了。
    1. 业务容忍度

    系统可用性也需要考虑业务的容忍度。比如支持幂等的服务增加重试提高成功率

    对于京东结算页支付功能来说,任何一个请求的失败都有可能带来资金的损失,因此对于这类的服务,对于错误的容忍度是比较低的,也就要求系统可用性较高(比如99.99+%)。

    对于常用的商品详情页话术展示等而言,即使请求一次失败也是 可以接受的,下次再请求成功就可以了。因此对于这些业务来说,业务容忍度较高,系统可用性不要求一定要很高(比如3个9)。

    3、面向业务的高可用目标

    在实际的操作和讨论过程中,发现这几个9的指标虽然简单,但是并不能直观的理解,而且对于我们分析问题和设计方案没有很强的指导意义,因此我们决定找更加容易理解和操作的目标。结合上面说的稳定性的建设目标(“降发生”和“降影响”),出了一个可量化、可衡量、可操作的2个高可用目标:

    1降发生”:尽量避免发生问题,618和11大促0问题,半年最多发生1次问题(P7)

    不出问题当然是高可用的首要目标了,不然的话天天出问题,恢复的再快也没意义。

    2)降影响:快速恢复业务,30分钟内恢复业务(优先止血)

    特别注意这里我们强调的是“恢复业务”,而不是“解决问题”。很多人在处理生产问题或者故障的时候有一个误区:那就是一定要找到问题根因,然后解决。

    这样计算下来一年不可用的时间大约就是60分钟,正好契合4个9 的业界通用的可用性目标。


    在这2年的项目执行过程中,这个目标真的是非常有用,非常具有指导意义,具体表现为:

    1. 团队目标聚焦于业务,而不是聚焦于技术,以结果为导向,确保最终效果不会走偏。
    2. 将目标自顶向下分解。比如需求改造上线,从PRD》架构》编码》测试》发布 等很容易就得出要做的事情了
    3. 上线结果导向。比如上线发布是否有灰度、回滚、验证功能?线上有问题多久可快速恢复止血等?


    综上所述,我们应该从业务角度出发,降发生概率和降影响,否则MTTR(平均修复时长)高,反过来影响可用率,比如本来服务可用率是99.95,由于MTTR增加,导致高可用降低到99.9%。


    三、高可用架构设计原则

    保持简单,使问题易于发现,快速解决。
    价值回归,成本收益要合理。

    1、解耦

    耦合度过高是软件设计中的一大隐患,也是导致系统可用性问题的主要原因。记得在大学老师就讲“高内聚、低耦合”。大到系统设计小到API接口方法。核心都是降低不同模块间的耦合度,避免"牵一发而动全身"。一个高度耦合的系统,一旦发生微小的改动,就可能导致意想不到的bug和系统崩溃。在这种情况下,即使是最基本的功能维护也会变得非常困难,更不用说实现高可用性了。因此,降低耦合度对于提高系统的可维护性和可用性至关重要。

    1.1、组件的低耦合原则

    1. 无循环依赖原则:技术组件之间不能存在循环依赖,即A组件不能依赖于B组件,同时B组件又依赖于A组件。
    2. 稳定依赖原则:被依赖的组件应尽量保持稳定,尽量减少因业务需求变化而导致的变化。
    3. 稳定抽象原则:为了使组件更加稳定,组件需要具备更高的抽象性,不牵扯具体业务需求。

    1.2、面向对象的低耦合原则

    1. 开闭原则:对修改封闭、对扩展开放,即对象可以扩展新功能,但不能修改原有代码。
    2. 依赖倒置原则:高层对象不应直接依赖于低层对象,而是应依赖于抽象接口,而抽象接口属于高层。
    3. 接口隔离原则:不要强迫使用者依赖他们不需要的方法,应该使用接口来隔离方法。

    参考案例:

    案例1:
    时效上游核心是获取订单下传库房时间和妥投时间,Promise内部分为时效域、产能域(订单占用仓库/站点产能),全程跟踪话术,时效持久化(一线业务)等。基于解耦的原则,优先保障时效、产能域。


    ❌待改进案例

    案例1:0/1级线上应用报线程池耗尽
    基本原因:es性能下降导致jsf接口tp99升高,下游超时时间重试,jsf线程池被异常接口方法占用无法释放,导致其他接口报出线程池耗尽问题。
    根本原因:非0/1级应用的功能【模糊查询es耗性能的功能】放在0/1级应用中,未良好的解耦。


    案例2:JMQ和JSF接口混合在应用,MQ消费性能差导致MQ积压,需要重启服务,但由于有JSF,导致恢复JSF较慢
    基本原因:JMQ环节监控服务,MQ相关服务性能下降,会导致消息积压,同时导致其提供的Jsf接口性能下降,影响可用率
    根本原因:Jmq服务与Jsf服务未解耦,应该拆封,迁移其中的16个Jsf接口。

    2、隔离

    解耦是逻辑上的分割,但隔离是物理上的分割。比如常见的微服务架构,微服务把系统拆分很多业务子系统,各自独立开发、部署、通过RPC(比如JSF)或者MQ进行依赖调用。

    隔离使得系统间关系更加清晰,故障可以更加隔离开来,问题的发现与解决也更加快速,系统的可用性也更高。下面讲下常见的读写分类和线程隔离。

    1.1、读写分离

    读写隔离通常是指将读操作和写操作分离到不同的服务或实例中处理,比如常见的MYSQL数据库读写分离,这个就不细说了

    注意点:如果数据库主从存在延迟,需要根据业务评估是否可以读从库,比如支付金融行业需要数据强一致性,很多是读主库的

    1.2、线程隔离

    线程隔离通常是指线程池的隔离,在应用系统内部,将不同请求分类发送给不同的线程池,当某个服务出现故障时,可以根据预先设定的熔断策略阻断线程的继续执行

    比如JSF接口A和接口B共用相同的线程池,当接口A的访问量激增时,接口C 的处理效率就会被影响,进而可能产生雪崩效应;使用线程隔离机制,可以将接口A和接口B做一个很好的隔离。

    注意:隔离必须在低耦合的建设基础原则上进行才有意义。如果组件之间的耦合关系千头万绪、混乱不堪,隔离只会让这种情况乱上加乱。


    参考案例:

    案例1:
    一个应用提供N个相关联的接口,通过JSF线程池配置方法的并发大小参数concurrents,隔离开防止影响其他方法调用


    ❌待改进案例

    案例1:
    现象:数据库模糊查询,由于数据库量大,SQL未加索引,导致系统卡顿
    基本原因:未加索引,导致数据库cpu高,进而影响线上业务
    根本原因:数据库读写未分类,如果主从同步延迟可接受范围内,需要mysql读写分类,读的再慢也不能影响线上写逻辑,如果线上场景读也是简单的读操作

    3、依赖

    依赖原则是去除依赖、弱化依赖、控制依赖。多一个依赖多一分风险。能不依赖则不依赖,能异步弱依赖不要同步强依赖

    通过对核心链路内外部服务依赖进行治理,我们的目标是实现以下两个关键目标:

    1.非核心业务故障不影响核心业务:通过优化服务依赖关系,确保非核心业务的故障不会对核心业务造成影响。这可以通过输出服务、应用及场景的依赖关系来实现,包括强弱依赖关系的明确划分。同时,我们会定期进行全量强弱依赖验证,以确保核心服务、应用及场景相关上下游依赖的强弱合理清晰。

    2.提高系统的稳定性:通过弱依赖出现各类异常(包括但不限于超时、失败等)场景时的容错逻辑和应急预案,有效避免弱依赖故障对核心业务的影响。


    附:依赖关系和UMP服务可用率关系图


    4、异步

    异步可以认为是在隔离的基础上进一步解耦,将物理上已经分割的组件之间的依赖关系进一步切断,使故障无法扩散,提高系统可用性。异步在架构上的实现手段主要是使用MQ消息队列。对于那些必须确认服务调用才能继续下一步操作的应用不适宜异步调用。

    比如获取下传库房时效接口,通过异步的方式处理非黄金链路业务逻辑,比如订单时效全程跟踪发送、定时时效数据库持久化等。通过异步MQ发送消息的形式不影响接口核心流量


    参考案例:

    案例1:
    现象:promise对外提供的时效域JSF接口TP99抖动频繁
    根本原因:是因为发送JMQ抖动导致
    解决方案:把之前发送MQ强依赖改成弱依赖
    最终结果:接口tp99有明显下降并且无抖动跳点,性能比较平稳
    备注
    橘色代表改造前同步发送MQ消息,tp99在25ms以上并且抖动比较频繁
    蓝色代表改造后采用异步发送MQ消息,tp99保持平稳在10ms左右,并且没有抖动跳点



    5、重试

    超时是一件很容易被忽视的事情,超时控制的本质是fail fast,良好的超时控制可以尽快清空高延迟的请求,尽快释放资源避免请求堆积。

    对于网络抖动这种情况,解决的办法之一就是重试。但重试存在风险,它可能会解决故障,也可能会放大故障。

    需要注意的是,可以重试的服务必须是幂等的,否则是有风险的。所谓幂等,即服务重复调用和调用一次产生的结果是相同的。

    重试方式:同步重试、异步重试

    重试次数:应评估系统的实际情况和业务需求来设置最大重试次数:

    1. 设置过低,可能无法有效地处理该错误;
    2. 设置过高,同样可能造成系统资源的浪费

    1.1、重试算法策略:

    1. 线性间隔:每次重试间隔时间是固定的,比如每 1s 重试一次。
    2. 线性间隔+随机时间:加入随机时间可以在线性间隔时间的基础上波动一个百分比的时间,防止多个请求在同一时间请求
    3. 指数间隔:间隔时间是指数型递增,例如等待 3s、9s、27s 后重试。
    4. 指数间隔+随机时间:在指数递增的基础上添加一个波动时间

    1.2、重试风暴:

    ServiceA ---retry*3-----> ServiceB ---retry*3-----> ServiceC ---retry*3-----> DB

    通过上面调用关系简单介绍下重试风暴:这样在一次业务请求中,对DB的访问可能达到3^(n)次此时负载高的 DB 便被卷进了重试风暴中,最终很可能导致服务雪崩。

    应该怎么避免重试风暴呢?可采用限制链路重试

    1. 多级链路中如果每层都配置重试可能导致调用量指数级扩大;
    2. 核心是限制每层都发生重试,理想情况下只有最下游服务发生重试;
    3. Google SRE 中指出了 Google 内部使用特殊错误码的方式来实现。

    关于 Google SRE 的实现方式,大致细节如下:

    • 统一约定一个特殊的 status code ,它表示:调用失败,但别重试;
    • 任何一级重试失败后,生成该 status code 并返回给上层;
    • 上层收到该 status code 后停止对这个下游的重试,并将错误码再传给自己的上层。

    该方法可以有效避免重试风暴,但请求链路上需要上下游服务约定好重试状态码并耦合对应的逻辑。


    6、熔断

    重试主要解决偶发的因素导致的单次调用失败,但是如果某个服务器一直不稳定,甚至已经宕机,再请求这个服务器或者进行重试都没有意义了。所以为了保证系统整体的高可用,对于不稳定或者宕机的服务器需要进行熔断。服务熔断是在分布式系统中避免从系统局部的、小规模的故障,最终导致全局性的后果的手段。它是通过快速失败(Fail Fast)的机制,避免请求大量阻塞,从而保护调用方。

    熔断的主要方式是使用断路器阻断对故障服务器的调用,断路器状态图如下。

    断路器有三种状态,关闭、打开、半开。断路器正常情况下是关闭状态,每次服务调用后都通知断路器。如果失败了,失败计数器就+1,如果超过开关阈值,断路器就打开,这个时候就不再请求这个服务了。过一段时间,达到断路器预设的时间窗口后,断路器进入半开状态,发送一个请求到该服务,如果服务调用成功,那么说明服务恢复,断路器进入关闭状态,即正常状态;如果服务调用失败,那么说明服务故障还没修复,断路器继续进入到打开状态,服务不可用。


    参考案例:

    案例1:promsie调用gis进行熔断处理
    Sentinel熔断的降级处理方法,与Hystrix非常相似。只需要使用@SentinelResource注解的fallback属性来指定具体的方法名即可
    @Override
    @SentinelResource(value = GET_FENCE_RESOURCE,fallback = "getFenceIdFallback")
    public String getFenceId( GisAgingFenceRequest gisAgingFenceRequest){
        //内部代码逻辑
    }
    
     public String getFenceIdFallback(GisAgingFenceRequest gisAgingFenceRequest) {
        //内部熔断降级逻辑
    }

    7、降级

    降级是从系统功能角度出发,人为或自动地将某些不重要的功能停掉或者简化,以降低系统负载,这部分释放的资源可以去支撑更核心的功能

    目的是为了提升系统的可用性,同时要寻找到用户体验与降级成本的平衡点;
    降级属于有损操作。简而言之,弃卒保帅。

    降级策略:

    降级一般在应急预案、大促期间使用。降级策略如下:

    降级的注意点:

    1.每个服务都需要制定自己的降级策略,根据服务不同的优先级来设定降级方案,紧急情况下可以通过启用降级开关关闭非核心功能,损失一定的客户体验,以确保核心关键业务的服务可用性。

    2.对业务进行仔细的梳理和分析,哪些是核心流程必须保证的,哪些是可以牺牲的,干掉一些次要功能。比如电商功能大促期间可以把评论关闭或者简化评论流程

    3.什么指标下能进行降级:吞吐量、响应时间、失败次数等达到一个阈值才进行降级处理

    4.降级最简单的就是在业务代码中配置一个开关或者做成配置中心模式,直接在配置中心上更改配置,推送到相应的服务。比如DUCC开关技术。


    参考案例:

    案例1:Promise日常会通过详细地址获取GIS围栏信息,该接口tp99大概在120ms左右,在大促期间会通过ducc开关进行降级不调用,业务可接受的范围。

    ❌待改进案例

    案例1:线上出现问题后,无法通过人工干预的形式快速止血。其中的人工干预指的是ducc开关,界面配置等方式

    8、限流

    预期外的突发流量总会出现,对我们系统可承载的容量造成巨大冲击,极端情况下甚至会导致系统雪崩

    在高并发场景下,如果系统的访问量超过了系统的承受能力,可以通过限流对系统进行保护。限流是指对进入系统的用户请求进行流量限制,如果访问量超过了系统的最大处理能力,就会丢弃一部分用户请求,保证整个系统可用。这样虽然有一部分用户的请求被丢弃,但大部分用户还是可以访问系统的,总比整个系统崩溃,所有的用户都不可用要好。

    主流的限流算法有:

    1. 计数器法
    2. 漏桶算法
    3. 令牌桶算法(JSF限流模式之一)
    4. 滑动时间窗口算法(JSF限流模式之一)

    熔断&降级&限流:

    • 熔断和限流都可以认为是降级的一种方式
    • 降级依靠牺牲一部分功能或体验保住容量,而限流则是依靠牺牲一部分流量来保住容量。
    • 限流的通用性会更强一些,因为每个服务理论上都可以设置限流,但并不是每个服务都能降级。

    参考案例:

    案例1:服务被打挂了,应急处理标准处理流程:限流、重启和扩容
    1、首先确认限流是否生效,通过限流将自己保护住;
    2、限流生效之后再对服务重启,因为服务被打挂了之后线程被夯住,机器宕机,需要快速通过重启恢复服务;
    3、服务重启之后再是扩容

    限流:针对集群限流,可以通过自身系统的承载能力进行集群total限流,先保护好自己系统;针对应用进行限流,希望达到限流值80%左右的时候能有预警,前置处理以免影响到实际业务;


    案例2:关于限流如何配置
    比如服务A1、A2、A3、AN调用服务B。服务B要对A进行限流,那限流值配置多少合适呢?这个话题很难,没有标准答案,固定QPS这种限流方案存在痛点太多:以下是个人看法,不一定对
    1. 限流的目的是保护自己,服务B需要通过性能压测,自身能抗的最大量是多少(比如CPU达到60%的时候吞吐量)
    2. 如果服务B只提供1个JSF接口,则评估最大吞吐量比较简单。
    3. 如果是N个接口呢,没有特别好的办法,可根据N接口日常流量趋势图评估,但不一定要按流量比来压测,还是参考单个接口压测峰值来评估。因为哪天B服务新增加了1个接口,或者某个接口调用量增加了。
    4. 服务B需要针对total限流
    5. 核心服务A和B沟通限流值,可参考历史峰值,业务发展趋势图,峰值趋势图.
    6. 限流阈值会随着环境的变化而变化,比如内部接口改造等等
    7. 限流阈值提前报警,而不是等触发了限流才来报警,这时候已经晚了
    8. 如果是切量,则需要AB沟通切量的流量,随着切量比例越来越大,需要进行扩容,最终限流总值增加
    9. 切记注意别因为某个不起眼的接口打爆系统
    10. 限流值需要定期更新review维护
    案例3:相对与固定QPS限流机制,自适应限流机制是基于并发度进行限流,自适应限流算法在运行过程中会自动评估Provider单机性能瓶颈,根据Provider单机承载能力自动调整并发度完成限流。具体可参考JSF 再突破,1.7.8 支持自适应限流

    ❌待改进案例

    服务A和服务B沟通一个限流值,直接拍脑门配置。可能沟通的值本来就是错的,线上出现过好几次类似案例

    9、补偿

    补偿是在故障发生后,如何弥补错误或者避免损失扩大。比如将处理失败的请求放入一个专门的补偿队列,等待失败原因消除后进行补偿,重新处理。

    因为补偿已经是一个额外流程了,既然能够走这个额外流程,说明时效性并不是第一考虑的因素,所以做补偿的核心要点是:宁可慢,不可错

    做补偿的主流方式是事务补偿和重试,以下会被称作回滚和重试。

    补偿最典型的使用场景是事务补偿。在一个分布式应用中,多个相关事务操作可能分布在不同的服务器上,如果某个服务器处理失败,那么整个事务就是不完整的。按照传统的事务处理思路,需要进行事务回滚,即将已经成功的操作也恢复到事务以前的状态,保证事务的一致性。

    传统的事务回滚主要依赖数据库的特性,当事务失败的时候,数据库执行自己的undo日志,就可以将同一个事务的多条数据记录恢复到事务之初的状态。但是分布式服务没有undo日志,所以需要开发专门的事务补偿代码,当分布式事务失效的时候,调用事务补偿服务,将事务状态恢复如初。

    10、故障转移

    故障转移(failover)是一种高可用性策略,用于在系统组件发生故障时保持服务的连续性。以下是一些理论知识点:

    1. 定义:故障转移是指在活动服务或应用意外终止时,自动启用冗余或备用的服务器、系统、硬件或网络来接替工作的过程。
    2. 目的:其主要目的是确保系统的连续运行和数据的完整性,减少因系统故障导致的服务中断时间。
    3. 类型:故障转移可以分为热备(Hot Standby)和冷备(Cold Standby)。热备指的是备用系统随时准备接管工作,通常与主系统同步更新;冷备则是在主系统故障后才开始启动并接管工作。
    4. 实现方式:故障转移可以通过多种技术实现,如使用负载均衡器、集群技术或者具有冗余配置的网络组件。
    5. 应用范围:故障转移可以应用于各种系统组件,包括但不限于处理机、服务器、网络连接、存储设备等。

    参考案例:

    案例1:jimdb读操作的failover机制,比如jimdb的config7配置读组是s2,s4,s3,如果s2实例挂了,可读s4节点,s4节点挂了读s3节点


    ❌待改进案例

    案例1:JIMDB节点config读组配置单点s2实例,如果s2实例有问题,则无法故障转移

    11、缓存

    缓存一般用于高性能,但同样也适用高可用,在接口缓存是应对大并发量请求,提高系统吞吐量,保证系统可用性的有效手段。基本原理是,在系统内部,对于某部分请求参数和请求路径完成相同的请求的结果进行缓存,在周期时间内,这部分相同的请求的结果将会直接从缓存中读取,减少业务处理过程的负载。

    接口缓存同样有着它不适用的场景。接口缓存牺牲了数据的强一致性,对于实时性要求高的系统并不适用。接口缓存加快的是相同请求的请求速率,对于请求差异化较大的系统同样无能为力,过多的缓存反而会大量浪费系统内存等资源。


    参考案例:

    案例1:应用定时任务刷缓存的时候,如果缓存刷新时间较长,应该使用双缓存区,先将待刷缓存的集群流量摘掉切走,待刷完缓存之后再重新将流量切回来,再对另一个集群进行同样操作;使用本地缓存同理;


    ❌待改进案例

    案例1、不管业务系统如何,直接上分布式缓存Redis,本地缓存。架构其实是越简单越好,能不引入新的组件则不引入
    案例2、应用启动从redis加载数据到本地缓存,扩容期间机器较多,同时加载导致Redis压力大,并且本地缓存大小巨大,是否合理呢?
    案例3、用java注解的方式 把数据库数据存入redis缓存,但由于代码编写问题其实注解一直未生效,在流量峰值期间导致穿透打爆mysql数据库(来源COE报告)

    四、常见的架构模式

    1、主备复制&主从复制

    主从复制和主备复制只有一字之差,区别在于:主从复制模式中,从机要承担读操作

    主从复制要点:

    • 存在一主多从
    • 主机负责读&写,并定期复制数据给从机。
    • 从机只负责读。
    • 一旦主机宕机,可以通过人工手段,将其中一个从节点作为主节点。

    优点

    • 主从复制架构中,主机故障时,读操作相关的业务可以继续运行。
    • 主从复制架构中,从机提供读操作,发挥了硬件的性能。

    缺点

    • 主从复制架构中,客户端需要感知主从关系,并将不同的操作发给不同的机器进行处理,复杂度比主备复制要高。
    • 主从复制架构中,从机提供读业务,如果主从复制延迟比较大,业务会因为数据不一致出现问题。
    • 主从复制架构中,故障时需要人工干预。

    适用场景

    综合主从复制的优缺点,一般情况下,写少读多的业务使用主从复制的存储架构比较多。例如新闻网站这类业务,此类业务的读操作数量是写操作数量的 10 倍甚至 100 倍以上。


    参考案例:

    日常的mysql主从读写分类模式,这个没什么好讲的


    ❌待改进案例:

    目前有部分系统用mysql来存储日志
    MySql关系型数据库存储的是结构化数据,核心是为了保持存储数据的完整性和一致性,主要用于业务生产数据的存储,对于日志等非业务生产数据,不要存储到Mysql中(成本高,得不偿失),更不要同业务生产数据混用存储中间件,避免非业务数据查询/写入导致存储中间件性能下降影响到生产。
    日志这类非生产数据可以存储到HBase,定期自动清理

    2、集群&分区

    在主备复制和主从复制模式中,都由一个共性问题:每个机器上存储的都是全量数据。但是,单机的数据存储量总是有上限的,当数据量上升为 TB 级甚至 PB 级数据,单机终究有无法支撑的时候。这时,就需要对数据进行分片(sharding)。分片后的节点可以视为一个独立的子集,针对子集,任然需要保证高可用。


    3、冗余设计

    分布式系统中单点故障不可取的,而降低单点故障的不二法门就是冗余设计,通过多点部署的方式,并且最好是部署在不同的物理位置,避免单机房中多点同时失败。冗余设计不仅可以提高服务的吞吐量,还可以在出现灾难时快速恢复。目前常见的冗余设计有主从设计和对等治理设计,主从设计又可以细分为一主多从、多主多从。

    冗余设计中一个不可避免的问题是考虑分布式系统中数据的一致性,多个节点中冗余的数据追求强一致性还是最终一致性。即使节点提供无状态服务,也需要借助外部服务,比如数据库、分布式缓存等维护数据状态。根据分布式系统下节点数据同步的基本原理CAP(Consistency (一致性)、Availablity (可用性)、Partition tolerance (分区容忍性)三个指标不可同时满足),数据强一致性的系统无法保证高可用性,最典型的例子就是 Zookeeper保证了集群数据的强一致性,但是放弃了集群的高可用性。Eureka 点对点对等的设计保证了服务注册与发现中心的高可用性,但是牺牲了数据的强一致性,降级为数据的最终一致性。


    N + 2 就是说平时如果一个服务需要 1 个实例正常提供服务,那么我们就在生产环境上应该部署 1 + 2 = 3 个节点。大家可能觉得 N + 1 很合理,也就是有个热备份系统,比较能够接受。但是你要想到:服务 N + 1 部署只能提供热备容灾,发布的时候就失去保护了,并且如果其中1台机器故障则变单点了。

    从另一个角度来讲,服务 N + 2 说的是在丢失两个最大的实例的同时,依然可以维持业务的正常运转。尤其廊坊或者汇天机器过保机器故障概率大。


    五、高可用开发运维

    上面讲了很多通用的高可用架构原则和常见的行业架构模式,但回到文章开头我们高可用的目标,结合日常工作,大部分都是在现有系统上进行需求开发,如果保障日常需求开发上线的高可用呢?

    接着说说上面的 MTBF (平均失效间隔)吧。请各位想一下,影响服务MTBF的三大因素!

    1. 发布
    2. 发布
    3. 还是发布上线

    一般服务只要你不去碰它(代码&配置)一年都不会坏一次。上线发布更新越频繁,坏的可能性就越大。凡是 软件都有 BUG。发布新版本,新功能上线就是 MTBF 最大的敌人。关于开发运维从团队稳定性文化建设、日常需求关注点、发布流程、报警管理四个方面来说。

    1、团队高可用稳定性文化建设

    1. 首先小组团队核心功能业务,必须有Backup至少2+人掌握,否则人员单点是个最大问题

    1.1、地基要打牢

    稳定性建设工作重在预防,根据多年的工作经验,至少70%的线上故障都可以通过预防工作来消除。因此,在日常工作中,我们需要投入相应的精力来进行根基建设。所谓的根基建设,就是要把开发、测试和上线这三大流程做到透彻。包括:DesignReview、CodeReview、提测流程、上线流程、引流验证、性能测试等。

    1.2、工作在日常

    俗话说养兵一日,用兵一时。稳定性工作不是一蹴而就,而是日常的点点滴滴,一步一个脚印走出来的。

    需要团队人人参与、持续完善监控告警、检查每一个告警是否配置、及时消灭线上小隐患。可参考每周的稳定性会议。

    梳理:主动梳理团队的业务时序、核心链路流程、流量地图、依赖风险,通过这个过程明确链路风险,流量水位,时序冗余;

    技术债务治理:主动组织技术债务的风险治理,将梳理出来的风险,以专项的形式治理掉,防患于未然。但需要注意别由于治理而导致线上问题,需要加强引流验证比对。

    演练:把风险化成攻击,在没有故障时制造一些可控的故障点,通过演练来提高大家响应的能力和对风险点的认知。

    报警:除了前面说过的主动响应之外,还要经常做报警保险和机制调整,保证报警的准确度和大家对报警的敏感度。同时也要做到不疏忽任何一个点,因为疏忽的点,就可能导致问题。

    1.3、预案是关键

    我们需要认识到预案的重要性,并投入相应的精力来进行预案的制定和更新。这样,我们才能更好地应对各种突发情况,保障项目的顺利进行。通过每周的稳定性去深入挖掘每个接口的隐患及不足,比如业务指标是否加上、业务指标是否能真实反馈该接口的特性等。

    1.4、前置:扁鹊三兄弟

    与扁鹊三兄弟一样,如果想要让稳定性有价值,SRE同学一定不能站到系统的屁股后面等着擦屁股,必须走到前面,看到未来的风险。

    既要在发生问题时快速解决问题(做扁鹊)

    也要把风险归纳总结,推动解决(做二哥)

    还要在系统健康的时候评估链路,发现隐藏的问题(做大哥);

    2、日常需求开发

    1. 新需求上线,务必要确保不影响线上已有功能。读服务可通过引流回放比对方式来规避,写链路可能复杂点需要环境隔离。
    2. 功能降级:当出现故障的时候,可以将非核心功能直接降级,保护核心功能不受影响
    3. 开关机制:需求牵扯黄金流程上线必须带DUCC开关,如有问题开关可秒级恢复
    4. 新需求上线,需要思考如何确保高可用?比如这需求上线最坏情况是什么?我如何规避?如何发现 等等?
    5. 我们需要考虑系统的依赖性、发生故障的概率、故障发生的时间和故障影响的范围。这四个因素是设计高可用的关键因素。

    看到一些线上问题应急预案采用的是回滚方案,但是在大部分牵扯代码场景下,开关技术才是线上问题快速止血的最佳方式。如遇线上问题的话,采用通用的回滚方式需要5-10+分钟(500+台机器)并且回滚如果操作不当会加重问题,而采用开关技术则是秒级

    对于高频率的发布上线来说,开关技术是一种合理的技术手段,被赋予了两种新的用途。

    1.快速止血:一旦生产环境出了问题,直接找到对应功能的开关选项,将其设置为“关闭”。

    2.隔离:即将功能代码隔离在线上执行路径之外,对用户不产生影响。

    3、测试

    在高可用架构的构建过程中,测试环节扮演着至关重要的角色。它不仅是上线前的最后一道防线,更是确保系统稳定性和可靠性的关键所在通过全面的测试,我们能够及时发现并修复潜在的缺陷和漏洞,从而极大地降低了系统上线后出现故障的风险。因此,我们应当充分认识到测试工作的重要性,并将其作为产品质量保障的核心部分来对待。

    4、发布流程

    还记得影响 MTBF 最大的因子吗?发布质量不提高,一切都是空谈。流程是为了防止最差的情况发生,通过严格遵守流程,可确保在发布过程中尽量减少风险,提高系统高可用性。

    1、上线发布必须严格遵守流程checklist确认,并且建立doublecheck机制

    建立发布流程:

    流程可以确保最终结果不会太差,好的发布流程具有如下特性:

    1.完整性:完整地、一致地在各个环节内跟踪重要的细节问题

    2.可执行:相对简单,流程可落地,并且能避免最坏情况发生。

    3.可扩展性:可以应用在简单的发布上、也可以用在复杂的发布过程中

    建立CheckList清单

    检查列表可以提醒人遗漏的东西、用来减少失败,保持一致性和完整性。把checklist清单作为xbp流程中一部分,集成到了行云部署发布中,申请上线的时候必须填写。

    2、底层中间件、配置文件等变更的执行过程往往伴随着一系列的风险和挑战

    变更管理在稳定性建设中扮演着至关重要的角色。它涵盖了兼容设计、新版本发布计划、灰度变革、数据迁移、可回滚设计、配置变更控制和复核验证等多个方面,旨在确保系统在变更过程中的稳定性和可靠性。

    首先,兼容设计和新版本发布计划是变更管理的基础。通过充分考虑现有系统的功能和架构,我们可以预测并解决可能出现的兼容性问题。同时,制定详细的新版本发布计划,可以确保变更过程有序进行,避免对用户造成不必要的影响。

    其次,灰度变革和数据迁移是降低变更风险的重要手段。通过逐步引入变更,我们可以及时发现和解决问题,减少对整个系统的影响。而数据迁移则是确保用户数据安全和完整性的关键步骤,需要仔细规划和执行。

    另外,可回滚设计和配置变更控制是保障变更可控性的重要措施。可回滚设计意味着我们可以随时将系统恢复到变更前的状态,以应对可能出现的问题。而配置变更控制则可以确保变更过程的合规性和安全性,防止未经授权的变更发生。

    最后,复核验证是确认变更有效性和正确性的关键步骤。通过对变更后的系统进行全面的测试和验证,我们可以确保变更没有引入新的问题,并且达到了预期的效果。

    综上所述,变更管理在稳定性建设中起着至关重要的作用。通过合理的变更管理措施,我们可以降低变更带来的风险,确保系统的稳定性和可靠性。只有在充分重视和有效实施变更管理的前提下,我们才能够建立一个稳定、可靠的系统。

    3、上线发布必须遵守“发布三板斧”: 可灰度、可验证、可回滚

    复杂需求或者高风险需求的前提下,在架构设计阶段,应该将灰度计划、验证兼容和回滚策略等考虑在内,并做好评估与平衡。具体来说,需要考虑以下两个方面:

    1.风险程度:在评估系统稳定性和可靠性时,需要对可能出现的问题和风险进行充分的评估,并根据风险程度制定相应的灰度计划、验证兼容和回滚策略。

    2.成本投入:在进行灰度计划、验证兼容和回滚策略时,需要考虑相应的成本投入,包括人力、物力、时间等方面,以确保实施计划的可行性和经济性。

    综上所述,灰度计划、验证兼容和回滚策略等应该在架构设计阶段就进行充分的考虑和评估,以便在实施过程中能够做到有条不紊、稳妥可靠。


    5、可观察能力建设&快速止血恢复现场

    告警是监控系统中最为重要的一部分,可以帮助运维人员及时发现并解决问题,确保服务的可用性和稳定性。但目前很多团队都存在报警泛滥,狼来了的感觉,导致告警麻木了,这时候就需要告警治理了,达到快(第一时间发现问题)、准(报警有效性)、少(以防告警泛滥)的目标


    基本原则: 在故障处理过程中采取的所有手段和行动,一切以恢复业务为最高优先级,恢复现场止血方案高于寻找故障原因。

    消防队员到达事故现场,第一反应是救火,而不是查失火的原因。


    现在很多线上问题都是业务优先反馈,能不能技术先发现呢?可通过技术业务指标的建设……

    六、大促高可用保障

    正如文章开头说的在低峰期停机1分钟和高峰期停机1分钟,对业务影响的结果完全不一样在大促活动期间,高可用更是重中之重。回归问题本质,系统在大促的高可用性和日常的区别在哪呢?个人理解核心是两点:

    1、【技术】高并发流量:大促流量峰值是日常的N倍(几十、几百倍),需要具备更高的并发流量处理能力,以保证系统的高可用稳定性。

    2、【业务】业务场景多样化:大促会增加很多日常用不到的场景,很明显的比如预售场景等,需要确保业务高可用。

    针对上面的特性,除了进行备战事项(军演全链路压测,性能压测、预售场景验证等),大促要达到绝对高可用一般都是使用扩容机器冗余+降级非核心功能。


    1、容量规划

    容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡

    1. 流量模型评估
    2. 数据增长预测
    3. 容量应急预案

    2、军演全链路&性能压测

    1、军演全链路方案设计

    全链路压测技术方案的核心思路是压测数据隔离。通过对压测流量进行标识、中间件识别和透传压测流量的改造、选用合适影子技术持久化压测流量等手段以达到数据隔离的目的。

    实现全链路压测的核心步骤:

    1、生成带压测标识的压测流量。

    2、压测标识处理组件识别并透传压测流量,同时保证压测标识在被压测服务间传递不丢失。

    3、选用合适的影子技术,持久化压测流量(与生产存储介质物理或者逻辑隔离,风险可控、易于维护)。

    2、如何进行高保真压测,使压测结果更接近于线上真实性能表现?

    可使用R2平台录制线上流量进行高保真压测。

    3、预案演练

    预案演练主要解决的问题是:根据单个系统的应急预案,模拟应用系统的一种或多种故障场景,验证系统的高可用性。检验预案可落地性

    1.写的应急预案(计划预案、业务预案、突发预案)之前演练过吗?

    2.应急预案从问题开始(历史发生过什么?当下可能发生什么?)

    3.从目标切入(预案影响是什么?预案是否可落地执行?)

    4.从风险着手(最坏情况是什么?还有哪些风险点?)

    5.真正出问题了,如何第一时间快速止血,如何缩短MTTR平均修复时长。


    参考案例:

    案例1:运行时动态调整日志级别
    在应用运行时动态修改日志级别的功能。比如Promise在618&11大促峰值期间对日志进行降级(只打印出入参及下游依赖的出入参),TP99从30ms降低到13ms,待大促峰值过后日志调整回来,方便排查。


    案例2、弱依赖降级
    Promise日常会通过详细地址获取GIS围栏信息,该接口tp99大概在120ms左右,在大促期间会通过ducc开关进行降级不调用,业务可接受的范围。

    七、线上问题COE复盘

    对于每次的线上问题,都应该使用业界公认的COE(Correction of Error)复盘的方式。需要识别根因并做出改进,故障复盘黄金三问:

    1. 故障原因有哪些?根本原因是什么?可根据5W、鱼骨图等方式找到根本原因。
    2. 举一反三,杜绝下次发生类似问题,但不需要列一堆Action,根据2/8法则抓重点即可。
    3. 思考如果当时做了哪些可以更快缩短MTTR(Mean Time To Repair)的方法。

    复盘是从故障中学习并且改进杜绝问题再次发生,而不是回放当时的情景。

    八、业务高可用

    上面的案例说明了技术的高可用并不等于业务的高可用,那么业务的高可用是什么呢?

    以电商业务为例,个人认为业务的高可用可以用四个词概括:

    1. 正常访问:无论请求量多大,必须保证用户的正常访问和操作通畅,至少核心高频业务没问题(比如保证最起码的商品商详、结算页和支付业务可用)。
    2. 友好提醒:假设遇到上述的请求量过大需要排队,导致页面打不开或者卡单情况,也需要有友好的处理机制和提醒,降低用户的不友好体验。
    3. 异常冗余:如果遇到请求量过大排队,就需要及时的切流和扩容,或者在网关层限流,而不是放任人为的流量过高导致业务不可用时长拉长。
    4. 防止资损:业务的高可用还要注意预防资损。比如用户支付成功了,但是订单状态未更新导致下单失败。比如用户明明有优惠券,但是优惠券服务挂了导致无法使用优惠券,用户多付款。再比如发放优惠券没做规则限制,导致用户重复领取优惠券叠加使用甚至黄牛薅羊毛。

    个人认为,技术的高可用目标,一定是在保证业务的高可用的前提下才有意义,否则只会陷入技术的自嗨陷阱里。

    九、总结

    在本文中,我们深入探讨了打造高可用架构的关键要素,从系统设计的基本原则到发布上线过程中的最佳实践。我们了解到,创建一个能够抵御各种故障并确保持续服务的系统,不仅需要深入的技术知识、精心的规划和不懈的努力,还需要对人的素质和责任心有深刻的认识

    1. 高可用性不仅仅是一个技术问题,它更是一种哲学。海恩法则告诉我们,事故的发生是量的积累的结果这意味着在构建高可用架构时,我们必须关注每一个小细节,每一个小的疏漏都可能成为未来大问题的导火索。因此,我们需要一点一点地做好每件小事,无论是设计、编码、上线检查、变更管理、快速恢复还是持续监控。每个小的成功都是通往整体可靠性的一步。
    2. 再好的技术、再完美的规章,在实际操作层面也无法取代人自身的素质和责任心。我们需要建立一种文化,鼓励团队成员始终保持警惕,对可能出现的问题保持敏感,并准备好迅速应对。这种文化不仅要求我们对技术细节有深入的理解,还要求我们对自己的工作有高度的责任感。
    3. 高可用架构不是一次性的项目,而是一个持续的过程。我们必须不断地评估我们的系统,识别潜在的弱点,并采取措施加以改进。这种持续的努力和对细节的关注,以及对人的因素的重视,是确保系统长期稳定运行的关键。
    4. 通过本文的探讨,我们希望读者能够带走这样一个信息:构建高可用架构是一项既需要艺术感又需要纪律性的工作只有通过不断地优化每一个小细节,并且持之以恒地将这些小事做好,我们才能构建出真正强大且可靠的系统。让我们一起致力于这一点点的进步,最终实现系统的高可用性和业务的连续性。


    由于高可用架构这个知识点覆盖面广、挑战性大,本文也是结合日常实践经验,浅谈部分知识点,供大家参考,如里面信息不对请指正,如有更好的知识点评论交流。谢谢!


    参考文献

    1. SRE Google运维解密 书籍
    2. 信通院稳定性建设指南 书籍
    3. 持续交付 书籍
    4. 高可用架构 书籍
    5. SRE口中的3个9,4个9:https://blog.csdn.net/qq_41453285/article/details/126111711
    6. 面向业务的高可用架构设计:https://xie.infoq.cn/article/dfa60de7fd1f521a3e9c6de04
    7. 高可用架构设计的7大核心原则:https://blog.csdn.net/m0_37578675/article/details/118342360
    8. 架构之高可用: 如何保证高可用性?https://java.isture.com/arch/base/arch-y-ensure-high-availability.html
    9. 高可用架构的十种武器:怎么度量系统的可用性?https://learn.lianglianglee.com/
    10. 高可用流量治理核心策略:https://mp.weixin.qq.com/s/yaCgQlZp1sfZhfJU_Qu67A
    11. 系统高可用架构:https://dunwu.github.io/design/pages/9a462f/
    12. Google高可用架构理念与实践http://www.360doc.com/content/16/0104/18/7353658_525443371.shtml
    13. Why you should develop a COE:https://aws.amazon.com/cn/blogs/mt/why-you-should-develop-a-correction-of-error-coe/