微服务架构-系统可靠性保障

Posted 天秤座的架构师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务架构-系统可靠性保障相关的知识,希望对你有一定的参考价值。

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.主动限流与被动限流

如何保障微服务架构下的数据一致性

1、微服务架构的数据一致性问题

以电商平台为例,当用户下单并支付后,系统需要修改订单的状态并且增加用户积分。由于系统采用的是微服务架构,分离出了支付服务、订单服务和积分服务,每个服务都有独立数据库做数据存储。当用户支付成功后,无论是修改订单状态失败还是增加积分失败,都会造成数据的不一致。

为了解决例子中的数据一致性问题,一个最直接的办法就是考虑数据的强一致性。那么如何保证数据的强一致性呢?我们从关系型数据库的 ACID 理论说起。

关系型数据库具有解决复杂事务场景的能力,关系型数据库的事务满足 ACID 的特性。

 

  • Atomicity:原子性(要么都做,要么都不做)

  • Consistency:一致性(数据库只有一个状态,不存在未确定状态)

  • Isolation:隔离性(事务之间互不干扰)

  • Durability: 永久性(事务一旦提交,数据库记录永久不变)

 

具有 ACID 特性的数据库支持数据的强一致性,保证了数据本身不会出现不一致。

然而微服务架构下,每个微服务都有自己的数据库,导致微服务架构的系统不能简单地满足 ACID,我们就需要寻找微服务架构下的数据一致性解决方案。

微服务架构的系统本身是一种分布式系统,而本文讨论的问题其实也就是分布式事务之数据一致性的问题,我们来聊聊分布式系统的 CAP 理论和 BASE 理论。

CAP 是指在一个分布式系统下, 包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且三者不可得兼。

 

  • C:Consistency,一致性,所有数据变动都是同步的。

  • A:Availability,可用性,即在可以接受的时间范围内正确地响应用户请求。

  • P:Partition tolerance,分区容错性,即某节点或网络分区故障时,系统仍能够提供满足一致性和可用性的服务。

 

关系型数据库 单节点 保证了数据强一致性(C)和可用性(A),但是却无法保证分区容错性(P)。

然而在分布式系统下,为了保证模块的分区容错性(P),只能在数据强一致性(C)和可用性(A)之间做平衡。具体表现为在一定时间内,可能模块之间数据是不一致的,但是通过自动或手动补偿后能够达到最终的一致。

BASE 理论主要是解决 CAP 理论中分布式系统的可用性和一致性不可兼得的问题。BASE 理论包含以下三个要素:

 

  • BA:Basically Available,基本可用。

  • S:Soft State,软状态,状态可以有一段时间不同步。

  • E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致。

 

BASE 模型与 ACID 不同,满足 CAP 理论,通过 牺牲强一致性来保证系统可用性。由于牺牲了强一致性,系统在处理请求的过程中,数据可以存在短时的不一致。

系统在处理业务时,记录每一步的临时状态。当出现异常时,根据状态判断是否继续处理请求或者退回原始状态,从而达到数据的最终一致。

例如,在上面的案例中,支付成功,订单也成功,但增加积分失败,此时,不应回滚支付和订单,而应通过一些 补偿方法 来让积分得以正确地增加。后面会讲到具体的实现方法。

在分享我们的分布式事务实践方案之前,先看看早期解决分布式事务问题的二阶段提交协议。

2、二阶段提交协议

 

X/Open DTP(Distributed Transaction Process)是一个分布式事务模型,此模型主要使用二阶段提交(2PC,Two-Phase-Commit)来保证分布式事务的完整性。在这个模型里面,有三个角色:

  • AP:Application,应用程序,业务层。

  • RM:Resource Manager,资源管理器,关系型数据库或支持 XA 接口(XA 规范是 X/Open 组织定义的分布式事务规范)的组件。

  • TM: Transaction Manager ,事务管理器,负责各个 RM 的提交和回滚。

当应用程序(AP)调用了事务管理器(TM)的提交方法时,事务的提交分为两个阶段实行。

2.1、第一阶段(准备阶段)

 

TM 通知所有参与事务的各个 RM,给每个 RM 发送 prepare 消息。

