写点什么

微服务沉思录 - 可靠性

发布于: 2021 年 08 月 01 日
微服务沉思录-可靠性

1.可靠性

可靠性(Reliability)是指微服务系统在面对异常情况时,如关键组件损坏、流量或数据量异常、延迟波动、级联故障传导、分布式集群雪崩、系统过载等等,能够持续保持稳定运行或快速恢复的能力。


当我们在说可靠性时,我们到底在谈什么?是指高可用架构设计?系统容忍宕机的时间?弹性能力、自愈能力?还是指故障的转移和恢复时间?


本文试图从以下几个方面来说明一个高可用架构所需的必备因素:概念与度量、故障体系、单点治理、超时、重试、限流、降级、熔断、Oncall 轮值与变更管理。


相信在这些方法论和实战经验相结合、多管齐下的协助下,能最大限度改善和保障系统的可靠性,让微服务系统可以固若金汤,具备较强的韧性。

1.1.概念与度量

首先需要说明的是,我们在谈及可靠性时,往往会把以下几个相关的概念弄混淆:可靠性、可用性、弹性、持久性、冗余性。这些似乎都是和系统稳定性有关的近义词,但实际上是存在区别的。

1. 可靠性(Reliability)。可靠性是指系统能够正常工作和稳定运行的时间,这里的稳定性代表功能性操作正确无误,且性能表现符合预期。


2. 可用性(Availability)。可用性是指系统可触达的时间,一般是指服务或数据能够连续提供给用户的时间,或可用时间的占比。


3. 弹性(Resiliency)。弹性本质上是指一种修复能力,代表系统在组件失败或遇到灾难后的恢复能力。弹性能力一般由系统故障修复时间来衡量。


4. 持久性(Durability)。持久性是指数据能否具备持久化、稳定存储的能力,不会随着时间变动而轻易流失。和服务不同,持久性更侧重于数据层面的稳定性。


5. 冗余性(Redundancy)。冗余性是指系统是否存在多个副本,互为备份,也称为系统的冗余度。冗余度越高,系统的灾备能力就越强。


举个综合的例子来说明,假设有一个视频网站,对用户提供 7*24 小时视频展示和播放服务。该视频网站具备以下特征:

  • 该网站每年顶多宕机 5 分钟;

  • 经常会出现页面加载不完整,比如会出现 CSS 无法加载而导致的页面参差不齐;

  • 视频偶尔会出现播放到一半的时候,出现长时间卡顿;

  • 该网站在请求延迟升高的时候,能在 1 分钟内迅速恢复;

  • 该网站的数据做了完整的冷备,备份介质采用磁带和光盘,且异地保存。任何时候都可以快速恢复数据;

  • 网站采用了异地多活的物理架构部署,服务器和中间件采用 N+3 的 Buffer 来设计;


不难看出,该视频网站具备很高的可用性,因为宕机时间足够短,用户几乎在任何时候都可以访问。但是可靠性比较糟糕,因为页面经常无法正常加载,视频播放体验也不尽如人意。网站的弹性恢复能力很强,不到 1 分钟就能从故障中恢复。另外,网站具备很优秀的数据持久化能力,由于备份和恢复的存在,数据几乎不用担心丢失。网站的冗余度能达到 N+3,意味着系统架构具备较高的灾备程度。


以上这些概念,通常来说在不同的上下文中具备不同的释义。除非特别说明,默认情况下本文不会有意区分这些名词术语,而是统一采用广义的可靠性概念来表达系统稳定性语义。


可靠性的常见度量指标,是基于系统可用时间计算出来的比例,即 R(%) =  (uptime/uptime + downtime)  * 100%,常见的 SLA 指标会借助多少个 9 来表示,如图 1‑1 所示。

图 1‑1 常见 SLA 指标


还有一种可靠性度量指标是利用 MTTR、MTTF 和 MTBR 来表示,如图 1‑2 所示。

  • MTTR ( Mean Time To Repair),平均故障修复时间。指系统从发生故障到故障修复的时间间隔之平均值;

  • MTTF ( Mean Time To Failure),平均无故障时间。指系统从正常运行到故障发生的时间间隔之平均值;

  • MTBF ( Mean Time Between Failure),平均故障间隔时间。指系统在两次故障发生之间的时间间隔之平均值;

图 1‑2 MTTR、MTTF 和 MTBR 的关系


这种度量指标下,可靠性通常用 R(%)  = (MTTF/MTBR)  * 100%  =  (MTTF/MTTR + MTTF) * 100%来表示。


SLA 指标每提升一个 9,背后就需要付出较大的努力和人力、资源消耗。一般来说,大部分互联网企业的关键业务可以达到 4 个 9,部分金融级别的业务系统或公有云会承诺 5 个 9。对大部分企业来说,需要根据自身的实力和业务需要,合理评估 SLA 需求,不要一味的去追求极高的 SLA,其投资回报比(ROI)可能并不高。

1.2.故障体系

故障体系是一套科学的方法论,用于指导和管理日常的系统故障。根据互联网企业的实践经验总结,再结合 Google SRE 标准,我们试着给出一个标准的、完整的故障管理体系,包括:故障画像、故障预案、故障演练、故障转移、故障恢复。

1.2.1.故障画像

故障画像是故障体系和故障处理流程的源头及理论依据,所有线上问题的发生、发现和处理几乎都是率属于故障画像所穷举的各种范畴。


故障画像本质上来说就是通过分析大量的故障案例,进行充分总结和高度抽象得出的故障根因(Root Cause)。常见的故障画像大概分为以下几类:


1. 基础设施

常见的基础设施故障包括以下组成部分:


机房设施故障:如电力系统故障、空调系统故障等一系列 IDC 机房底层相关的环境因素。


服务器故障:常见的有物理机、虚拟机、容器的系统宕机和指标异常。宕机包括计划内和计划外,服务器指标异常包括 CPU 使用率异常,内存使用率过高,SWAP 过于频繁、网络、磁盘等设备 I/O 读写异常,中断及上下文切换频次过高等等。对于虚拟机、容器来说,还会存在资源超卖导致的流量高峰期系统平均负载频繁抖动。


网络故障:网络问题是影响面最大的故障因素,往往会导致 P0 级别的生产事故。常见的有网络中断、丢包、重复、延迟、交换机路由器等设备故障、网络专线故障等等。


接入层故障:常见的问题有 DNS 解析失败、负载均衡 SLB 故障、VIP 转移故障、Nginx 请求转发异常(如路径解析错误、健康检查机制失败)等。


2. 中间件故障

常见的中间件故障包括以下组成部分:


存储系统故障:一般指数据库(MySQL、MongoDB、HBase、Cassandra 等 SQL 或 NoSQL 数据库)、缓存、分布式存储系统、分布式对象系统(OSS)等广义上的存储系统存在的故障点,如:系统单点,存储系统只部署了单实例或单机房。这这种情况下,如果发生单实例宕机或单机房故障,或单实例读写流量过高、数据量较大导致的慢查询、Load 升高等负载问题,由于没有更多的冗余,使得系统无法进行故障转移。


消息中间件故障:作为削峰填谷和解耦的利器,消息中间件被大量使用在异步化场景中。但消息中间件并不能保证长久稳定运行。如:数据量过大导致磁盘被填满,从而使得集群不可用;读写压力过大导致 MQ 集群出现 GC 频繁、节点脱离集群等故障,如 ActiveMQ 的 Network Broker 模式,很容易在大流量的读写压力下导致 Broker 断开连接,影响到总体可用性。


3. 外部服务

一般是指第三方服务(如 REST API/RPC/消息等)的故障,如服务超时、HTTP 响应状态码异常、服务过载、返回报文异常等。这类故障是出现频次最高的,需要有重点预案、演练和监控。


4. 系统缺陷

一般是指我们服务自身的功能、性能或稳定性问题,如:JVM 内存溢出、频繁 GC 停顿、代码死循环、CPU/内存/磁盘使用率异常、系统过载、响应时间过高、功能性 BUG 等。


5. 流程问题

流程问题一般是人为误操作或流程不够标准造成的故障,如代码缺少 Code Review、缺乏渐进式灰度发布机制、缺乏充分的功能或性能测试、缺乏监控告警等。这类问题应该尽量避免。

1.2.2.故障预案

凡事预则立,不预则废。针对上一节描述的基础设施、中间件、外部服务、系统缺陷和流程问题等故障画像,我们需要拟定一套成熟的故障应对预案。目标是在故障突发时,能够使故障处理流程做到有章可依、有条不紊、敏捷高效。


以下是笔者结合自身的实际经验总结,整理出的一个故障预案模板,供参考:

