网站高可用架构设计基础——高可用策略和架构原则
一、正面保障与减少损失
要想让系统能够稳定可用,首先要考虑如何避免问题的发生。比如说可以通过 UPS(不间断电源)来避免服务器断电,可以通过事先增加机器来解决硬件资源不足的问题。
然后,如果问题真的发生了,就要考虑怎么转移故障(Failover)。比如说可以 通过冗余部署,当一个节点发生故障时,用其它正常的节点来代替问题节点。如果故障无法以正面的方式解决,就要努力降低故障带来的影响。比如说流量太大,可以通过限流,来保证部分用户可以正常使用,或者通过业务降级的手段,关闭一些次要 功能,保证核心功能仍旧可用。最后是要快速恢复系统。要尽快找到问题的原因,然后修复故障节点,使系统恢复到正常状态。
处理线上事故的首要原则是先尽快恢复业务,而不是先定位系统的问 题,再通过解决问题来恢复系统。因为这样做往往比较耗时,这里给出的处理顺序也体现了 这个原则。
1.正面保障
(1)冗余无单点——节点本身的故障
①单节点故障点
网络链路,服务端网络;
DNS;
机房;
机架,同机房的一批机器;
交换机/路由器;
负载均衡;
物理服务器;
业务服务本身;
缓存 / 数据库 / 存储。
②解决方法
首先要保证系统的各个节点在部署时是冗余的,没有单点。比如在接入层中,可以实现负载均衡的双节点部署,这样在一个节点出现问题时,另一个节点可以快速接管,继续提供服务。还有远程网络通信,它会涉及到很多节点,也很容易会出现问题,就可以提供多条通信 线路,比如移动 + 电信线路,当一条线路出现问题时,系统就可以迅速切换到另一条线 路。甚至可以做到机房层面的冗余,通过系统的异地多 IDC 部署,解决自然灾害(如地 震、火灾)导致的系统不可用问题。
- 在服务逻辑层采用多运营商多IP入口、跨地&同地多机房部署、同机房多机器部署、分布式任务调度等策略。
- 在数据存储层采用数据库分库分表、数据库主从备集群、KV存储&消息等分布式系统集群多副本等策略。
- 有分布式处理能力后,需要考虑单个服务器故障后自动探活摘除、服务器增删能不停服自动同步给依赖方等问题,这里就需引入一些分布式中枢控制系统,如服务注册发现系统、配置变更系统等,例如zookeeper是一个经典应用于该场景的一个分布式组件。
(2)水平扩展——节点处理能力的不足
很多时候,系统的不可用都是因为流量引起的:在高并发的情况下,系统往往会整体瘫痪, 完全不可用。由于硬件在物理上存在瓶颈,通过硬件 升级(垂直扩展)一般不可行,需要通过增加机器数量,水平扩展这些节点的处理能 力。对于无状态的计算节点,比如应用层和服务层来说,水平扩展相对容易,直接增加机器 就可以了;而对于有状态的节点,比如数据库,可以通过水平分库做水平扩展,不过这 个需要应用一起配合,做比较大的改造。
- 提前预测大流量,提前扩容
- 实现流量变大,自动扩容
(3)分散、均衡、隔离原则
- 分散原则(核心):鸡蛋不要放一个篮子,分散风险
-
- 例如:所有交易数据都放在同一个库同一张表里面,万一这个库挂了,此时影响
- 均衡原则(核心):均匀分散风险,避免不均衡
-
- 最好N份中的每份都是均衡的;避免某个份额过大,否则过大的那份一有问题就影响范围过大了。例如:xx应用集群有1000台,但由于引流组件BUG,导致所有流量引到了其中100台上面,导致负载严重不均衡,最后因负载无法扛着全面崩溃。类似重大故障已经发生了多次。
- 隔离原则(核心):控制风险不扩散,不放大
-
- 每份之间是相互隔离的;避免一份有问题影响其他的也有问题,传播扩散了影响范围。例如:交易数据拆分成10库100表,但是部署在同一台物理机上;万一某张表有一条大SQL把网卡打满了,那10库100表都会受影响。
- 对于请求量大的客户进行隔离,分散到一个单独的库
2.减少损失
(1)柔性事务——基本可用和数据的最终一致
系统的可用性经常会和数据的一致性相互矛盾。在 CAP 理论中,系统的可用 性、一致性和网络容错性,三个最多只能保证两个,在分布式系统的情况下,只能在 C 和 A 中选一个。在很多业务场景中,系统的可用性比数据的实时一致性更重要,所以在实践中,更多地 使用 BASE 理论来指导系统设计。
资金交易类系统要仔细考虑资损的风险.交易系统对于数据准确性、一致性、资金损失等都是很敏感的,这一块在是否使用缓存、事务一致性考量、数据时延、数据不丢不重、数据精准核对和恢复等需要额外架构设计考量。仔细评估交易以及营销的全链路各个环节,评估其中出现资损的可能性以及做好应对设计,例如增加多层级对账、券总额度控制、异常金额限制和报警等资损防控的考量等。不同层次不同维度不同时间延迟的对账以及预案是一个重要及时感知资损和止血的有效方式。全链路的过程数据要做好尽可能持久化和冗余备份,方便后续核对以及基于过程数据进行数据修复,同时尽量针对特殊数据丢失场景提供快速自动化修复处理预案(如交易消息可选择性回放和基于幂等原则的重新消费)。
(2)系统可降级——损失非核心功能来保证核心功能的可用
能不依赖的,尽可能不依赖,弱依赖原则:一定要依赖的,尽可能弱依赖。
对于稳定性要求很好的关键系统,在成本可接受的情况下,同时维护一套保障主链路可用的备用系统和架构,在核心依赖服务出现问题能做一定时间周期的切换过渡(例如mysql故障,阶段性使用KV数据库等),例如钉钉IM消息核心系统就实现对数据库核心依赖实现一套一定周期的弱依赖备案,在核心依赖数据库故障后也能保障一段时间消息收发可用。
当系统问题无法在短时间内解决时,就要考虑尽快止损,为故障支付尽可能小的代价。具体的解决手段主要有以下这几种。
- 限流:让部分用户流量进入系统处理,其它流量直接抛弃。
- 降级:系统抛弃部分不重要的功能,比如不发送短信通知,以此确保核心功能不受影 响。
- 超时:避免调用端陷入永久阻塞。
- 熔断:我们不去调用出问题的服务,让系统绕开故障点,就像电路的保险丝一样,自己 熔断,切断通路,避免系统资源大量被占用。比如,用户下单时,如果积分服务出现问 题,我们就先不送积分,后续再补偿。
- 功能禁用:针对具体的功能,设置好功能开关,让代码根据开关设置,灵活决定是 否执行这部分逻辑。比如商品搜索,在系统繁忙时,我们可以选择不进行复杂的深度搜索。
(3)自愈 Self-healing
自我修复可以帮助恢复应用程序。自愈是指应用程序可以做一些必要的步骤来恢复崩溃状态。在大多数情况下,这样的操作是经由一个外部系统来实现的,它会监控实例的健康,并在它们较长时间处于错误状态的情况下,重新启动应用程序。自愈是非常有用的,但是在某些情况下,不断地重启应用程序会引起麻烦。由于负载过高或者数据库连接超时,你的应用程序不停的重启,会导致无法提供一个正确的健康状态。实现一种为微妙的情况而准备的高级自我修复解决方案,可能会很棘手,比如数据库连接丢失。在这种情况下,你需要为应用程序添加额外的逻辑来处理一些极端情况,并让外部系统知道不需要立即重启实例。
二、研发流程
1.运维策略
- 可灰度:保障及时在小流量情况,发现问题,避免引发大范围故障。因此在做系统任何变更时,要考虑灰度方案,特别是大用户流量系统。灰度方式可能有白名单用户、按用户Id固定划分后不同流量比例、机器分批发布、业务概念相关分组分比例(例如某个行业、某个商品、某类商品)等,灰度周期要和结合系统风险和流量做合适设计,重要系统灰度周期可能持续超过一周或更多。
- 可监控:通过监控,我们可以实时地了 解系统的当前状态,这样很多时候,业务还没出问题,就可以提前干预,避免事故;而 当系统出现问题时,我们也可以借助监控信息,快速地定位和解决问题。
- 可回滚:相同的版本可以反复发布,新增功能增加配置开关,当线上出现问题时,可通过关闭功能开关,快速下线最新升级 或部分有问题功能。针对不同出错场景,有配置驱动一些预案,例如降级对某个服务的依赖、提供合适功能维护中公告、切换到备用服务等预案,在特定问题出现时,可以快速做线上止损和恢复。发布功能注意提前考虑出现问题时快速回滚步骤,部分经常发布注意对回滚步骤做演练。
- 可转移:需要具备故障转移能力
-
- 接入层:DNS、VipServer、SLB。
- 服务层:服务发现 + 健康检查 + 剔除机制。
- 应用层:无状态设计(Stateless),便于随时和快速切换。
- 密闭性。所谓密闭性(Hermetic),简单说就是环境的完整性。比如,软件的源代码必须是密闭的,每次通过特定的版本号,检出内容必须是完整的,一致 的且可重复的。编译的时候不需要再去任何第三方额外检出外部依赖的源代码。再比如,从构建过程来说,同样必须确保一致性和可重复性。让两个工程师在两台不同的机 器上基于同一个源代码版本构建同一个产品,构建结果应该是相同的。这意味着它不应该受 构建机器上安装的第三方类库或者其他软件工具所影响。构建过程需要指定版本的构建工 具,包括编译器,同时使用指定版本的依赖库(第三方类库)。编译过程是自包含的,不依 赖于编译环境之外的任何其他服务。
2.团队研发运维流程机制
- 技术Review:不同体量设计安排经验更加丰富同学Review,架构师、主管、外部架构师的Review、定期系统整体Review等。
- 代码Code Review:建立规范和标准,通过CR认证合格同学执行code review动作。
- 单测:不同风险的系统设定尽量高的行覆盖 & 分支覆盖率标准,复杂逻辑类追求100%分支覆盖。
- 回归测试:持续积累回归用例,在上线前和上线后执行回归动作;上线前线上引流测试也是一种模拟测试方式,端类型系统还可以用monkey工具做随机化测试。
- 发布机制:设计发布准入和审批流程,确保每次上线发布都是经过精细设计和审核的(记录系统的任何一次发布和变化),上线过程要做到分批、灰度、支持快速回滚、线上分批观察(日志确认)、线上回归等核心动作。建立发布红线等机制,不同系统设计合适发布时段以及发布灰度观察周期。关于配置修改的时机也应该详细考虑,列在发布流程之中。
-
- 自动化运行单元测试案例(unit test);
- 单元测试覆盖率检查(code coverage);
- 静态代码质量检查(lint);
- 人工的代码互审(code review);
- 无感发布:先让服务从注册中心中下掉,但是java进程先不kill掉
- 团队报警值班响应机制 (报警群、短信、电话):确保报警有合适人员即时响应处理,团队层面可定期做数据性统计通报,同时建立主管或架构师兜底机制。
- 定期排查线上隐患:定期做线上走查和错误日志治理、告警治理,确保线上小的隐患机制化发现和修复。例如在钉钉针对企业用户早晚高峰的特点,设计早值班机制,用于高峰期第一时间应急以及每天专人花一定时间走查线上,该机制在钉钉技术团队持续践行多年,有效发现和治理了钉钉各个线上系统的隐患。
- 用户问题处理机制:Voc日清、周清等。在钉钉也经历Voc周清到日清的持续机制精进。
- 线上问题复盘机制:天内、周内问题及时复盘,确保针对每个线上问题做系统和团队精进。
- 代码质量抽查通报:定期抽查团队同学代码,做评估和通晒,鼓励好的代码,帮助不好代码的改善。
- 成立稳定性治理专门topic:合适同学每周做好稳定性过程和精进。
- 定期压测机制:定期机制化执行,核查线上容量情况。
- 日常演练机制:预案演练,模拟线上故障的不通知的突袭演练提升团队线上问题应对能力。
- 错误日志要重视.要定期分析线上错误日志,隐患的问题是藏在错误日志中的。我们现在技术团队会有早值班机制,每个方向每天都有一个技术同学走查线上,以发现线上隐患问题为导向,走查监控大盘、错误日志、用户反馈,通过这个例行机制,很好地防微杜渐。
- 异常一定要消灭:有异常,基本就意味着系统存在风险,一定要消灭异常;与终端用户相关的异常,要以最高优先级处理:即便是 IT 研发,也要以用户为中心。不是所有的异常都要从 Log 中消失,但对于保留下的异常,一定提交管理层进行审批,说明保留原因;理由不够充分的,需要按排期规划并解决。
- 异常一定要管理:消灭异常是个长期工程,短期要通过管理行为来进行控制;每个异常都要有具体的负责人:没有和具体的负责人一一对应,往往就意味着管理流于形式;异常也应该企业内部形成规范,不可以各个系统不一致。(最好企业内部有一个专门的异常识别及管理的网站)
流程机制要和团队同学共创达成一致后,配合建立topic负责人机制,对流程机制执行度和执行效果要做好过程监测和通晒,建立明确数字化标准和衡量机制(例如钉钉技术团队针对线上问题设定1-5-10标准,1分钟响应5分钟内定位10分钟内恢复),同时建立对应奖惩机制。流程机制也要根据系统状态进行精简或精进,确保流程机制可执行性和生命力。要确保在发布过程中,只有指定的人才能执行指定的操作,而不能随随便便跳过必要的环节 进行发布。
3.发布检查列表
(1)容量规划相关
- 本次发布是否与新闻发布会、广告、博客文章或者其他类型的推广活动有关?
- 发布过程中以及发布之后预计的流量和增速是多少?
- 是否已经获取到该服务需要的全部计算资源?
(2)故障模式相关
针对服务进行系统性的故障模式分析可以确保发布时服务的可靠性。在检查列表的这一部分中,我们可以检查每个组件以及每个组件的依赖组件来确定当它们发 生故障时的影响范围
- 该服务是否能够承受单独物理机故障?单数据中心故障?网络故障?
- 如何应对无效或者恶意输入,是否有针对拒绝服务攻击(DoS)的保护?
- 是否已经支持过载保护?
- 如果某个依赖组件发生故障,该服务是否能够在降级模式下继续工作?
- 该服务在启动时能否应对某个依赖组件不可用的情况?在运行时能否处理依赖不可用和 自动恢复情况?
(3)客户端行为相关
最常见的客户端滥发请求的行为,是配置更新间隔的设置问题。比 如,一个每 60s 同步一次的新客户端,会比 600s 同步一次的旧客户端造成 10 倍的负载。重试逻辑也有一些常见问题会影响到用户触发的行为,或者客户端自动触发的行为。假设我 们有一个处于过载状态的服务,该服务由于过载,某些请求会处理失败。如果客户端重试这 些失败请求,会对已经过载的服务造成更大负载,于是会造成更多的重试,更多的负载。客
户端这时应该降低重试的频率,一般需要增加指数型增长的重试延迟,同时仔细考虑哪些错 误值得重试。例如,网络错误通常值得重试,但是 4xx 错误(这一般意味着客户端侧请求 有问题)一般不应该重试。
自动请求的同步性往往还会造成惊群效应。例如,某个手机 APP 开发者可能认为夜里 2 点 是下载更新的好时候,因为用户这时可能在睡觉,不会被下载影响。然而,这样的设计会造 成夜里 2 点时有大量请求发往下载服务器,每天晚上都是如此,而其他时间没有任何请 求。这种情况下,每个客户端应该引入一定随机性。
其他的一些周期性过程中也需要引入随机性。回到之前说的那个重试场景下:某个客户端发 送了一个请求,当遇到故障时,1s 之后重试,接下来是 2s、4s 等。没有随机性的话,短 暂的请求峰值可能会造成错误比例升高,这个周期会一直循环。为了避免这种同步性,每个 延迟都需要一定的抖动,也就是加入一定的随机性
- 客户端在请求失败之后,是否按指数型增加重试延时?
- 是否在自动请求中实现随机延时抖动?
(4)流程与自动化相关
虽然鼓励自动化,但是对于发布这件事情来说,完全自动化 是灾难性的。为了保障可靠性,我们应该尽量减少发布流程中的单点故障源,包括人在内。这些流程应该在发布之前文档化,确保在工程师还记得各种细节的时候就完全转移到文档 中,这样才能在紧急情况下派上用场。流程文档应该做到能使任何一个团队成员都可以在紧 急事故中处理问题。
- 是否已将所有需要手动执行的流程文档化?
- 是否已将构建和发布新版本的流程自动化?
(5)外部依赖相关
有时候某个发布过程依赖于某个不受公司控制的因素。尽早确认这些 因素的存在可以使我们为它们的不确定性做好准备。例如,服务依赖于第三方维护的一个类库,或者另外一个公司提供的服务或者数据。当第三 方提供商出现故障、Bug、系统性的错误、安全问题,或者未预料到的扩展性问题时,尽早 计划可以使我们有办法避免影响到直接用户。
- 这次发布依赖哪些第三方代码、数据、服务,或者事件?
- 是否有任何合作伙伴依赖于你的服务?发布时是否需要通知他们?
- 当我们或者第三方提供商无法在指定截止日期前完成工作时,会发生什么?