RM 接收到消息后进入准备阶段后,要么直接返回失败,要么创建并执行本地事务,写本地事务日志(redo 和 undo 日志),但是 不提交(此处只保留最后一步耗时最少的提交操作给第二阶段执行)。

2.2、第二阶段(提交 / 回滚阶段)

 

TM 收到 RM 准备阶段的失败消息或者获取 RM 返回消息超时,则直接给 RM 发送回滚(rollback)消息,否则发送提交(commit)消息。

RM 根据 TM 的指令执行提交或者回滚,执行完成后释放所有事务处理过程中使用的锁(最后阶段释放锁)。

 

2.3、二阶段提交的利弊

 

优点

2PC 提供了一套完整的分布式事务的解决方案,遵循事务严格的 ACID 特性。

缺点

  • TM 通过 XA 接口与各个 RM 之间进行数据交互,从第一阶段的准备阶段,业务所涉及的数据就被锁定,并且锁定跨越整个提交流程。在高并发和涉及业务模块较多的情况下对数据库的性能影响较大。

  • 二阶段是 反可伸缩模式 的,业务规模越大,涉及模块越多,局限性越大,系统可伸缩性越差。

  • 在技术栈比较杂的分布式应用中,存储组件有很多 不支持 XA 协议。

二阶段的诸多弊端,导致分布式系统下无法直接使用此方案来解决数据一致性问题,但它提供了解决分布式系统下数据一致性问题的思路。。

下面就通过案例来分享我们是如何保证微服务架构的数据一致性的。

3、可靠消息最终一致性

可靠消息最终一致性方案本质上是 利用 MQ 组件实现的二阶段提交。此方案涉及 3 个模块:

 

  • 上游应用,执行业务并发送 MQ 消息。

  • 可靠消息服务和 MQ 消息组件,协调上下游消息的传递,并确保上下游数据的一致性。

  • 下游应用,监听 MQ 的消息并执行自身业务。

 
3.1、上游应用执行业务并发送 MQ 消息(第一阶段)

上游应用将本地业务执行和消息发送绑定在同一个本地事务中,保证要么本地操作成功并发送 MQ 消息,要么两步操作都失败并回滚。

上游应用和可靠消息之间的业务交互图如下:



  1. 上游应用发送待确认消息到可靠消息系统

  2. 可靠消息系统保存待确认消息并返回

  3. 上游应用执行本地业务

  4. 上游应用通知可靠消息系统确认业务已执行并发送消息。

  5. 可靠消息系统修改消息状态为发送状态并将消息投递到 MQ 中间件。

以上每一步都可能出现失败情况,分析一下这 5 步出现异常后上游业务和消息发送是否一致:

 
上游应用执行完成,下游应用尚未执行或执行失败时,此事务即处于 BASE 理论的 Soft State 状态。
 
3.2、下游应用监听 MQ 消息并执行业务(第二阶段)

下游应用监听 MQ 消息并执行业务,并且将消息的消费结果通知可靠消息服务。

可靠消息的状态需要和下游应用的业务执行保持一致,可靠消息状态不是已完成时,确保下游应用未执行,可靠消息状态是已完成时,确保下游应用已执行。

下游应用和可靠消息服务之间的交互图如下:

  1. 下游应用监听 MQ 消息组件并获取消息

  2. 下游应用根据 MQ 消息体信息处理本地业务

  3. 下游应用向 MQ 组件自动发送 ACK 确认消息被消费

  4. 下游应用通知可靠消息系统消息被成功消费,可靠消息将该消息状态更改为已完成。

以上每一步都可能出现失败情况,分析一下这 4 步出现异常后下游业务和消息状态是否一致:

通过分析以上两个阶段可能失败的情况,为了确保上下游数据的最终一致性,在可靠消息系统中,需要开发 消息状态确认 和 消息重发 两个功能以实现 BASE 理论的 Eventually Consistent特性。
 
3.3、消息状态确认

可靠消息服务定时监听消息的状态,如果存在状态为待确认并且超时的消息,则表示上游应用和可靠消息交互中的步骤 4 或者 5 出现异常。