1 基础设施故障1.1 服务器故障【问题】宿主机(物理机)故障导致的一个或多个虚拟机实例宕机,或虚拟机发生计划外(不明原因)的重启;【预案】一个虚拟机实例宕机一般影响较小,确定负载均衡的自动健康检查已将流量转走,待虚拟机恢复后重新部署应用。注:正在调研服务器重启时自动触发Jenkins部署以减少人工运维;【预案】多个虚拟机实例宕机需评估影响范围,极端情况可能会造成流量击垮其他服务器,紧急情况需启用备用虚拟机。确定负载均衡的自动健康检查已将流量转走;
【问题】服务器(包括物理机、虚拟机和容器)一项或多项指标异常,如平均负载升高、内存空间不足、磁盘空间不足、CPU消耗过高、I/O消耗过高、文件句柄泄露等,导致程序无法正常运行;【预案】在基础监控系统中查看服务器指标变化情况;【预案】根据指标变化找到原因,解决问题。如程序中资源释放、JVM调优、清磁盘机制优化等;
1.2 网络故障【问题】机房网络误操作(网络割接/板卡升级替换/打补丁/设备升级/配置优化/扩容/虚拟网络组件升级等),或部分网络设备故障,造成一台或多台服务器级别网络问题(内网不通、丢包、延迟等);【预案】将问题反馈到网络组;【预案】一台服务器网络问题一般影响较小,确定负载均衡的自动健康检查已将流量转走,并关注网络恢复情况;【预案】多台服务器网络问题需评估影响范围,必要情况需要切流量。确定负载均衡的自动健康检查已将流量转走;
【问题】机房核心交换机等大型故障,造成子网级别网络问题,或机房级别网络问题;【预案】将问题反馈到网络组;【预案】若网络故障影响到负载均衡SLB,则通过控制台修改DNS配置,将流量导向其他正常SLB,并等待DNS缓存更新;【预案】若影响到转发前置机Nginx,通过SLB配置后台,将流量导向其他正常Nginx;【预案】若影响到服务器,通过Nginx配置将流量导向其他正常的服务器;
【问题】运营商问题或网络流量过大,造成专线问题,或专线带宽打满;【预案】将问题反馈到网络组;【预案】参考上面预案;
1.3 电力故障【问题】机房例行检修(倒闸检修/柴油发电带载等)导致故障,或意外停电故障,造成机房无法正常运行;【预案】若影响到负载均衡SLB,则通过控制台修改DNS配置,将流量导向其他正常SLB,并等待DNS缓存更新;【预案】若影响到Nginx,通过SLB配置将流量导向其他正常Nginx;
1.4 接入层故障【问题】DNS故障,无法解析【预案】公司层面报障,切换客户端DNS高可用配置,开启备用DNS域名;
【问题】SLB故障,如VIP无法访问,或丢包;【预案】通知SLB运维人员;【预案】SLB部署需要做到:1)外网:同一个运营商出口会配置2个不同机房SLB,2)内网:同一个内网域名会配置多个不同机房SLB用于冷备;【预案】通过控制台修改DNS将流量导向其他正常SLB,并等待DNS缓存更新 ;
【问题】Nginx故障;【预案】Nginx前置机部署时需要跨机房;【预案】通过SLB配置将流量导向其他正常Nginx;
2 中间件故障2.1 存储故障【问题】MySQL故障,无法访问或访问超时;【预案】要求部署时必须是Master-Slave架构且跨机房;【预案】遇到单实例故障或机房故障时,会自动进行Failover且切换主从,业务方会经历一段有损服务后自行恢复;
【问题】MongoDB故障,无法访问或访问超时;【预案】要求部署时必须是跨机房;【预案】遇到单实例故障时,会自动进行Failover且重新选举Primary,业务方会经历一段有损服务后自行恢复;
【问题】HBase故障,无法访问或访问超时;需要封装客户端代理SDK(内置配置中心开关),底层做双向数据复制;遇到集群故障时,通过配置中心切换到另一个集群;
【问题】Couchbase故障,无法访问或访问超时;需要封装客户端代理SDK(内置配置中心开关),底层做XDCR数据复制;遇到集群故障时,通过配置中心切换到另一个集群;
【问题】Redis故障,无法访问或访问超时;【预案】要求部署必须是Master-Slave架构且跨机房;【预案】遇到到单实例故障时,会自动进行Failover且切换主从,业务方会经历一段有损服务后自行恢复;
2.2 搜索系统故障【问题】Elasticsearch故障,无法访问或访问超时【预案】通过客户端代理组件,将读写流量切换到备用Elasticsearch集群;
2.3 消息中间件故障【问题】ActiveMQ故障,无法访问或访问超时【预案】通过客户端代理组件,将读写流量切换到备用ActiveMQ集群;
【问题】Kafka故障,无法访问或访问超时【预案】通过客户端代理组件,将读写流量切换到备用Kafka集群;
2.4 分布式系统故障【问题】Zookeeper故障,无法访问或访问超时;【预案】等待Zookeeper完成奔溃恢复和重新选举,密切观察监控指标变化;
3.外部服务故障【问题】外部服务出现调用超时或异常返回;【预案】在编码时务必处理好第三方调用的超时、重试、降级与熔断机制,做好监控告警与日志埋点,并对异常情况做好测试;【预案】遇到故障时,启动手动或自动降级熔断,使得调用快速失败和快速返回兜底数据,避免调用链雪崩;
复制代码

1.2.3.故障演练与混沌工程

光说不练假把式。故障演练是整个故障管理体系中最重要的一环。缺乏真实演练尤其是生产环境的真实故障演练,故障预案则一文不值。线上环境的故障演练能真实反映系统的潜在故障点,验证监控、告警及日志系统的及时性和有效性,也能高效验证故障预案和处理流程的实战效果。


企业、部门乃至业务团队应该成立故障演练小组,制定完备的演练规划、方案和执行计划,定期或不定期进行生产环境的演练,并生成故障演练报告和跟踪项,做到及时发现和解决各种故障隐患。


随着 Netflix 的 Chaos Engineering 思想和部分头部互联网公司的实践(如阿里的 Chaos Blade),混沌工程开始进入了国内众多工程师的视野,风靡大江南北,且有愈演愈烈的趋势。混沌工程的基本原则主要有:

  • 建立稳定状态的假设;

  • 多样化现实世界事件;

  • 在生产环境运行实验;

  • 持续自动化运行实验;

  • 最小化“爆炸半径”;


我们可以根据混沌工程的方法论和工具包,探索和开发自动化混沌测试方案,支持基础设施、中间件、服务的自动故障注入和测试。通过在生产环境设置定时器,可以支持预先编排好的混沌测试用例自动化执行,可以尽早发现系统的可靠性隐患。以上操作可以在混沌测试平台上进行开发、配置、部署、自动化运行和监控。


以下是根据一个实际生

产环境故障注入案例,经过整理得到的混沌测试用例:

1.模拟虚拟机VM CPU满载时间:2021-08-20 00:00~00:30执行:8核CPU使用率均达到100%期望:基础监控可观察到CPU使用率上升,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。
2.模拟虚拟机VM 内存完全占用时间:2021-08-20 00:30~01:00执行:32GB RAM使用率达到100%期望:基础监控可观察到MEM使用率上升,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。
3.模拟虚拟机VM 磁盘I/O完全占用时间:2021-08-20 01:00~01:30执行:磁盘I/O使用率达到100%期望:基础监控可观察到磁盘I/O使用率上升,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。
4.模拟虚拟机VM 磁盘完全填充时间:2021-08-20 01:30~02:00执行:300GB磁盘使用率达到100%期望:基础监控可观察到磁盘使用率上升,系统告警,无法写入。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。
5.模拟虚拟机VM 宕机时间:2021-08-20 02:00~02:30执行:将该虚拟机实例关闭期望:基础监控可观察到虚机关闭,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。
6.模拟Kubernetes Pod宕机时间:2021-08-20 02:30~03:00执行:将某Workload下的Pod关闭期望:基础监控可观察到虚机关闭,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。Kubernetes自动启动新的Pod。
7.模拟不同物理机的虚拟机、Pod宕机时间:2021-08-20 03:00~03:30执行:将隶属于不同物理机的虚拟机和Pod关闭期望:基础监控可观察到虚机关闭,系统告警。应用监控显示该节点工作不正常并及时告警。负载均衡自动摘除不健康节点。Kubernetes自动启动新的Pod。
8.模拟单机房网络故障时间:2021-08-20 03:30~04:00执行:通过模拟网络延迟、丢包、专线断开等方式,实现单一机房网络通讯故障期望:基础监控可观察到虚机关闭,系统告警。应用监控显示该节点工作不正常并及时告警。
9.模拟MySQL故障时间:2021-08-20 04:00~04:30执行:将MySQL Master节点和Slave节点分别关闭期望:基础监控可观察到虚机关闭,系统告警。应用监控显示该节点工作不正常并及时告警。MySQL主从集群自动提升Slave为主节点,并自动修改域名绑定。应用层面感知不明显。
10.模拟推荐服务故障时间:2021-08-20 04:30~05:00执行:通过制造网络延迟及丢包,模拟推荐服务故障期望:应用监控显示该节点工作不正常并及时告警。推荐服务(强依赖服务)被自动降级到缓存数据,推荐效果出现折损。
11.模拟用户服务故障时间:2021-08-20 05:00~05:30执行:通过制造网络延迟及丢包,模拟用户服务故障期望:应用监控显示该节点工作不正常并及时告警。用户服务被自动熔断,业面上大部分用户的关键信息(如头像、昵称)出现丢失,主体服务不受影响。
复制代码


以上给出了一些基本的混沌测试注入案例,覆盖了基础设施(服务器、网络)、中间件(存储系统)和外部服务(强依赖服务、弱依赖服务)等。实际执行混沌测试时,可参考这些案例,并灵活变动。

1.2.4.故障转移

根据墨菲定律,该发生的故障终究会发生。尽管我们做了大量的故障预案和演练,但只要系统存在薄弱环节(几乎 100%无法避免),则迟早会被攻破,问题也会爆发。


