如何建设全面的高可用及容灾架构体系,是一个涉及到广泛领域的话题,将分成上、下两篇呈现给读者。本文讨论的架构体系,解耦具体产品实现,尽量只从架构原理出发,从构建一个韧性的应用基础架构内核开始,到增强应用高可用架构能力,加上弹性与稳定的云化基础设施,共同形成稳定性基础架构。在后续的下篇推文中,将在此架构基础上再构建出完整的故障与危机的处理能力,同时通过持续运营与组织保障机制的协同,打造出全面的高可用及容灾架构体系。
高可用及容灾架构体系化建设缘起
2022年8月8日:谷歌,数据中心爆炸事故,导致核心业务搜索引擎、 Gmail 等中断,时长 41 分钟;
2021年11月2日:字节跳动,机房网络故障,抖音和飞书故障,引发微博热搜;
2021年10月4日:Facebook,配置变更故障中断 6 小时,影响 26 亿用户,收入减少 5.1 亿元,市值减少 500 亿美元;
2020年11月25日:AWS,Amazon Kinesis 服务扩容引起雪崩效应,影响多个关联云服务,时长十数小时;
……随着数字化、互联网、云计算等技术的快速发展和普及,各行业越来越以更信息化的形态服务大众,信息化系统已经越来越深入到我们生活的方方面面,在促进各行业发展的同时,也强化了我们对其的依赖程度。而我们除了要面对由于自然灾害、电力中断、网络故障等“黑天鹅”事件引起的系统容灾场景外,还会面临由于人为疏漏、系统复杂度增加、架构劣化等带来的“灰犀牛”故障事件。
在这些充满着不确定性的现实场景中,需要一套架构体系框架来指导我们通过技术手段建立起具有高度确定性和稳定性的业务系统。当前在高可用、容灾、业务连续性这些关联领域中,已经存在着很多架构理论、技术方法、产品组件,在本文中,尝试构建起一个全面的高可用及容灾架构体系模型,串联起相关领域的技术理论方法,形成一个能够对云上业务连续性建设产生有效指导作用的架构模型体系。
怎样建设稳定性基础架构
我们先从稳定性基础架构的建设说起。所谓的稳定性基础架构,就是构建在具备弹性和可扩展能力的云化基础设施之上,进行了良好韧性应用架构设计,并且具备关键高可用架构特性(通常进行委托化处理)的基础架构设计方案。
构建韧性的应用基础架构
应用架构是一个业务系统的技术核心,下面从可靠性、可扩展性和可演进性三个与稳定性为相关架构属性,从设计理论和思路上展开讨论一下,该如何来构建应用架构自身的韧性能力。
1、可靠性
随着现代应用系统逐步向着规模化和复杂化的趋势发展,伴随着系统的长期运行,一些人为的疏忽犯错、代码中的 bug、物理服务器宕机、网络中断堵塞等问题,不可避免地会对系统产生影响。
在这种情况下,一次业务请求要经过多个“不稳定”的服务相互协作来完成,其中的误差就会不断地累积叠加,使得终结果不能收敛稳定。那么从应用架构层面来看,如何用一些不可靠的组件来构造出一个可靠的系统呢,这也是冯·诺依曼的《自复制自动机》中讨论的一个理论。
以生命系统为例,把“不可靠组件”理解为构成生命的细胞分子,由于热力学扰动、生物复制差错等因素干扰,这些分子本身是不可靠的。但生命系统本身的可靠性,是来自于使用不可靠的部件来完成遗传迭代。就是承认细胞等这些组件可能会出错,某个具体的组件可能会崩溃消亡,但在生命的生态系统中一定会有其后代的出现,重新代替该组件的作用,以维持系统的整体稳定。
应用架构风格从大型机、单体、SOA、微服务、服务网格到 Serverless,呈现出逐步小型化的发展趋势。这里面除去微服务带来的简化部署、逻辑拆分解耦、技术异构、更高的性能弹性等好处外,架构演变还有一个重要的驱动力,就是为了方便每个服务能够顺利地完成迭代更新而设计的,这也就是系统整体层面能够保持可靠续存的关键。
在微服务架构的视角下,可以通过不停机进行线上滚动更新、切流、测试、金丝雀发布等技术手段,来对应用缺陷与变更进行处理。在这种情况下,即使其中某些服务中存在有重大缺陷的代码,存有严重的资源占用问题,服务 5 分钟就可能会崩溃。但只要在整体架构设计有恰当的、自动化的错误熔断、服务淘汰和重建的机制,在整体上就仍然可表现出稳定和健壮的服务能力。
所以,微服务架构是我们实现应用架构可靠性的一个重要手段,但在微服务架构设计中,我们也会面临很多的选择和决策:
● 如何进行领域的设计(业务全景 usecase、鲁棒图分析模型推演、模型对象分类优化、按模型划域)
● 如何进行开放性的设计(多态业务的共性和个性实现处理机制)
● 如何进行全面的解耦设计(粒度,语义独立性、依赖倒置、业务模型和存储模型、配置态和运行态)
● 如何进行应用代码结构的分层(api、biz、domain、dao)
具体应该如何合理的围绕业务能力构建自己的服务,进行合理微服务的拆分和架构的设计,这里暂不做展开论述。
2、可扩展性
另外一个会直接影响应用稳定性的架构因素就是可扩展性,应用系统的负载会随着业务以某种方式增长,我们需要有相应的应对措施来处理这些逐步增加的业务负载。常规的扩展性方案有垂直扩展(升级到更高性能的硬件)和水平扩展(将负载分布到多个廉价低配置的机器)。垂直扩展通常更简单,但成本会很高,且扩展水平有限,终往往还是无法避免需要水平扩展。
水平扩展需要要求系统为无共享体系结构,这就是说无状态的应用服务扩展至多台机器相对比较容易,而有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加。出于这个原因,通常的作法是将应用服务设计成无状态服务,同时将服务依赖的数据库运行在一个节点上(采用垂直扩展策略),直到高扩展性或高可用性的要求迫使不得不做水平扩展。
除了常规的重直扩展和水平扩展,比较通用的还有 AKF 可扩展架构模型。在这个模型中,通过 3 个维度进行架构扩展,我们通常将其描述为有一个有三维轴线的立方体,称其为 AKF 扩展立方体。AKF 扩展立方体的 X 轴,代表的是无差别地克隆服务和数据,也就是上文提到的无状态应用服务多机部署。Y 轴表示的是面向服务或资源的分割,即把任何特定业务活动或成组的紧密相关的的业务活动,以及完成业务活动所需要的信息和数据从其他业务活动和流程中分离出来,把整体的业务流程切割成流水线的工作流或异步并行的处理流,这部分可以对应微服务的拆分与设计。Z 轴通常是基于某种业务特征(比如客户 ID,或者客户所归属服务区域)来进行分割,这种方式可以形成比较理想的无共享体系架构,将服务依赖的数据库也进行基于业务特征进行拆分,使分离出来的业务单元具备完整、独立的业务处理能力。
除了上述通用场景下的架构可扩展性设计方法,针对具有超大规模的特定应用系统,还需要考虑数据读取量、写入量、待存储的数据量、数据的复杂程度、业务请求的响应时间要求、业务请求访问模式等,以及上述各种因素的叠加,再加上其他更复杂的问题,往往有别于通用的架构,需要更加定制化的可扩展方案。
只有在架构层面对于随着业务发展而带来的负载压力增长情况有了充分的设计考虑和应对策略后,应用系统才能具备稳定的架构基础。
3、可演进性
通常来讲,对于软件系统可以通过行为和架构两个维度来体现它的实际价值。其中行为价值是软件系统直观的价值维度。开发人员通过让机器按照某种指定方式运转来给系统的使用者创造价值或者提高利润。而架构维度,更多是体现在软件应该更易于改变,架构应该更具备可演进性。架构的可演进性不但对于稳定性甚至对于软件系统的整体价值都是具有决定性的重要意义。
假设一个极端的场景,如果某个程序可以正常工作,但是无法修改,那么当需求变更的时候它就不再能够正常工作了,我们也无法通过修改让它能继续正常工作,因此这个程序的价值将成为 0。反之,假如某程序目前无法正常工作,但是我们可以很容易地修改它,那么将它改好,并且随着需求变化不停地修改它,因此这个程序还会持续产生价值。
下面我就通过控制复杂度、持续迭代和架构度量几个方面来简单讲一下保持和改善架构可演进的思路。
控制复杂度
一般小型软件项目写出简单而优雅的代码是比较容易的,但随着项目规模的变大,代码也会越来越复杂和难以理解。这种复杂性会降低开发效率,增加维护成本。
复杂性会有多种表现方式,比如:状态空间的膨胀,模块紧耦合,相互纠缠的依赖关系,命名和术语的不统一,为了性能而采取的特殊处理,为解决特定问题而采用的特殊框架等。复杂性会使后续的维护和扩展变得越来越难,终会导致成本失控和系统难以修改。并且通过变更引入潜在错误的风险显著加大,终使得研发人员更加难以准确理解、评估或者更加容易忽略相关系统行为,包括背后的假设、潜在的后果,设计之外的系统交互等,使得系统稳定性大大下降。
控制复杂度一个主要手段就是保持系统设计简单性,这并不是要减少系统功能,而主要目标是消除意外的复杂性。即并非软件所固有,而是实现本身所衍生出来的复杂性问题。消除意外复杂性方法之一是抽象。良好设计的抽象可以隐藏大量的实现细节,并提供简洁、清晰的接口。良好设计的抽象可用于不同的应用复用,这样也会比重复实现更有效率,同时也带来更高质量的软件,而高质量的抽象组件也可以使运行其上的应用获益,围绕业务能力进行的微服务设计也是一种高维度的抽象设计实践。
持续迭代
业务系统不会是一直不变,因为系统需求和目标总是在不断变化中,例如:
● 外部环境变更带来适配工作
● 新的业务场景需求
● 业务优先级的变化
● 用户新的功能需求
● 法律或监管要求的变化
● 业务增长促使的架构演进
敏捷开发模式为适应变化提供了比较好的实践参考,在社区中有也一些类似于 TDD 和重构相关的技术工具和模式,以帮助在频繁变化的环境中进行系统开发。
架构度量
由于业务系统一直处在持续不断的迭代演进过程中,那么我们如何保障我们初的架构设计没有随着系统演进而变得劣化呢。可以通过将有助于验证架构问题的一些现有机制来构建适应度函数,包括传统的测试、监控等工具。将这些概念统一为一个整体机制,统一考虑许多现有的“非功能性需求”测试。收集重要的架构评估项和架构阈值形成架构质量模型,将评价标准通过架构质量模型变为更加具体架构度量标准。
代码架构指标示例
● 体量维度:过大的组件
● 耦合维度:枢纽组件,过深调用,循环依赖
● 内聚维度:霰弹式修改
● 冗余维度:冗余元素,过度泛化
基于 C4 模型的可视化分析
● 上下文
● 容器分析
● 组件分析
● 代码分析
除了研发域的代码架构度量,在涉及架构特性、架构能力、技术运营和组织保障等方面的整体的云化架构层面,可以通过云化可持续架构质量管理模型来进行度量和改进。
增强应用的高可用架构能力
在微服务时代中,应用研发本身的复杂度应该说是有所降低,利用微服务框架通过一致的接口、声明和配置,进一步屏蔽了源自于具体工具、框架的复杂性,降低了在不同工具、框架之间切换的成本。所以,作为一个普通的服务开发者,微服务架构减少了开的发复杂性。可是,微服务对架构设计者来说却是充演了挑战。
当微服务的规模越来越大的时候,复杂度也会指数级增长,随之而来会面临以下两类稳定性相关问题的困扰:
● 服务容错:由于某一个服务的崩溃可能会导致所有用到这个服务的其他服务都无法正常工作,一个故障经过层层传递,会波及到调用链上与此有关的所有服务,造成雪崩效应。如何微服务架构设计中没有处理好服务级联依赖中的雪崩效应,那么服务化程度越高,整个系统反而越不稳定。
● 流量控制:由于处理能力有限,服务虽然没有崩溃,但在面临超过预期的突发请求时,大部分请求直至超时都无法完成处理。这会造成类似交通堵塞现象产生的后果,如果没有进行及时的处理,就需要长时间才能使全部服务都恢复正常。
围绕应用架构所面临的稳定性问题,我们需要考虑设计服务容错、流量防护、评测演练等一系列高可用架构解决方案,通过这些措施相互关联配合,共同增强应用的高可用架构能力。
1、度量
服务可观测能力
相对于原来的单机系统,在分布式环境下如果要回答清楚服务出了什么问题、哪些服务质量未达标、故障的影响范围、以及变更对服务指标产生的影响等信息,就需要对整条业务链路上多个服务或组件上的信息做收集和关联,要求系统具备更强的可观测能力。
这种可观测能力要求能够在的分布式系统中,主动通过日志、链路跟踪和度量等手段,让一次业务请求背后的多次服务调用的耗时、返回值和参数都够清晰可见,也可以下钻到每次三方软件调用、SQL 请求、网络响应等,这样才能够掌握系统的实时运行情况,并结合多个维度的数据指标进行关联分析,不断对业务健康度和用户体验进行数字化衡量和持续优化。
在可观测能力的建设需要关注以下三个方面:
● 日志(Logging):日志的主要目的是记录离散事件,然后通过记录事后分析程序的行为,譬如曾经调用过的方法,操作过的数据等等。输出日志的关键点是格式标准化,以便于后续的收集和分析。在大规模复杂系统环境下,会面对成千上万的集群节点,面对迅速滚动的事件信息,面对数以 TB 计算的文本记录,这里需要解决传输、归集和海量数据存储以及查询的挑战。
● 追踪(Tracing):追踪的主要目的是排查故障,在单体系统中基于本机的栈追踪很容易做到,但在微服务系统中,一个外部请求需要内部若干服务的联动响应,这时候完整的调用轨迹将跨越多个服务,同时包括服务间的网络传输信息与各个服务内部的调用堆栈信息,因此,针对分布式系统中的“全链路追踪”能力就尤为重要了。
● 度量(Metrics):度量的主要目的是进行监控和预警,主要通过对系统中某一类信息的统计聚合来实现,比如在 java 中由虚拟机直接提供的 JMX 就可以对诸如内存大小、各分代的用量、峰值的线程数、垃圾收集的吞吐量、频率等指票进行度量。如某些度量指标达到风险阈值时触发事件,进行自动处理或者提醒管理员介入。
除了基础的可观测能力的建设外,还需要重点关注的就是观测和度量目标的选择,这里面的关键点是要从与业务价值和用户关心的方面入手来选择目标,然后再反向推导出具体应观测的指标,而不能够仅仅是从可以简单度量的指标入手。所以比较理想的方式是,根据业务价值自上而下的设计可观测体系,从业务指标监控、应用性能及链路追踪监控到系统资源监控一路推导下来,使终的 SLO 能够更好的发挥作用。
容量评测与规划
服务容量对系统稳定性来说是至关重要的,所以针对服务容量需求的预测和规划就要求能够保障一个业务有足够的容量和冗余度去服务预测中的未来需求。同时一个业务的容量规划,不仅仅要包括自然增长(用户使用和资源用量正比增长),也需要包括一些非自然增长的因素(新功能的发布、商业推广(大促),以及其他商业因素在内)。服务容量规划思路上,我们需要从依赖关系、性能指标和优先级等几个方面来进行综合考虑:
● 依赖关系
因为一个应用服务在运行时往往需要依赖很多其他基础设施和相关服务,所以部署位置的选择要受到相关依赖服务的可用信息影响。假设我们有一个服务 A,依赖某个基础设施存储服务 B,A 要求 B 服务必须处丁 30ms 的网络延迟范围之内。这就对 A 和 B 的位置提出了要求,资源规划过程必须将这条限制纳入考虑范围。另外,通常服务依赖都是嵌套的,设服务 B 要依赖一个底层的分布式存储系统 C,和一个查询处理系统 D。我们布要考虑 B、C、D 来决定在哪里放置 A。在某些情况下,不同系统可能依赖同一个系统,但是服务目标和要求不同。
● 性能指标
性能指标是依赖关系中的黏合剂, 在上面的场景中,A 服务需要多少计算资源以服务 N 个用户需求?我们需要 B 服务提供多少 Mb/s 的数据以服务 N 个 A 服务的用户请求?通过性能指标,我们将一种或多种高阶资源类型转换成低阶资源类型,并通过对整个依赖链的梳理帮助我们规划出大致的需求优化计划,性能指标都常需要负载测试和资源用量监控来获得。
● 优先级
服务资源不足是在某些情况必须面临的问题,在这种情况下,需要考虑哪些资源请求可以被牺牲掉?比如:可能A服务的 N+2 冗余度要比 B 服务的 N+1 冗余度更重耍,或者某个功能 X 的上线没有 B 服务的 N+1 需求重要(意思是必须保证 B 服务有足够容量)。规划过程强制使这些计划变得更为透明、开放和一致,同时让资源使用限制的妥协和规划,对服务相关人员更加透明。
性能评估的步骤
● 确定流量模型:必须有一个准确的自然增长需求预测模型和非自然增长的需求预测模型的统计(大促下单链路:首页(12w/s)->商品列表(4w/s)->商品详情(2w/s)->加购(1w/s)->下单(3w/s))
● 制定验收标准:确定系统服务能力目标(例如:下单链路 10000TPS, 系统水位 50%, RT 500ms 以内, 成功率 99.9%)
● 梳理系统开关:是否存在在大促态和日常态需要变更的开关或预案?(比如:下单链路降级掉历史商品购买数量)
● 评估应用性能:全链路压测+性能基线,将系统原始资源信息与业务容量对应起来(全链路压测 + 性能基线)
● 资源容量评估:基于流量和水位的容量规划方法,可以通过流量合并将系统水位和 TPS 建立起来近似的线性对应关系,来大大减化容量评估的难度。
2、防护
流量控制
即便我们已经做了较为完善的容量评测与规划,也还是要做好相应的兜底保障措施,当系统资源不足以支撑外部超过预期的突发流量时,进行相应的取舍,建立起面对超额流量自我保护的机制,也就是微服务中的流量控制措施即限流措施。
● 流量统计指标
要做流量控制,首先需要了解哪些指标能反映系统的流量压力大小。我们先来理清经常用于衡量服务流量压力的三个指标的定义:
- 每秒事务数(TPS):TPS 是衡量信息系统吞吐量的终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。
- 每秒请求数(HPS):HPS 是指每秒从客户端发向服务端的请求数。如果只要一个请求就能完成一笔业务,那 HPS 与 TPS 是等价的,但有些场景里一笔业务可能需要多次请求才能完成。
- 每秒查询数(QPS):QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那 QPS 和 HPS 是等价的,但在分布式系统中,一个请求的响应往往要由后台多个服务节点共同协作来完成,这里面每次访问都要消耗掉一次或多次查询数。
以上这三个指标都是基于调用计数的指标,理想上是能够基于 TPS 来限流。但是由于不同的业务操作对系统的压力差异较大通常不具备可比性;并且真实业务操作的耗时还可能受限于用户交互带来的不确定性。所以直接针对 TPS 来限流实际上不容易操作的。而使用 QPS/HPS 作为限流指标比较常见,它相对容易观察统计而且能够在一定程度上反应系统当前以及接下来一段时间的压力。
基于调用计数的限流指标在实际应用中,需要从流量全链路角度关注下面几点因素:
- SLB/DNS 流量不均
- 链路流量有损,后端应用服务能力无法大化
- 链路流量调用关系非等比,分布式服务中有短板
这样,就形成了一个流量控制的漏斗模型,所以要整合接入层、应用网关、微服务等多层限流策略和模型以及相关的服务容错保护机制,来形成完整的流量控制策略。
● 限流设计模式
限流设计模式需要重点关注 QPS 计算的平滑度和准确性。
流量计数器模式
限流基础的方法就是根据当前时刻的流量计数结果是否超过阈值来决定是否限流,这种直观的方法经常被一些简单的限流系统所使用,但它并不严谨,主要缺陷根源在于它只是针对时间点进行离散的统计。
漏桶模式
漏桶模式是参考计算机网络中的流量整形的理念,当网络报文的发送速度过快时,首先在缓冲区中暂存,然后再在控制算法的调节下均匀地发送这些被缓冲的报文。常用的控制算法有漏桶算法和令牌桶算法。
漏桶算法:
- 应对请求到达速率不定的情况
- 漏桶以恒定速率通过请求
- 超过漏桶容量即丢弃请求
大限度保证流量的平滑度,有突增流量,且不连续时,可能出现提前限流。
令牌桶模式
令牌桶算法:
- 应对请求到达速率不定的情况
- 以恒定速率下放令牌到令牌桶,超过容量,丢弃令牌
- 从令牌桶中以不定速率拿令牌,拿到令牌则通过
- 未拿到令牌则丢弃
面对突发流量时,大限度允许系统根据的消费速度处理请求,能力大化,令牌桶消耗完时,控制速率。
滑动时间窗模式
滑动窗口算法在计算机科学很多领域中都有着广泛的应用,像 TCP 协议的中的流量控制、服务容错中对服务响应结果的统计、流量控制中对服务请求数量的统计等等。
滑动时间窗口模式的限流解决了流量计数器的缺陷,可以保证任意时间片段内,只需经过简单的调用计数比较,就能控制住请求次数一定不会超过限流的阈值。不过,虽然能滑动窗口模式很好的保证流量的平滑度,但有突增流量且不连续时,还是有可能出现提前限流。
优化策略:
- 在具体实现上,在基础于滑动窗口实现的基础上,支持动态调整滑动窗口格子数,以实现更好的平滑度、准确度(注意性能以及内存占用上的平衡)
- 流控策略不分桶,允许突发流量,格子越大,允许突发流量概率越大
- 使用优化的平均值算法(推荐 Cumulative moving average)
● 分布式限流
前面讨论过的那些限流算法,主要是使用在单机场景的,但在微服务架构下,无法细粒度地管理流量在内部微服务节点中的流转情况。所以,我们把前面介绍的限流模式都统称为单机限流,把能够精细控制分布式集群中每个服务消耗量的限流算法称为分布式限流。
分布式限流面对的一些特殊场景:
- 限流精度要求高、流量不均导致单机流控效果不佳
- 接口阈值小于应用机器数
- 应用机器数经常变动,单接口需要设定恒定阈值(如应用网关)
分布式限流的核心差别在于如何管理限流的统计指标,由于分布式限流的目的就是要让各个服务节点的协同限流,所以需要将原本在每个服务节点自己内存当中的统计数据给开放出来,让全局的限流服务可以访问到。常规方案可以通过分布式缓存或者专门的分布式限流服务,来解决这些数据的读写访问时并发控制的问题,但代价是每次服务调用都必须要额外增加一次网络开销,效率比较低,流量压力大时,限流本身可能会限制系统的处理能力。
服务容错
容错性设计源自于分布式系统的本质是不可靠的,为了避免服务集群中出现的任何差错导致的系统崩溃的风险,面向失败的服务容错设计就成为必要的措施。
● 容错策略
容错策略指的是当出现故障时我们该做些什么(what),常见的容错策略有以下几种:
故障转移:高可用的服务集群中服务均会部署有多个副本。故障转移是指如果调用的服务器出现故障时系统会自动切换到其他服务副本,从而保证了整体的高可用性。
快速失败:在不允许做故障转移的业务场景下(服务要求具备幂等性),此时就应该以快速失败作为容错策略。就是尽快让服务报错,避免重试,尽快抛出异常,由调用者自行处理。
安全失败:在一个调用链路中的服务通常也有强依赖和弱依赖之分,对于失败了也不影响核心业务的弱依赖逻辑,容错策略可以在调用失败的情况下自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可,这种策略被称为安全失败。
沉默失败:如果请求需要等到超时才宣告失败,很容易由于堆积而消耗大量的资源,进而影响到整个系统的稳定。这种情况下的失败策略是当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,将错误隔离开来,避免对系统其他部分产生影响,此即为沉默失败策略。
故障恢复:是指当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。故障恢复也是要求服务必须具备幂等性的,为了避免在内存中异步调用任务堆积,也应该有大重试次数的限制。
● 容错模式
容错模式指的是当出现故障时我们该如何去做(how),常见的容错模型有以下几种:
断路器模式
断路器就是通过代理接管服务调用者的远程请求,持续监控并统计服务返回的成功、失败、超时、拒绝等各种结果,当达到断路器的阈值时,它状态就自动变为“OPEN”,后续此断路器代理的远程访问都将直接返回调用失败,通过断路器对远程服务的熔断,避免雪崩效应的出现。断路器本质是一种快速失败策略的实现方式。
主动服务降级模式
这种容错模式不是在出现错误后才被动执行的,许多场景里面是指需要主动迫使服务进入降级逻辑的情况。例如,出于应对可预见的峰值流量,或者是系统维护等原因,要关闭系统部分功能或关闭部分弱依赖服务,这时候就有可能会主动进行这些服务降级。除了容错外,服务降级也可算作为流量控制的措施和手段。
隔离模式
隔离模式符合容错策略中沉默失败策略,调用外部服务的故障中“超时”引起的故障尤其容易给调用者带来全局性的风险。这是由于目前主流的网络访问大多是基于 TPR 并发模型,请求会一直占用着某个线程不能释放,而线程是整个系统的全局性资源,为了不让局部失败演变成全局性的影响,就必须设置种止损方案,常见的实现方式有线程池隔离和信号量机制。
重试模式
故障转移和故障恢复策略都需要对服务进行重复调用,差别是这些重复调用是同步的还是异步的;是重复调用同一个服务还是会调用到服务的其他副本。这些都重试设计模式的应用范畴,可以遵循一些共同的规范。
- 仅在强依赖逻辑的关键服务上进行同步的重试,不是关键的服务一般不做重试。
- 不要对致命错误重试,只对瞬时故障导致故障进行重试。
- 重试的服务必须具备幂等性。
- 重试必须有明确的终止条件,常用的终止条件有两种:
- 超时终止:重试模式需要配合超时机制来使用,避免重试的副作用。
- 次数终止:重试必须要有次数限制。重试不仅会给调用者带来负担,对于服务提供者也是同样是负担。
3、演练
由于大规模分布式系统会包含大量的复杂的交互和依赖,潜在可能出错的地方很多,任何一个异常情况处理的不好就有可能导致出现严重业务问题、性能问题,或者其他各种无法预料的异常行为。既然我们不能完全避免这些故障的发生,那就需要能够尽早地识别出系统中脆弱的、易出故障的环节。同时有针对性地进行加固、防范。
故障演练的实践理论基础:混沌工程
随着分布式系统的逐步演进和发展,我们会面临着各种类型的问题,比如:
● 分布式系统日益庞大,很难评估单个故障对整个系统的影响。
● 服务间的依赖错综复杂,隐藏很多配置不合理的问题。
● 请求链路长,监控告警日志记录容易有遗漏。
● 业务和技术迭代速度快,引入不确定性风险。
● 基础设施的老化产生越来越多的隐患。
● 系统中的人员角色众多、分工协作复杂以及人员变动频繁带来的对系统整体认知程度下降。
● ......
混沌工程是一门实践中形成的,在分布式系统上进行实验的学科, 能够建立对系统抵御生产环境中失控条件的能力以及信心,是解决上述问题提供一种工程实践思路。
混沌工程的实践原则
● 建立一个围绕稳定状态行为的假说
- 关注业务状态可测量输出,而不是系统内部属性
- 短时间内的度量结果,代表了系统的稳定状态
- 验证系统是否工作,而不是如何工作
● 基于多样的真实世界事件的进行验证
- 任何能够破坏稳态的事件都是混沌实验的一个潜在变量
- 通过潜在影响或预估频率确定事件的优先级
● 在生产环境中运行实验
- 系统的行为会根据环境和流量模式而变化
- 为了保证系统行为的真实性与当前部署系统的相关性,混沌工程强烈推荐在生产环境中进行实验
● 持续自动化运行实验
- 手工运行实验是不可持续的工作,所以需要把实验变为自动化且持续的执行
● 小化爆炸半径
- 在生产环境中进行实验可能会引发真实的故障发生,所以在执行试验时需要确保影响范围小化且可控
故障模型的设计
在故障模型的设计中,我们可以通过下面几个维度来进行定义:
● 实验靶点
实验作用的目标组件:CPU、磁盘……
● 实验范围
实验作用的范围:机器、集群、容器……
● 规则匹配
匹配符合实验规则的请求:端口、进程……
● 实验行为
执行的实验规则:延迟、异常……
故障演练的范围及验证目标
故障演练的类型范围以及验证目标,需要根据业务系统的实际情况来制定,下面更举一些基础和通用场景:演练的故障类型范围
● 变更类故障
业务变更故障:代码变更/配置变更/开关预案变更
基础设施变更故障:流量调度变更/数据存储变更
● 异常类故障
系统资源类
业务类
依赖服务类
中间件、存储类
● 基础设施类故障
物理机、容器故障
网络通讯故障
运维系统故障
演练的验证目标
● 验证业务功能异常处理
● 验证监控告警覆盖度
● 验证失败重试策略有效性
● 验证降级熔断策略有效性
● 验证异常隔离与流量调度
● 验证自动化编排部署
● 验证自动化水平扩容
故障演练的实施流程
下面是故障演练的实施流程的通用框架,在业务系统中进行实际调整和扩展:
混沌工程理论流程
1)定义稳态,提出假设
2)进行实验
3)验证稳态
故障演练实际实施流程
1)剧本定义
● 触发条件
- 链路
- 系统
- 触发请求
● 监控
- 指标
- 影响范围
● 故障场景
- 数据模型
- 故障类型
- 故障范围
2)准备阶段
开启流量
确认环境
……
3)执行阶段
执行故障注入
……
4)检查阶段
● 业务表现
- 接口状态
- 接口RT
- 返回数据结果
● 监控
- 指标波动
- 告警信息
● 日志
- 错误信息
5)恢复阶段
清除故障规则
重启系统
重启机器
……
6)演练复盘
● 复盘过程
- 演练是否符合流程
- 故障效果是否符合预期
● 复盘结果
- 系统表现是否符合预期
- 人员应急流程是否符合预期
云化基础设施:弹性与稳定的基石
目前容器+k8s 已经成为“云原生”架构“不可变基础设施”的事实标准,不仅能够带来运维、程序发布和部署的方便和灵活性,更重要的是能够对应用隐藏分布式架构复杂度、让分布式架构风格能够更具普适性。通过容器、编排系统和服务网格等技术,把软件与硬件之间的界限逐渐模糊掉,在基础设施层面上帮助微服务隐藏复杂性,解决原本只能通过编程来解决的分布式问题。
在技术视角上,云化基础设施主要是通过容器的隔离与封装技术来实现轻量的应用虚拟化,提升研发、运维、和交付效率,加快业务敏捷迭代。通过 k8s 自动化的编排能力,结合可观测和高可用技术来提升应用的可用性和容灾容错能力,提升服务的弹性和韧性。
1、容器的隔离与封装技术
首先,我们来看一个容器技术是如何实现隔离与封装的。
隔离
linux 系统中容器的隔离技术主要通过对文件的隔离、对访问的隔离和对资源的隔离来实现,初的目的不是为了部署软件,而是为了隔离计算机中的各类资源,以便降低软件开发、测试阶段可能产生的误操作风险。
● 隔离文件
在主流的 Linux 发行版中,文件隔离的主要实现方式是 chroot 命令或系统调用,按照 Linux 的设计哲学所有的处理都可以视为对文件的操作,所以理论上只要隔离了文件系统,一切资源都应该被自动隔离。但实际上从低层次硬件资源,到高层次操作系统资源(进程 ID、用户 ID、进程间通信),还都存在着大量以非文件形式暴露的操作入口,所以仅仅依赖文件隔离是无法实现完美的隔离性。
● 隔离访问
Linux 对文件、进程、用户、网络等各类信息的访问,基本都被囊括在 Linux 的名称空间中,Linux 的名称空间是一种由内核提供的针对进程设计的访问隔离机制。进程在一个独立的 Linux 名称空间中看起来拥有独立的 PID 编号、UID/GID 编号、网络等主机上的资源。
● 隔离资源
除了隔离进程的访问操作外,还需要能独立控制分配给各个进程的资源使用配额,防止一个进程发生了资源使用异常而影响到其它进程,破坏隔离性。Linux 是能过控制群组(cgroups)的功能解决上述问题的,它用于隔离并限制某个进程组能够使用的资源配额,包括处理器时间、内存大小、磁盘 I/O 速度等等。
封装
● 封装系统
当文件系统、访问、资源都可以被隔离后,Linux 又发布了能够综合使用 namespaces、cgroups 这些特性的系统级虚拟化功能 Linux 容器(LXC)。LXC 的容器与传统的 OpenVZ 和 Linux-VServer 系统级虚拟化的解决方案差别不大,也是一种封装系统的轻量级虚拟机。
● 封装应用
Docker 的容器化能力直接来源于 LXC,镜像分层组合的文件系统直接来源于 AUFS,但关键的是它把容器定义为了一种封装应用的技术手段。虽然这些在技术层面与 LXC 系统封装并没有什么本质区别,但 LXC 以封装系统为出发点,还是按照着先装系统然再装软件的思路,是无法形成快速构造出演足要求的系统环境的弹性能力的。
● 封装集群
以 Docker 为代表的容器引擎将软件的发布流程从分发二进制安装包转变为直接分发虚拟化后的整个运行环境,令应用得以实现跨机器的快速部署;而以 Kubernetes 为代表的容器编排框架,则是把大型软件系统运行所依赖的集群环境也进行了虚拟化,令集群得以实现跨数据中心的快速部署,并能够根据实际情况自动扩缩。
2、服务的弹性与韧性
控制器模式
一个具有韧性能力的系统是指能够健壮运行的、并且能够抵御意外与风险的系统。
我们可直接创建 Pod 将应用运行起来,但这样的应用是比较脆弱的,无论是软件缺陷、意外操作或者硬件故障,都可能出现容器异常或系统性的崩溃。而编排系统在这些服务出现问题运行状态不正确的时候,能够自动将它们调整成正确的状态,这就是工业控制系统中控制回路的思想。
将这种思路应用到容器编排上,就是为 Kubernetes 中的资源附加上了期望状态与实际状态两项属性。用户通过描述清楚这些资源的期望状态,由 Kubernetes 中对应监视这些资源的控制器来驱动资源的实际状态逐渐向期望状态靠拢,以此来达成目的。这种交互风格也被称为是 Kubernetes 的声明式 API。
只要是实际状态有可能发生变化的资源对象,都会由对应的控制器进行追踪,同时Kubernetes 设计了统一的控制器管理框架来维护这些控制器的正常运作,以及统一的指标监视器来为控制器工作时提供其追踪资源的度量数据。
Kubernetes 就是通过控制器模式,以部署控制器、副本集控制器和自动扩缩控制器为基础来实现应用的韧性和弹性能力的,下面稍微展开讲一下实现的过程。
故障恢复、滚动更新与自动扩缩
由直接创建的 Pod 构成的系统是十分脆弱的,更好的方式是通过副本集来创建。副本集也是工作负荷类的一种资源,它代表一个或多个 Pod 副本的集合,可以在副本集资源的元数据中描述期望 Pod 副本的数量。副本集控制器就会持续跟踪该资源,如果一旦有 Pod 发生崩溃退出,或者状态异常,副本集都会自动创建新的 Pod 来替代异常的 Pod,确保任何时候集群中这个 Pod 副本的数量都向期望状态靠拢。
在升级程序版本时,副本集需要主动中断旧 Pod 的运行,重新创建新版的 Pod,这会造成服务中断。对于那些不允许中断的业务,可以通过滚动更新来避免这种情况。
在控制器模型下,可以由 Deployment 来创建副本集,再由副本集来创建 Pod,当你更新 Deployment 中的信息以后,部署控制器就会跟踪到新的期望状态,自动地创建新副本集,并逐渐缩减旧的副本集的副本数,直至升级完成后彻底删除掉旧副本集。
遇到流量压力时,也可以手动或通过命令来修改 Deployment 中的副本数量,促使 Kubernetes 部署更多的 Pod 副本来应对压力,然而这种扩容方式需要人工参与和判断需要扩容的副本数量,不容易做到与及时。为此可以利用 Kubernetes 的 Autoscaling 资源和自动扩缩控制器,自动根据度量指标,如处理器、内存占用率、用户自定义的度量值等,来设置 Deployment 的期望状态,实现当度量指标出现变化时,实现根据度量指标自动扩容缩容。
附录:参考资料
- 飞天技术服务平台星轨工具中心
- CMH:https://www.aliyun.com/product/apds
- 云上容灾交付服务白皮书:https://developer.aliyun.com/ebook/7696
- 应用高可用服务 AHAS:
https://www.aliyun.com/product/ahas - 《某银行系统云原生上云佳实践》
- 《阿里容灾硬骨头--中心容灾战役设计策略与方案》
- 《The Are Of Scalability》
- 《凤凰架构:构建可靠的大型分布式系统》
- 《混沌工程:Netflix 系统稳定性之道》
- 《SRE:谷歌运维解密》
- 《Building Evolutionary Architectures》
- 《Mitigating Datacenter-level Disasters by Draining Interdependent Traffic Safely and Efficiently》