可靠消息则携带消息体内的信息向上游应用发起请求查询该业务是否已执行。上游应用提供一个可查询接口供可靠消息追溯业务执行状态,如果业务执行成功则更改消息状态为已发送,否则删除此消息确保数据一致。具体流程如下:

  1. 可靠消息查询超时的待确认状态的消息

  2. 向上游应用查询业务执行的情况

  3. 业务未执行,则删除该消息,保证业务和可靠消息服务的一致性。业务已执行,则修改消息状态为已发送,并发送消息到 MQ 组件。

3.4、消息重发

消息已发送则表示上游应用已经执行,接下来则确保下游应用也能正常执行。

可靠消息服务发现可靠消息服务中存在消息状态为已发送并且超时的消息,则表示可靠消息服务和下游应用中存在异常的步骤,无论哪个步骤出现异常,可靠消息服务都将此消息重新投递到 MQ 组件中供下游应用监听。

下游应用监听到此消息后,在保证幂等性的情况下重新执行业务并通知可靠消息服务此消息已经成功消费,最终确保上游应用、下游应用的数据最终一致性。具体流程如下:

  1. 可靠消息服务定时查询状态为已发送并超时的消息

  2. 可靠消息将消息重新投递到 MQ 组件中

  3. 下游应用监听消息,在满足幂等性的条件下,重新执行业务。

  4. 下游应用通知可靠消息服务该消息已经成功消费。

通过消息状态确认和消息重发两个功能,可以确保上游应用、可靠消息服务和下游应用数据的最终一致性。

当然在实际接入过程中,需要引入 人工干预 功能。比如引入重发次数限制,超过重发次数限制的将消息修改为死亡消息,等待人工干预。

代入开篇案例,通过可靠消息最终一致性方案,第一阶段,订单状态更改之前,订单服务向可靠消息服务请求保存待确认消息。可靠消息服务保存消息并返回。

订单服务接收到返回信息后执行本地业务并通知可靠消息服务业务已执行。消息服务更改消息状态并将消息投递到 MQ 中间件。

第二阶段,积分系统监听到 MQ 消息,查看积分是否已增加,如果没有增加则修改积分,然后请求可靠消息服务。可靠消息服务接收到积分系统的请求,将消息状态更改为已完成。

到这里,已经介绍完如何通过可靠消息服务来保证数据的一致性。但由于引入了可靠消息服务和消息队列,带来了一定的 复杂性,所以,它更 适用于跨平台技术栈不统一的场景。

下面再来介绍在技术栈统一的情况下,如何通过 TCC 来解决数据一致的方法。

4、TCC(Try-Confirm-Cancel)

TCC 方案是二阶段提交的 另一种实现方式,它涉及 3 个模块,主业务、从业务 和 活动管理器(协作者)。

下面这张图是互联网上关于 TCC 比较经典的图示:

 

第一阶段:主业务服务分别调用所有从业务服务的 try 操作,并在活动管理器中记录所有从业务服务。当所有从业务服务 try 成功或者某个从业务服务 try 失败时,进入第二阶段。

第二阶段:活动管理器根据第一阶段从业务服务的 try 结果来执行 confirm 或 cancel 操作。如果第一阶段所有从业务服务都 try 成功,则协作者调用所有从业务服务的 confirm 操作,否则,调用所有从业务服务的 cancel 操作。

在第二阶段中,confirm 和 cancel 同样存在失败情况,所以需要对这两种情况做 异常处理 以保证数据一致性。

  1. Confirm 失败:则回滚所有 confirm 操作并执行 cancel 操作。

  2. Cancel 失败:从业务服务需要提供自动 cancel 机制,以保证 cancel 成功。