在故障发生时,故障转移(Failover)手段就显得尤为重要,好的转移技术手段能有效缩短平均故障修复时间 MTTR,减少系统、数据、资金、公司信誉损失,也能将用户体验影响降到最低。


故障转移基本原理是利用 HA 监控组件,实时追踪和发现系统的故障点,并自动执行故障点转移,将服务流量和数据迁移到冗余的组件上。


需要说明的是,这里的组件是一个广义的概念,可以是一个物理机、虚拟机、容器,也可以是一个集群或集群节点,再大一点可以是 IDC 机房、AZ 可用区或地理位置 Region。


故障转移主要有两种模式:主动-主动模式和主动-被动模式。


主动-主动模式(Active-Active):主动-主动模式是指组件之间相互热备,且正常承担读写流量。在故障发生时,HA 监控组件检测到不健康组件后,对其进行摘除或屏蔽,避免流量访问。


举例来说明,常见的主动-主动模式有无状态的服务器集群,或去中心化的存储集群。


如 REST API 服务集群,多节点提供服务,由接入层提供负载均衡、鉴权及流量路由。这种情况可以视为一种无状态的主动-主动模式。当单一节点或多个节点发生故障(宕机、服务器 CPU/MEM/DISK/NETWORK 指标异常等)时,由接入层的健康检查组件自动检测出不健康状态的节点,并将这些节点从集群临时剔除。

另一种情况是无中心的存储集群,如 Couchbase。当某一节点宕机后,集群会将该节点屏蔽,将读写流量转移到其他节点。


主动-被动模式(Active-Passive):主动-被动模式是指备用组件平时不会提供服务,仅充当冷备的角色。当故障发生时,由 HA 监控组件将不健康组件进行摘除处理,同时将流量导入备用节点。


这种情况需要格外小心,很多时候备用节点往往只是在冷备和待命,在关键时刻可能会“掉链子”,不能正常工作。在故障切换时,有时候会发生流量打到备用设备后,备用组件再次出现问题,故障影响面反而被再次放大。

1.2.5.故障恢复

故障恢复是故障处理的收尾动作,当故障转移完毕,就应该对故障组件进行及时修复,这可能是一个手动或自动的处理过程。


故障组件恢复后,通常会面临两个选择:将流量再次转移回已恢复的组件,恢复到故障前的拓扑结构;另一种方案就是将恢复的组件加入到总体架构,充当备用角色。


故障恢复的时间应该尽可能短,并且可控。因为故障转以后,系统总体可能会处于“单点”的状态,如果故障组件不能很快恢复并再次加入系统,则又会增加长时间单点的风险。如果此时剩下的组件再次发生故障,系统就会出现无组件可转移的问题。


举一个实际生产环境发生的例子来说明。某 Feed 服务集群,物理架构上采用 2 个机房来部署节点,假设为 IDC1 和 IDC2。在某个晚高峰,IDC1 出现核心交换机网络故障,所有服务器实例无法建立网络连接,机房总体不可用,运维人员紧急将 IDC1 从负载均衡设备上进行摘除。


IDC1 在接下来的 2 天都没有完全恢复,不幸的事情发生了,IDC2 在另一个晚高峰也出现了网络故障,这个系统彻底瘫痪了。


这里先不讨论基础架构的薄弱及运维能力的欠缺,单从故障恢复的层面来看,我们应当遵循“尽早恢复”的原则,将故障组件尽可能快速、高效的恢复和重新加入系统。

1.3.单点治理

从这一节开始,我们来逐步介绍提升微服务系统可靠性的手段。


分布式系统存在单点故障(Single Point of Failure,SPOF),是故障频发且无法收敛的一个重要原因。故障本身是不可避免的,但是当问题发生时,如果系统单点导致无法进行及时的 Failover,则系统可靠性就会大打折扣。因此,系统的单点治理迫在眉睫,故障的单点治理水平也是衡量一个系统可靠性的重要指标。


单点治理的核心思想是提升冗余度,不管是无状态的服务,还是有状态的存储、中间件,都可以通过冗余部署的方式,形成同构、同质的集群化架构,从而达到压力分摊、互相备份与切换的效果。

1.3.1.组件类型与粒度

在梳理系统单点时,必须要弄清楚系统组件的类型和粒度,这些内容通常也代表了故障发生的颗粒度。常见的粒度有:


服务器:服务器是系统赖以生存的基本运行时环境,通常包括物理机、虚拟机和容器。服务器的粒度可以简单按物理机、机架、机柜、机房、地域来划分。具体可归纳总结为:

  • 单台物理机、单一虚拟机或容器实例;

  • 来自同一机架或机柜的多台物理机,来自同一物理机的多个虚拟机、容器实例;

  • 来自不同机架或机柜的多台物理机,来自不同物理机的多个虚拟机、容器实例;

  • 来自同一机房的多个物理机、虚拟机和容器;

  • 来自不同机房的多个物理机、虚拟机和容器;

  • 来自同一地域的多个物理机、虚拟机和容器;

  • 来自不同地域的多个物理机、虚拟机和容器;


中间件:中间件通常包括存储(数据库、缓存等)、消息中间件、搜索中间件、微服务组件等各类除服务器之外的基础设施。中间件以单实例和集群化的部署架构最为常见,集群化往往使用场景更普遍,很多中间件提供了原生的集群解决方案,如 Redis Cluster、Kafka、MongoDB、HBase、Elasticsearch 等;


中间件的实例本身也是由服务器构成,这里不再重复赘述服务器的单点情况,而是直接列举中间件的拓扑单点:

  • 中间件只有单实例;

  • 中间件为集群架构,包含多实例,分布在同机房;

  • 中间件为集群架构,包含多实例,分布在同地域的不同机房;

  • 中间件为集群架构,包含多实例,分布在不同地域的不同机房;


现代的企业级、分布式物理架构基本都是多机房、多可用区和多地理位置部署,本文观点是,如果不特别指出,默认的单点粒度至少是机房级别,即系统至少应该跨机房部署,以及具备跨机房容灾能力。


从以上列举的服务器和中间件组件的粒度可以看出,有很多种粒度类型其实是存在单点风险的,亟待治理。

如何进行单点治理,提升系统冗余度、弹性和可用性?通常见仁见智,各企业、机构、各架构师的解决方案通常会不一样,并没有标准答案。本文试图给出一种经过实践的冗余高可用方案,如图 1‑3 所示。

图 1‑3 一种跨地理位置的多数据中心物理架构


这是一种在实际生产环境中验证过的多数据中心物理架构。物理层面,由华北 Region 和华南 Region 组成,每个 Region 又由 3 个物理 IDC 机房组成。逻辑层面,包括接入层、服务层和中间件组件。下面


来分析每一层的潜在单点故障风险,以及对应的冗余方案和故障转移措施。

1.3.2.接入层高可用

接入层包括 DNS 域名智能解析、SLB 负载均衡以及 Nginx 反向代理。


DNS 域名解析:DNS 域名智能解析,根据用户的地理位置和运营商进行智能解析,将请求流量引流到离用户最近的 SLB 负载均衡 VIP 上。


通常会针对每一组 VIP,设置多个 SLB 负载均衡互相热备,流量可以打到任意一个 SLB 上,这种方案既可以起到负载均衡、分摊压力的作用,还可以达到冗余、故障转移的目的。


这样做的好处可以使得用户就近接入,避免跨地理位置处理请求事务,造成不必要的延迟。比如北京移动用户,就会优先访问部署在北京移动机房的 SLB 负载均衡。


DNS 智能解析故障包括两方面:

1. 局部解析失效,或无法解析到正确的 VIP 上,如图 1‑3 中 1 所示。在实际环境中,偶尔会发生,特别容易发生在解析策略比较细的情况下。比如,北京联通的用户,可能会被解析位于河北机房的 VIP 上。还有一种情况是局部解析策略失效,如北京电信的用户无法解析。


由于配置了多种解析策略以及多组 VIP(每组又包含了 N 个 SLB 组件),因此,当故障发生时,完全可以通过调整 DNS 解析策略,将故障域的用户请求解析到正常的 VIP 上。


2. DNS 域名解析完全失效,如图 1‑3 中 2 所示。这是一种极其严重、灾难性的单点故障,也是很容易被忽略的一种场景,会导致客户端访问完全无计可施,所有网站和 APP 页面均无法加载。


一种常见的解决方案是采用多 DNS 域名机制。客户端预先对服务端提供的 DNS 做冗余,埋点备用 DNS 域名,最好采用 2 个以上的备用域名。


当故障发生时,通过远程配置中心下发切换指令,进行域名切换。或者客户端会植入负载均衡和健康检查算法,当探测到当前 DNS 域名不可访问时,可自动切换到备用域名上,这种能力通常会被封装成组件或 SDK,供客户端透明的使用。


SLB 负载均衡:SLB 负载均衡主要是提供一个高可用的集群,对外通过 VIP 暴露访问端点。通常采用硬件设备,或 LVS、HAProxy 之类的软件实现。SLB 的重点是高可用的 VIP 机制,以及负载均衡、健康检查、SSL 卸载机制,通常只做流量转发。


SLB 的故障通常有两种:

1. 单 SLB 故障,如图 1‑3 中 3 所示。通常是 VIP 漂移失败导致。解决方案也很简单,直接在 DSN 解析层面,将该 SLB 的 VIP 摘除即可。业务层面需要接受短暂的 DNS 缓存带来的请求失败问题。


2.多 SLB 故障,如图 1‑3 中 4 所示。这种情况不常见,通常是多个 SLB 部署在一个物理相关的设备下导致,如共享一个机架机柜、共享一个机房或网络设备。在故障预案层面,需要将多 SLB 进行合理的分散部署,至少需要跨 2 个机房。


当问题发生时,由于我们有多组 SLB,可以修改 DNS 解析策略,将故障组进行隔离,流量导入其他组 SLB 即可。


Nginx 反向代理:Nginx 反向代理的作用主要是提供限流、黑白名单、鉴权、流量转发、监控、日志等功能,也是众多 API 网关得以实现的基础组件(如 OpenResty、 Kong)。


Nginx 的物理部署架构和 SLB 完全不同,一般是按业务机房进行部署的,作为业务系统在当前机房的前置机。这样做的好处是,一旦发生机房级别故障,直接摘除 Nginx 集群即可快速对流量进行阻断和隔离。


Nginx 反向代理的单点故障通常有两种:

1. Nginx 单机故障,如图 1‑3 中 5 所示。这种情况很常见,因为 Nginx 本身也是借助服务器建立的服务,服务器的各种问题当然也会导致 Nginx 故障。解决方案也很简单,由上层的 SLB 通过健康检查,自动摘除即可。


2.一组 Nginx 故障,如图 1‑3 中 6 所示。通常由软件 BUG 或机房级别故障导致,这种问题发生比例不高,但确实会存在。解决方案是由 SLB 自动摘除该组 Nginx,流量自动被旁路到其他机房。

1.3.3.服务层高可用

这里的服务层一般指无状态业务集群,即任何一个服务实例均不存储数据,除了一些可随时失效(Invalidate)或剔除(Evict)的本地缓存,以及写入本地磁盘的日志日志。业务集群无状态的好处是显而易见的,可以有效的提升系统灵活性、伸缩性和故障转移速度。


服务层的单点问题有两种:

1. 单一服务器实例故障,如图 1‑3 中 9 所示。一般是由服务器自身的故障点导致,这种问题的发生概率非常高,当集群规模达到 1000 以上的实例时,几乎每天都会随机出现 1~5 个实例故障。


当单一实例故障时,通常不需要采取任何手动措施,而是由上层的 Nginx 代理通过健康检查来自动摘除不健康实例。


2. 多服务器实例故障,如图 1‑3 中 10 所示。一般是虚拟机、容器对应的宿主机故障导致,当然也可能会由程序 BUG 触发或性能问题导致,如不合理的超时、重试设置导致的服务雪崩,流量突增导致的服务过载等,往往会将该服务集群直接压垮。


当多实例故障发生时,故障面相对较大,可能在借助 Nginx 自动健康检查摘除的同时,需要人工介入。


值得注意的是,Nginx 的健康检查机制和 Proxy 重试一般是基于 Upstream 的延迟和 HTTP 状态码进行判断的。当 Upstream 节点宕机或进程退出时,Nginx 是很容易检测并将其剔除。然后,现实情况并不总是这样,之前的故障演练里也提到过,服务器节点可能会发生 CPU、存储、磁盘、网络的负载指标不正常,对外的表现通常是耗时时高时低,很不稳定,这种情况 Nginx 不一定能检测和判断准确,往往会导致不能及时摘除,或者相反,导致误判。


因此,一个高效、精准的健康检查机制至关重要,有条件的读者可以尝试自行研发一些组件,通过读取监控数据来智能分析故障点,并预测问题发生的时间和部位,实现自动 Failover。

1.3.4.中间件高可用

很多中间件都提供了原生的集群部署能力,如互联网业务系统常见的面向 OLTP 的存储系统:MySQL、Redis、MongoDB、HBase、Couchbase、Cassandra、Elasticsearch 等;消息中间件如 RocketMQ、Kafka 等;分布式协调服务如 Chubby、Zookeeper 等。


这里讨论的也是这种集群化架构的冗余能力,如果读者的生产系统中间件尚未具备多实例的集群部署,建议尽快对架构进行升级换代。


集群化的中间件天然消除了单实例问题,但这样就能万无一失、永保平安了么?显然不是,我们来分析下常见的中间件故障点以及应对措施。


以常见的存储系统为例,通常有主从式(Master-Slave)和去中心化两种拓扑架构模式。如 MySQL(主从式)、Redis(3.0 版本以下或主从式)、MongoDB(复制集)、Zookeeper 就是采用经典的主从式架构,而 Couchbase、Cassandra 这类 NoSQL 存储则是采用无中心的架构。


主从式架构:主从式集群通常以读写分离的方式为客户端提供服务,即所有应用节点写入 Master 节点,多个 Slave 组合并采用负载均衡(服务端代理或 DNS 域名)的方式分摊读请求。


当主从式集群的 Master 实例发生故障时(如图 1‑3 中 11 所示),通常会由健康检查组件自动检测,执行重新选举,将集群的 Slave 节点提升为 Master。整个过程对客户端透明,切换时间视具体数据量和网络状况而变化,当数据量很小时,客户端几乎无感知。


当 Slave 实例发生故障时(如图 1‑3 中 12 所示),处理流程相对简单,只需要将故障节点从负载均衡摘除即可。


无中心架构:去中心话的架构简单高效,以 Cassandra 为例,任何节点均可支持读写以及请求转发,数据通过一致性哈希的方式进行分片,并且在其他节点上保存副本,客户端可随意调整一致性级别(Consistency Level)来在读写性能、强弱一致性语义之间达到平衡。


当节点发生故障时(如图 3‑3 中 13 所示),集群会自动识别到故障节点,并进行摘除。数据会发生 Rehash,而客户端也会执行 Failover 操作,挑选其他健康的协调节点,重新建立连接。

1.3.5.同城多中心

前面的章节重点介绍了实例级别、分组级别和集群级别的故障点以及冗余、故障转移方法,并没有提及机房级别故障。然而,机房级别故障确实是频繁发生的问题,需要引起重视。


在实际生产环境中,尤其是需要保持 7*24 小时高可用的互联网业务,不可能只部署一个机房,而是采用“同城多中心”的方式,来消除机房级别的单点故障,如图 3‑3 所示,通过负载均衡和前置机转发,实现了南北流量及东西流量在多机房的调度和转移。


一般来说,企业会在分布在各地理位置的数据中心建立多机房,如在华北数据中心建立 3 个 IDC,依次编号为 HB-IDC-01、HB-IDC-02 和 HB-IDC-03。


还有一种数据中心的建设方式也比较常见,即遵循 Region-AZ-IDC 的设计模式,其中 Region 就是地理位置,如华北、华南。AZ(Available Zone)指可用区,表示应用的最小部署单元,通常由多个 IDC 组成。


除非特别指定,本文通篇默认会遵循“地理位置-数据中心-机房”的设计模式。


如图 1‑3 中 7 标注所示,当 HB-IDC-03 出现故障时,整个机房将不可用。这种情况时有发生,不可避免。原因也很复杂,可能是机房基础设施故障(如电力系统问题、温度、湿度失调),也可能是网络故障(如核心交换机出错、网络割接误操作等)导致。


常见的故障转移方式在故障预案中有提及过。接入层面,通过调整各级组件(DNS、SLB、Nginx 等),实现下一级接入层安全转移;服务层面,通过调整接入层,对流量进行重新调度,将故障机房的服务进行隔离;中间件层面,通过集群的健康检查机制,调整集群拓扑和读写方式。

1.3.6.两地三中心

前面提到了机房级别的故障,实际中还会存在地域级别的故障,一般由自然灾害(暴雨、洪涝灾害、地震等)或骨干网中断(如光纤被挖断)导致。因此,对于规模较大、可靠性要求较高(尤其是金融级别)的业务系统,靠一个地理位置数据中心来支撑流量显然是不够的。因此,很多公司从容灾的角度考虑,建立了“两地三中心”机制。


所谓两地三中心,是指在两地建立三个数据中心,两地之间通常需要保持 1000 公里以上的距离,避免距离太近导致两地一起失效,反而起不到灾备效果。


以华北两个数据中心,华南一个灾备中心来举例说明。业务系统在华北两个个数据中心正常提供服务,而华南数据中心一般不提供服务,偶尔也会提供少部分读服务。生产环境产生的数据,会异步复制到华南灾备中心。


当华北的两个数据中心出现重大故障时,可以将用户流量切换到华南灾备中心,由该数据中心接管线上业务请求。


正如故障转移章节提到的,两地三中心有一个很大的弊端,在于灾备中心通常是冷备角色,平时不会频繁的进行切换演练,或者几乎就不会考虑演练。关键时刻可能就会由于各部件失效、或过载,导致没法承接流量。另外一个问题就在于,冷备中心一般需要和两个主数据中心的容量一致,但又不会提供活跃的服务,这会导致严重的资源浪费。


鉴于此,我们需要考虑异地多活的部署架构,下一章节会重点说明。 

1.3.7.异地多活

异地多活架构可以说是高可用物理架构中的终极形态。


大型的跨国公司(如 Google、Amazon、Microsoft)业务系统繁多且复杂度高,服务的用户也会遍布全球,因此往往会在全球主要战略位置建立多个数据中心。

图 1‑4 全球数据中心分布样例


如图 1‑4 所示,是一个大规模的公有云服务(如 Amazon AWS,Microsoft Azure 等)在全球的数据中心分布:北美、南美、欧洲、亚太、澳洲、非洲等。


国内的很多互联网业务(如电商、社交、视频、广告、出行、本地服务等)也争先恐后在全国各地建立了多个数据中心,常见的有:华北、华东、华南、华中等。


如此规模的异地多活物理架构部署,挑战性是毋庸置疑的。主要来自两个方面:

1. 数据的复制延迟。跨地域的网络情况不容乐观,很容易发生延迟升高、丢包率升高。而数据在各个数据中心的复制也会随之受到影响,数据不能高效及时的复制到目标数据中心,就会导致用户访问的数据出现延迟和不一致,有时候用户体验是不可接受的。


2. 一致性要求与写冲突。某些业务要求的一致性较高,甚至是强一致,在跨地域复制的情况下几乎是不可能实现的。另外,有些存储系统如果设计成多点写入的话,则会出现写入冲突,如何解决冲突也是一个比较棘手的问题。


如图 1‑3 所示,通过 DNS 智能解析,将不同地理位置的用户就近接入当地数据中心。在数据中心内部,服务集群一定是本地部署的,而中间件集群则会根据不同的中间件特点而独立设计:

  • 一种设计模式,是单一中间件集群横跨各数据中心,通常是类似于 MySQL 的 Master-Slave 架构。写入操作回流到 Master 节点所在数据中心执行单点写入,再读取本地 Slave 节点以提升性能。这种方式比较常见,因为操作简便,缺点是写入性能差,当数据中心之间网络发生分区后,写入只得被迫中断。另外会带来数据复制延迟问题。

  • 另一种设计模式,也是单一中间件集群横跨各数据中心,通常是类似于 Cassandra 的去中心化结构。可以支持多点写入,这样读、写操作就可以在本地数据中心完成。性能表现较好,但会带来数据复制延迟和冲突修复的问题。

  • 还有一种设计模式,是在每个数据中心部署独立的集群,本地直接执行读、写操作,中间件集群之间跨地域复制数据。性能也比较好,但同样会带来数据复制延迟和冲突修复的问题。


不难看出,无状态的服务在异地多活架构中可以做到游刃有余,只需要调度流量即可。但数据层面却成了阻碍多活架构的核心焦点。为了解决以上问题,很多公司尝试了 Sharding 化部署的思想,比如阿里的单元化架构设计。


Sharding 化部署的核心思路,在于将用户流量集中在一个数据中心完成,完成交易的封闭处理,即同一个事务请求处理以及对应的数据,不应该跨数据中心。


常见的设计方案就是选择 Sharding Key(比如用户 ID),在接入层将请求会话路由和粘滞到对应的机房单元。数据层面也是根据该 Sharding Key 进行路由读写。路由算法是稳定的,因此无论该用户发起多少次请求,处理流程和对应数据都一定会进入对应的部署单元处理,不会出现由于跨单元而出现数据不一致的问题。


仔细思考一下,这种 Sharding 化部署真的无懈可击吗?不一定!最常见的问题就是维度的变化使得读写必须要横跨数据中心。以电商业务为例来说明,假设存在买家、卖家和商品维度,当前选取的 Sharding Key 为买家 ID,也就是说同一买家的任意请求都会落到同一数据中心来处理,包括产生的数据也会落到该中心的存储系统中。


如果买家下单后,同时要求减少商品库存,如果库存不足还要对该商品进行下架。另外,还需要给买家增加信誉度积分。如果以上这些操作要求是强一致的,即下单后需要马上看到库存变化和卖家积分变化在页面上同步显示,这种场景通过 Sharding 化的多活架构就没法实现。当然,如果可以接受最终一致,则可以通过消息中间件,将数据变化同步到其他的数据中心,页面经过一段延迟后即可显示变化。


显然,异地多活是没有银弹的,很难做到高性能、强一致性和高可用性都能同时得到满足,这和 CAP 的原理也很相似。 

1.3.8.容易忽视的 Worker 高可用

Worker 类组件是微服务架构中不可或缺的一部分,但其高可用却最容易被忽视,因为这一类组件往往默默无闻在背后工作,却又担任着重要的角色,一旦失效,可能报表就无法生成,数据预处理就会失败,缓存预加载的数据就会丢失。


Worker 类的高可用关键在于“监视”和冗余部署,当有合适的 HA 监视组件发现 Worker 不工作或状态不健康,并能及时执行 Failover,则问题就可以迎刃而解。


有一类 Worker 通常是消息中间件的消费者(Consumer),由于消息中间件天然具备消息投递的负载均衡和节点发现能力,所以这类 Worker 应用也就很容易具备高可用特性。当出现消费节点宕机或消费缓慢问题,消息中间件会自动停止消息投递。


而大部分自研的 Worker 类应用,由于不存在 HA 监控,再加上系统往往以单实例的方式部署,因此不具备任何高可用。我们可以设计一套分布式的架构,提升系统可用性,如图 1‑5 所示。

图 1‑5 一种高可用的分布式 Worker 设计


以 Zookeeper 为协调中心,设置 Leader 和多个 Follower 节点。其中,Leader 节点接受来自控制平面(Control Plane)的任务分配和一系列控制指令,而 Follower 节点则会执行 Leader 分配的任务。Follower 可以设计成 Active-Active 模式或 Active-Passive 模式。


当 Leader 或 Follower 节点宕机时,会触发 Zookeeper 的 ephemeral znode 感知到事件并通知所有节点,从而触发一次新的选举或任务 Rebalance,这些操作可以利用一些 high-level 的组件如 curator 来辅助实现,无需从底层开发。

1.4.超时

无数惨痛的生产环境事故和经验教训表明,不正确的超时设置是造成微服务级联故障和雪崩的头号杀手。遗憾的是,大量的工程师完全没有意识到这一点,每年都有大量的线上服务在超时方面反复栽跟头。


本章节就来聊一聊超时的问题、预防措施和经验总结。

1.4.1.超时与雪崩

微服务的分布式特性,使得服务之间的调用和互操作极其频繁,随着业务的拓展,服务间调用管用关系也会变得错综复杂,如图 1‑6 所示。

图 1‑6 微服务间调用关系


如果服务间调用未设置超时,或超时设置不合理,则会带来级联故障风险。当某一系统发生故障时,服务不可用,上游就会出现调用延迟升高。如果没有合理的超时参数控制,则故障就会顺着调用链级联传播,最终导致关键路径甚至所有服务不可用,如图 1‑7 所示。

图 1‑7 由微服务 G 故障导致的大部分服务不可用


这种级联故障现象也称为“雪崩”,雪崩带来的影响不仅仅是响应慢,更严重的是会耗尽系统资源,使得整个系统完全瘫痪。


以基于 Spring Boot 开发的微服务为例,默认的 Tomcat 连接池大小为 200,也就是说默认单实例只允许同时存在 200 个并发请求。这在平时是毫无压力的,但雪崩发生时,所有的请求就会一直在等待,连接池迅速就被占满,从而导致其他请求无法进入,系统直接瘫痪。


要想解决雪崩问题,就必须设置合理的超时参数。以 Java 应用来说,常见的超时设置主要有 HttpClient 和各类中间件系统,尤其是数据库、缓存。一般来说,服务间调用超时和数据库访问,建议设置超时时间不超过 500ms,缓存类超时时间不超过 200ms。

1.4.2.警惕不合理的连接池超时设置

有一类超时问题比较隐蔽而且风险很高,缺乏经验的工程师很难发现。以 HttpClient 连接池来说,通常大部分人都正确设置了创建连接超时(ConnectTimeout)和读取超时(SocketTimeout),但却忽略了连接池获取连接超时(ConnectionRequestTimeout)的设置。


当目标服务超时发生,有时候会发现尽管设置了读取超时,系统还是会出现雪崩。原因就是连接池获取连接默认是无限等待,这就导致了当前请求被挂起,直到系统资源被耗尽。这里给出一种实际生产环境使用的配置。

RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(500).setSocketTimeout(500).setConnectTimeout(500).build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();cm.setMaxTotal(maxTotal);// 整个连接池最大连接数cm.setDefaultMaxPerRoute(maxPerRoute);// 每路由最大连接数,默认值是2cm.closeExpiredConnections();cm.closeIdleConnections(200, TimeUnit.MILLISECONDS);CloseableHttpClient httpClient =HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(requestConfig).setKeepAliveStrategy(new ConsutomConnectionKeepAliveStrategy(30000L)).disableAutomaticRetries().setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)).build();
复制代码

 

当发生读取超时的时候,当前路由的并发连接数迅速被占满,无连接可用。新的业务请求在等待 500ms 后就会退出请求,从而有效保护了服务不被堆积的请求压垮。

1.4.3.动态超时与自适应超时

实际场景中,如果对所有的服务调用超时都搞“一刀切”,比如都设置为 500ms,则会过于死板,缺乏灵活性。比如有些服务调用量并不高,但是延迟偏高,而有些服务性能高,TP99 可能才不到 100ms,服务提供方也能承诺服务的性能可以长期得到保证。


这种情况就可以考虑动态超时和自适应超时方案。