目前有很多基于 RPC 的 TCC 框架,但是不适用于微服务架构下基于 HTTP 协议的交互模式。我们这次只讨论基于 HTTP 协议的 TCC 实现。具体的实现流程如下:


 

  1. 主业务服务调用从业务服务的 try 操作,并获取 confirm/cancel 接口和超时时间。

  2. 如果从业务都 try 成功,主业务服务执行本地业务,并将获取的 confirm/cancel 接口发送给活动管理器,活动管理器会顺序调用从业务 1 和从业务 2 的 confirm 接口并记录请求状态,如果请求成功,则通知主业务服务提交本地事务。如果 confirm 部分失败,则活动管理器会顺序调用从业务 1 和从业务 2 的 cancel 接口来取消 try 的操作。

  3. 如果从业务部分或全部 try 失败,则主业务直接回滚并结束,而 try 成功的从业务服务则通过定时任务来处理处于 try 完成但超时的数据,将这些数据做回滚处理保证主业务服务和从业务服务的数据一致。

 

代入开篇提到的案例,通过 TCC 方案,订单服务在订单状态修改之前执行预增积分操作(try),并从积分服务获取 confirm/cancel 预增积分的请求地址。

如果预增积分(try)成功,则订单服务更改订单状态并通知活动管理器,活动管理器请求积分模块的 confirm 接口来增加积分。

如果预增积分(try)失败,则订单服务业务回滚。积分服务通过定时任务删除预增积分(try)超时的数据。

另外如果活动管理器调用积分服务的 confirm 接口失败,则活动管理器调用积分服务 cancel 接口来取消预增积分,从而,保证订单和积分数据的最终一致性。

通过上面的对可靠消息服务和 TCC 方案的描述,我们 解决了技术栈一致和不一致的两种情况下的数据一致性问题。

但是,通常在这些核心业务上有 很多附加业务,比如当用户支付完成后,需要通过短信通知用户支付成功。

这一类业务的成功或者失败不会影响核心业务,甚至很多大型互联网平台在并高并发的情况下会主动关闭这一类业务以保证核心业务的顺利执行。那么怎么处理这类情况呢,我们来看看最大努力通知方案。

5、最大努力通知

最大努力通知方案涉及三个模块:

 

  • 上游应用,发消息到 MQ 队列。

  • 下游应用(例如短信服务、邮件服务),接受请求,并返回通知结果。

  • 最大努力通知服务,监听消息队列,将消息存储到数据库中,并按照通知规则调用下游应用的发送通知接口。

 

具体流程如下:


 

  1. 上游应用发送 MQ 消息到 MQ 组件内,消息内包含通知规则和通知地址

  2. 最大努力通知服务监听到 MQ 内的消息,解析通知规则并放入延时队列等待触发通知

  3. 最大努力通知服务调用下游的通知地址,如果调用成功,则该消息标记为通知成功,如果失败则在满足通知规则(例如 5 分钟发一次,共发送 10 次)的情况下重新放入延时队列等待下次触发。

 

最大努力通知服务表示在 不影响主业务 的情况下,尽可能地确保数据的一致性。它需要开发人员根据业务来指定通知规则,在满足通知规则的前提下,尽可能的确保数据的一致,以尽到最大努力的目的。

根据不同的业务可以定制不同的通知规则,比如通知支付结果等相对严谨的业务,可以将通知频率设置高一些,通知时间长一些,比如隔 5 分钟通知一次,持续时间 1 小时。

如果不重要的业务,比如通知用户积分增加,则可以将通知频率设置低一些,时间短一些,比如 10 分钟通知一次,持续 30 分钟。

代入上面提到的支付成功短信通知用户的案例,通过最大努力通知方案,当支付成功后,将消息发送到 MQ 中间件,在消息中,定义发送规则为 5 分钟一次,最大发送数为 10 次。

最大努力通知服务监听 MQ 消息并根据规则调用消息通知服务(短信服务)的消息发送接口,并记录每次调用的日志信息。在通知成功或者已通知 10 次时,停止通知。

6、总   结

上面通过案例详细介绍了我们解决微服务之间数据不一致问题的三种方案,下面通过一张简单的对比图,为大家选择合适的解决方案提供简单依据。

以上是关于微服务架构-系统可靠性保障的主要内容,如果未能解决你的问题,请参考以下文章

微服务架构的四大金刚利器

微服务架构 Sentinel 的服务限流及熔断

分布式系统之中心化复制集管理

链家网房源平台微服务架构实践

java微服务架构的分布式事务解决方案

微服务架构的分布式事务解决方案