所谓动态超时,是指可以通过控制后台,人为在运行时干预超时参数。还是以 HttpClient 为例说明,可以通过配置中心,针对某些 API URL 动态调整参数,代码样例如下:

// 配置中心控制接口超时HttpClientInterceptor.setupRequestConfigIfNeeded(request);
public static void setupRequestConfigIfNeeded(HttpRequestBase requestBase) { if (isDynamicTimeoutEnabled()) { String key = getKey(requestBase, HTTP_CLIENT_APPLICATION); TimeoutHolder timeout = HTTP_CLIENT_TIMEOUT_CACHE.get(key); if (timeout != null) { RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout( timeout.getConnectionRequestTimeout()).setConnectTimeout( timeout.getConnectTimeout()).setSocketTimeout(timeout.getSocketTimeout()).build(); requestBase.setConfig(requestConfig); } }}
private static boolean isDynamicTimeoutEnabled() { String dynamicTimeoutEnabled = getStringConfig(NS_HTTP_CLIENT_TIMEOUT, HTTP_CLIENT_SWITCH, "false"); return "true".equals(dynamicTimeoutEnabled);}
复制代码

所谓自适应超时,本质上是一种自动化的动态超时。可以在服务调用方实时监控目标服务的调用 QPS、耗时和错误率,当检测到耗时较低,则可以预测后续的耗时情况并调整参数配置。如果出现大量的超时,自适应控制也可以将超时阈值进一步调低,避免调用链路上耗时升高带来不必要的损失。

1.4.4.超时的套娃原则

理想情况下,超时的参数配置应该是遵循“套娃原则”,即沿着调用链,超时的阈值设定应该是逐级减小的。


如果超时是逐级增大的,则会出现:在上游已超时返回的情况下,下游还在继续处理,造成不必要的资源浪费。如果反过来,上游还在等待,下游可能已超时并进行再次重试,这样当前请求的成功率就会得到提升。

1.5.重试

重试是一把双刃剑,既是提升服务间调用成功率、容错的一种重要手段,也是造成流量放大、服务过载的原因之一。

1.5.1.为什么要重试

当微服务系统功能无法正常工作,或远程调用失败时,往往需要重试机制,在有限的时间段内来提升调用成功率。


服务调用的重试与否,需要看场景,如果对业务处理的成功率要求很高且目标服务可以承诺幂等性,可以增加重试。重试必须具备上限,不可以执行无限制的、反复的尝试。


重试的位置无处不在,既可以是来自客户端的用户手动重试,也可以是服务端各组件之间的自动重试。

当客户端出现全部或局部页面加载失败时,可以通过友好的文字、图片等方式来引导用户手动重试加载,如提示语“网络似乎开了点小差,请再次重试”。


在服务端,接入层也可以增加重试能力,常见的就是 Nginx 的代理重试。如以下配置:

#重试设置proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_504 | off;  //出现哪些情况,会请求其他后端服务器重试,off则不重试;proxy_next_upstream_timeout 10s; //即proxy_next_upstream_timeout时间内允许尝试proxy_next_upstream_tries次;proxy_next_upstream_tries  2; //
复制代码

而应用层重试一般指 HTTP API 调用或 RPC 服务调用的重试设置,也包括各种中间件 SDK 自带的原生重试能力。

1.5.2.重试风暴

毋容置疑,重试会放大请求量。重试触发的条件一般是目标服务或中间件系统出现了不健康状态或不可用,在这种情况下,无论是来自客户端的用户重试或来自服务的组件间的重试,都会引起“读放大”效应。这会给原本就处于故障状态的组件雪上加霜,极端情况下会导致系统直接奔溃。


我们把这种由于大量重试导致的瞬间流量放大造成的类似 DDoS 攻击效应,称为“重试风暴”。重试风暴是造成系统过载和奔溃的重要原因,需要努力避免。

1.5.3.退避原则

为了减少重试风暴带来的危害,一种行之有效的方法是增加重试的时间间隔,通过退避(back-off)的方法来逐步增加等待时间。最常见的做法是指数退避(exponential back-off)原则。


举个例子,设置初始重试等待时间为 10ms,重试上限为 5 次。当目标系统出现故障时,调用方等待 10ms 后发起重试,如果请求依旧失败,则客户端等待 20ms 后再次发起重试。如果再次请求失败,则等待 40ms 后再次重试,以此类推,直到返回正确的结果或达到重试上限。

1.5.4.指数退避的风险

指数退避的特性会使得重试间隔时间按指数级别递增,在极端情况下,会导致单次请求的耗时被严重放大,从而导致系统资源被耗尽,进一步拖垮系统。需要引起格外注意。


建议减少初始等待时间,并且严格限制重试次数上限,比如只重试一次。

1.5.5.动态重试

为了兼顾重试带来成功率提升的收益,以及减少重试风暴带来的流量放大和耗时提升。我们可以考虑采用动态重试方案来进一步提升系统的灵活性。


通过在服务调用方预先植入动态开关逻辑,可以在运行时动态调整重试参数。包括:

  • 开启和停止重试功能;

  • 调整重试次数上限;

  • 调整重试模式,是普通的重试操作,还是指数退避重试;

  • 如果是指数退避,可以调整初始重试间隔时间;


以上操作既可以是全局操作,也可以按系统、或目标服务的颗粒度进行操作。

1.6.限流

限流是保护业务系统的必不可少的环节。当流量突增时,一个拥有良好限流设计的系统能够抗住流量,快速丢弃多余的请求,让内部的微服务系统不受影响。


限流是一种有损的可靠性方案,限流生效时,有部分用户是无法正常加载页面的。同一用户也可能会出现正常访问和被限流交替发生。尽管如此,限流仍是抵御突发流量的不二法门,有损的服务一定胜过服务被全量压垮。

1.6.1.限流的位置与粒度

和超时、重试一样,限流的位置也是遍布到微服务的各组件,甚至是客户端应用。通常离用户越近的位置,限流效果越明显。


常见的限流位置有客户端限流、接入层限流和服务层限流。客户端可以对单一用户的操进行限流,避免用户反复重试,比如可以在用户下单后,将按钮置灰,防止用户多次重试。接入层限流是服务端最有效的位置,可以通过令牌桶或漏桶算法,对远程 IP 进行限流,或对下游服务进行限流。应用层限流一般为单机限流,具体限流阈值通常以单机压测的结果为基准进行估算。


限流的粒度可以是全局限流,也可以是服务粒度,或者是 API 根目录,甚至可以设置为特定的 API URL。这些粒度应该可以支持在控制后台动态配置。

1.6.2.主动限流与被动限流

所谓主动限流,是指服务的提供方主动评估服务的容量和峰值 QPS,提前配置限流阈值并说明限流后的服务表现。服务的使用方无需关心这些服务的性能和可靠性,只需要按照限流的标准进行容错处理即可。这种方式是一种比较友好的合作模式。


被动限流则恰恰相反,服务的提供方无法了解和评估自身的服务状态,上游流量突增后也不具备技术手段来应对。这种情况可以考虑服务调用方实施被动限流,即对服务的使用方、各种存储、中间件等资源增加限流约束机制,理想情况应该可以支持控制后台随时调整。

1.6.3.警惕泄洪流量

有一种情况很容易被忽略,当一组消费者(Consumer)同时处理来自消息中间件的消息时,往往由于某些内部组件的延迟(如写入存储缓慢、第三方服务延迟升高等)或上游消息量突增导致消息积压,一般会修复慢的组件或增加更多的消费者来解决这些问题。当这些瓶颈解除后,消费者的处理速度会大幅提升,瞬间积压的消息会被大量处理,从而导致下游系统出现过载现象,或出现主从数据同步出现大量的滞后(Lag)。我们姑且称这种流量为“泄洪流量”。


泄洪流量在真实场景中经常会发生,而且会带来严重的影响,因为很多系统的设计(如 CQRS 架构风格)出于资源限制、或架构的简洁性考虑,往往会让这些消费者和线上服务共享存储资源(尤其是写库)或调用公共服务。当泄洪流量发生时,这些存储资源可能就会出现负载极速上升、主从数据同步 Lag 被拉大,公共服务甚至可能会被立刻打垮。


解决方案其实很简单,消费者的应用集群也应该和在线服务一样,设置限流机制。

1.7.降级熔断

降级、熔断是另外一种保护手段,这是一种“弃卒保车”的制胜策略。当局部系统或组件不可靠时,通过对该故障域进行降级、熔断处理,并展示有损的降级数据,从而达到快速隔离故障的目的,且用户体验牺牲不明显。降级熔断的关键在于识别问题和降级策略。

1.7.1.降级的位置与粒度

和限流很相似,降级的位置也是越多越好,降级层次越多,系统就越灵活可控。通常可降级的位置有客户端、接入层和应用层。


客户端降级可以有效抵御大量的无效请求入侵服务端系统。当客户端识别到后端服务工作在不健康状态时,可以执行本地降级,屏蔽一些操作(如评论加载),并且将预先埋入的提示语展示给用户,如“评论似乎开了一点小差,请稍后再试”。


当接入层检测到 Upstream 层服务有故障时,如延迟升高、响应状态码不符合预期等,可以对该服务请求执行“快速失败”处理,向请求方输出错误提示,或输出预先缓存的退化数据。


应用层的降级相对更灵活,可以设置很多降级策略,如基于请求 QPS、请求的长尾延迟、超时率或错误率等,并且可以提前根据不同策略处理好降级数据。


降级的粒度可以是全局限流,也可以是服务粒度,或者是 API 根目录,甚至可以设置为特定的 API URL。这些粒度应该可以支持在控制后台动态配置。

1.7.2.主动降级与被动降级

主动降级是指直接将对外提供的服务(如 RESTful API)降级掉,输出降级错误提示,或一组缓存的业务数据,这些数据通常不是那么及时,甚至是“脏”数据。主动降级是一种粗粒度的降级。


被动降级是指将不健康的第三方服务或组件降级,这通常是一种细粒度的降级策略。降级的结果就是当前调用无法返回数据,或者返回缓存的旧数据,基于这些降级数据,服务端再处理本次请求事务。

1.7.3.强弱依赖

微服务架构的依赖非常多,比如服务间调用(HTTP/RPC)、数据库读取、缓存访问、消息中间件订阅与消费等等,这些依赖项构成了微服务系统的拓扑结构。


根据业务的不同,有些依赖是必不可少的,当出现故障时,整个大系统就面临着不可用的风险,我们把这种依赖称为“强依赖”。比如 Feed 业务中的推荐服务,就是一种典型的强依赖,当推荐服务不可用,整个 Feed 流就面临着无数据可用的困境。


有些依赖项在出现故障时,可以丢弃,业务上不会产生明显的影响,我们称之为“弱依赖”。比如在 Feed 业务中的用户服务和缓存,当出现问题时,完全可以抛弃,对用户的影响就是每条 Feed 的作者头像和昵称丢失,业务层面一般可以接受。


微服务架构设计的一个重要准则就是一切尽可能弱依赖,解除强依赖。这意味着在代码开发阶段,需要面向失败设计,做好依赖项失败的降级处理。

1.7.4.降级数据

降级就意味着丢弃弱依赖、解除强依赖,意味着需要输出退化的降级数据给调用方,降级数据的准备是有技巧的。我们来分析下常见降级数据和请求的关系:

  • 降级数据和用户无相关,如商品的基本信息,并不会随着用户的不同而发生变化;

  • 降级数据和用户强相关,如订单列表、购物车等,不同用户的数据完全不一样;

  • 降级数据和用户弱相关,如视频播放,视频的基本播放属性不随用户变化,但鉴权部分会随着免费、付费用户的不同而存在区别;


好的,有了这三种降级数据类型,我们就可以有针对的去准备数据。


一种建议的方案就是在正常读取依赖数据后,异步写入降级存储系统,这个存储一般由大存储量的 NoSQL 数据库来承担即可,可以有效提升降级数据的写入和读取效率。对于用户无相关的数据,可以设置维度 Key,如 fallback_product_<pid>标识了某商品的 Key,并将数据以 Value 的形式写入存储;对于用户强相关和弱相关的数据,Key 需要增加用户维度,如 fallback_order_<orderid>_uid_<uid>表示某用户的订单数据。

1.7.5.熔断

从某种意义上来说,超时和限流可以认为是一种低级形态的降级策略,而熔断则是一种高级形态的降级。


熔断器设计模式是一种很经典的弹性容错算法,如图 1‑8 所示。当检测到系统指标出现异常并且达到一定的阈值后,熔断器就会自动打开,从而切断对依赖性的调用,然后可以根据策略选择一份降级数据。当等待时间超过预设的门槛值后,熔断器会尝试半关闭状态,部分请求开始调用真实的依赖项,当结果符合成功率标准时,熔断器会关闭,反之则熔断器再次打开,依次类推。


熔断器的好处是显而易见的,使用熔断器可以在问题累计到一定的程度后,直接断开依赖、快速失败,从而有效保护系统本身和依赖方,避免全量系统发生雪崩。

图 1‑8 微服务熔断器设计模式


常见的开源熔断器实现有 Hystrix 及其后继者 Resillence4j、Sentinel 等。感兴趣的读者可以自行阅读和调研官方文档,并评估是否可以应用到自身微服务项目中。


使用熔断器需要注意几个问题:

  • 有些熔断器组件是代码高侵入的,但粒度细、可控性强,实际使用时需要在灵活性和代码可维护性方面仔细权衡利弊;

  • 使用熔断器是有额外性能开销(Overhead)的,比如 Hystrix 的线程池隔离模式,会按需创建一个或多个线程池,这些线程会占用更多的 CPU 和内存。有条件的读者完全可以自行设计轻量级的熔断器;

  • 一定要根据实际情况尤其是压测指标,设置好门槛值(Threshold),避免状态发生误判,造成不必要的线上故障;

1.8.过载

流量的过载是影响系统可靠性、导致服务不可用的的一个重要因素。笔者曾经经历过由于代码缺陷导致的流量被放大到 9 倍,服务器和存储系统的资源(CPU、内存、网络带宽、磁盘 I/O)在不到 5 分钟的时间内就迅速消耗殆尽,系统无法正常提供对外服务,几乎所有业务系统都未能幸免,其结果是灾难性的。


本章节来分析下过载的原因以及解决方案。

1.8.1.过载的原因

过载的根本原因是流量发生突增,但系统资源承载能力不足。这里的资源包括但不限于:CPU 计算能力、内存大小、外存(磁盘、SSD 等)容量、设备 I/O 速度、网络带宽、实例个数、中间件处理能力、第三方服务吞吐率,等等。


流量突增甚至在极端情况下可以产生脉冲流量。脉冲流量是指瞬间发生的超高流量,超出了日常流量峰值的好几倍甚至是几十、几百倍。流量突增的原因通常包括:

  • DDoS 攻击,尤其是来自于不同地理位置的僵尸网络;

  • 由于部分系统不可用,造成级联故障传导,导致从客户端到服务端内部全链路上的大量重试,引起的重试风暴;

  • 由于促销、秒杀、Push 推送或运营活动导致的短时间用户请求大量涌入;


内部系统承载能力不足可能来自两方面:

  • 容量已不能满足日益增长的业务需求;

  • 由于故障转移导致的临时资源不足。这种情况非常常见,鉴于此,很多系统都会设计成 N+1 甚至 N+2 冗余度;

1.8.2.过载解决方案

过载的解决方案包括两个基本原则:减少流量渗透和增加内部系统承载能力。


所谓减少流量渗透,是指丢弃不必要的流量。常见的做法就是限流操作,针对不健康、异常的流量(如 DDoS)在接入层进行清洗和过滤;针对其他突增的流量,如运营活动、Push 推送或重试导致的请求,通过限流组件来丢弃多余流量,确保传导到下游的流量符合预期。限流要尽可能靠近调用链路的头部,越靠前效果越佳。


另外,由于重试是造成流量突增的重要原因,我们应该增加系统的动态重试能力,当出现重试风暴时,可以远程下发指令关闭系统的重试功能。在一些大规模的促销活动之前,我们也可以提前预估峰值 QPS,决定是否要提前关闭重试。


增加内部系统承载能力,是指容量规划和扩容。容量规划通常依赖于压测的指标情况,根据系统压力的上限,来合理评估和圈定服务器、数据库、缓存、消息中间件等各种资源的需求上限,并提前预备好资源配额。


扩容一般包括按计划内扩容和紧急扩容。日常工作中会我们可以巡检系统的流量、数据量和容量,当发现当前系统承载能力已不能满足或即将不能满足容量需求时,需要执行扩容。紧急扩容通常发生在过载时,这类扩容要求迅速、无差错,这对系统的弹性伸缩能力要求很高。关于弹性伸缩,后面会单独说明。

1.9.轮值

Oncall 轮值是面向人员的可靠性措施,利用“人为治理“的思路,为“技术治理”提供有益的补充。Oncall 对在线服务尤其是互联网业务来说,是必不可少的一个环节。

1.9.1.有效的 Oncall

按照 Oncall 轮值的规模和模式,通常可以划分为两种情况:


专业运维团队:在一个小规模的 IT 企业及业务部门中,通常会设置专业的运维团队,该团队对所有系统的运行时状态进行 7*24 小时无间断监控和巡检,发出预警、处理和总结故障。这种方式的好处是各司其职,业务团队并不需要对运维部分负责,运维团队也不需要关心业务和开发。


DevOps 文化:随着企业规模的扩大及各种技术栈成熟度的提升,很多企业不再为各业务线设立独立的运维团队,而是奉行 DevOps 文化。简单理解就是研发和运维的界限开始变得模糊,大部分情况都是,研发团队同时也需要充当运维的角色。


不管采用哪种方式,都必须秉承几个基本的 Oncall 原则:

有合理的 Oncall 责任人排班。一般来说,每个人轮值一周的粒度比较恰当,既不会太疲惫,又不至于人员频繁交接带来额外成本。

对于 Oncall 责任人来说,应该在轮值周期内减免其他工作事项。比如对研发人员来说,Oncall 的一周内不应该再安排研发任务,这样有利于提升 Oncall 的专注度;

人员需要有备份。建议每个 Oncall 责任人的背后都应该至少有一个 Backup;

建立良好的 Oncall 规范,保证 Oncall 有章可依;


以下是一个实际的 Oncall 值日规范,供各位读者参考:

1. 值班目标•	通过告警推送(电话、IM、短信、邮件)、系统巡检(监控、日志等)等方式,及时发现系统异常并处理;•	接收并处理内部人员或外部用户报告的线上问题;2. 系统巡检2.1 巡检方式系统监控:按照值班报告,对后端的系统进行监控巡检,并留意告警监控详见:XXX
系统日志:在必要情况下,需要登录服务器,观察系统日志,重点查看异常日志(Exception,Error)服务器详见:XXX 2.2问题处理发现问题,或执行线上操作,需要及时周知项目组!!禁止私自做线上操作!!当出现以下指标异常时:• 系统QPS过高或过低,超过警戒线• 系统响应时间过高或过低,超过警戒线• 系统错误率升高,超过警戒线;导致异常的可能原因有:• 基础设施:如服务器、系统、网络、接入层、存储、消息中间件等故障;• 外部服务:如推荐服务、广告服务、直播服务等对外依赖的服务故障;• 系统缺陷:系统BUG导致的故障,如功能问题、性能问题;需要采取的措施:• 保留证据,如监控截图,日志保存,现场信息Dump等;• 对于紧急情况(如影响线上业务),必要情况可执行重启操作;• 对于非紧急情况,可尝试定位问题并hotfix处理;• 对于无法处理的问题,需要第一时间联系owner;
复制代码

1.9.2.Oncall 记录与故障总结

Oncall 值日生必然会发现、处理线上故障,这就意味着必须记录和总结故障。故障报告应该有固定的格式规范,便于复盘总结。以下是一个参考 Google SRE 规范而制定的故障报告模板和部分样例:

1.故障总体描述1.1 故障摘要//简单描绘故障基本情况2020-07-29日,由于代码误操作,将一条测试广告引入到线上Feed流中;
1.2 开始时间//故障开始时间,精确到分2020-07-29 16:20
1.3 结束时间//故障结束时间,精确到分2020-07-30 08:12
1.4 影响时间//故障持续的时间,精确到分17h52min
1.5 影响范围//故障的业务影响,如影响多少用户,影响多少数据,影响XX用户体验,影响XX效果日期 展现pv 展现uv 计费点击(已去重)2020-07-29 96598 20059 16462020-07-30 21741 7511 325累计影响点击1971人次
1.6 触发条件//能复现故障的步骤1.Feed流内容自动播放超过X秒,出现“这是一条测试广告”的内容广告;2.Feed流点击内容进入播放页面,播放超过X秒,出现“这是一条测试广告”的内容广告;3.点击该内容广告,导流到一个无效的Web页面;
1.7 根本原因//故障的根本原因,一般有:基础设施、外部服务、系统缺陷、流程规范等由于测试缺乏数据,开发为配合广告QA测试,在代码中写死Mock数据,上线时忘记删掉;
1.8 修复措施//故障的解决方案描述去掉Mock数据,上线修复;
1.9 发现途径//故障的发现方式,一般有:业务报障、报警、主动巡检等业务报障
2.经验总结2.1 好的方面//本次故障处理中,做的好的经验总结定位到问题后,响应及时,修复上线速度较快;
2.2 坏的方面//本次故障处理中,做的不好的经验教训1.问题发现不够及时,等待业务报障才开始处理;2.业务类数据无法触发告警,自动化用例尚未覆盖该场景;3.处理问题时,出现一次误判(时间线里有说明);4.开发代码自查,以及Peer Code Review不够充分;5.线上回测不够充分;
2.3 侥幸的方面//本次故障处理中,侥幸的方面使得损失没有扩大
3.未来改进措施//针对本次故障需要改进的措施,包括不限于:架构升级优化、系统查漏补缺、加强流程规范、升级监控告警等1.减少不必要的硬编码来Mock数据,改为利用Mock配置后台,更安全高效;;2.Mock配置后台目前在Feed服务中已具备,Q3会推广到其他10来个系统中,达到80%以上覆盖率;3.自动化用例增加广告字段检测能力,提前发现问题,预计本周五前完成 ;4.应给予研发更多的Code Review时间Buffer,在开发环节发现更多问题;5.QA应充分回测,多做冒烟测试,后端QA也建议增加前端验证环节,更全面发现问题;6.对代码Mock,需要增加“//TODO:上线前删除以下mock代码”,然后可以通过自动化工具检查,有这种注释的代码不应该通过Code Review;
4.时间线//故障处理流水线,时间精确到分。如:2020-07-22 10:30 系统告警,值班人员开始查看监控2020-07-29 16:20 开发A上线需求 (Merge Request地址)该Merge Request中携带一条硬编码的Mock数据,问题开始发生;2020-07-29 23:07 业务反馈Feed流自动播放及对应的播放页面,出现“这是一条测试广告”内容广告;2020-07-29 23:29 业务将该问题通知到开发和测试团队;2020-07-29 23:34 值日生B判断该问题为内容广告业务方向,由于需求在评审时逻辑是:广告由播放后台加载,认为该逻辑可能是播放后台的问题,等待QA报障(此处存在误判);2020-07-30 06:40 值日生B进一步思考,认为可能存在后端因素,查了之前的需求,发现实现过程中逻辑有变动。断定是后端上线引起。2020-07-30 07:00 值日生B抓包分析,发现后端下发恒定的广告Mock字段,开始查阅29号上线的Merge Request,发现问题代码;2020-07-30 07:32 值日生B呼叫开发A,简单沟通原因及修复措施,开发A在路上不能快速上线;2020-07-30 07:43值日生B呼叫开发C,快速沟通问题及修复措施,开发C开始修复上线;2020-07-30 07:51开发C开始发布对Hotfix进行灰度发布;2020-07-30 08:12 上线完毕,出现部分容器资源不足无法更新,和基础架构同事沟通紧急摘除,系统全部更新完毕;2020-07-30 08:30 经过半小时到一小时的线上检查,确定问题得以修复;
复制代码


1.10.变更管理

不知道读者有没有留意到一个现象,工作日经常会遇到各种系统不稳定:网络抖动、第三方服务耗时突增、服务器负载突然上升又快速回落,等等。而同样的现象在节假日尤其是较长的法定节假日反而不容易出现。


经验表明,90%以上的线上故障基本都是由变更引起的,因此,势必要高度重视变更带来的影响。变更不仅仅意味着代码的改动和发布,也包括容易被忽视的配置变更和数据变更。本章节来讨论下这三种变更的管理。

1.10.1.代码变更

发布无小事。代码变更是最常见、最频繁的变化方式,代码变更应该遵循以下原则:


1.必须要有完备的编码规范来指导日常开发。包括命名、常量、格式、异常、日志、监控,等等。另外还需要有代码格式化和代码检查插件,自动对代码进行格式化和规格检查,未通过检查的代码应该不允许提交到远程仓库,也不允许发布。


2.必须要有严格的 Code Review 规范。可以采用生成 Merge Request 来贯穿于开发、自测、QA 测试、预发布和正式发布环节。Code Review 应该采用技术手段来强制执行,没有 Merge Request 的代码变更不应该通过测试,更不应该被线上发布平台接受。


3.必须要有自动化代码分析。包括白盒分析(如 SonarQube)和黑盒自动化用例执行。当这些代码变更的扫描结果不符合预期时,代码不允许合并到主干,也不允许发布。


4.必须要有上线管理工具,对代码变更上线进行审批和流程跟踪,便于将来追溯。


5.必须要采用渐进式方式来发布代码变更。如常见的预上线(Staging)发布、金丝雀(Canary)发布、蓝绿发布等,可以执行一部分灰度流量先上线并观测指标变化(包括技术指标和业务指标),当指标符合预期时,可以考虑发布剩下的流量。需要注意,全量发布也应该渐进式、滚动式进行,因为即使功能正常工作,也可能会在发布过程中出现性能和可靠性异常。


6.必须要有回滚预案。需要注意的是,回滚尽可能不要重新拉取主干或上一个标签的代码并重新打包,一方面会影响回滚速度,另一方面可能发布异常是基础代码或打包过程出现问题导致,即使是上一个版本重新多次打包也可能无济于事,这种情况现实环境中是出现过的。因此,应该有一种基于版本镜像的回滚机制,即拉取任意版本的包镜像,直接快速部署,达到本地快速回滚的目的。

1.10.2.配置变更

配置变更一般指配置文件、各种配置后台、管理后台中技术参数的配置变更。这些变更几乎不被人重视,但往往是导致系统故障的罪魁祸首。


笔者的观点是,任何技术参数的变更,必须要有 Peer Review 机制,即使这些后台没有类似 GitHub 的原生 Pull Request 机制,也需要第三方研发人员了解该变更,评估改动风险并执行发布。

1.10.3.数据变更

研发人员不到万不得已的情况,不要轻易到存储系统中修改数据。如果确实由于 BUG 导致脏数据,需要批量变更数据,应该采用脚本(SQL 或编程实现)来控制变更的安全性,并且需要第三方研发人员执行 Peer Review。在数据变更的过程中,需要实时监控数据的变化,发现异常需要立即撤销变更操作。

发布于: 2021 年 08 月 01 日阅读数: 208
用户头像

还未添加个人签名 2018.08.18 加入

资深软件工程师,技术Leader。10多年IT从业经验,长期专注于软件开发和技术管理,喜欢思考总结与分享传播。

评论 (1 条评论)

发布
用户头像
写的好有创意。
2021 年 08 月 02 日 11:10
回复
没有更多了
微服务沉思录-可靠性