异地多活 ######## 顾名思义,异地多活架构的关键点就是异地、多活,其中异地就是指地理位置上不同的地方,类似于 “不要把鸡蛋都放在同一篮子里”;多活就是指不同地理位置上的系统都能够提供业务服务,这里的 “活” 是活动、活跃的意思。判断一个系统是否符合异地多活,需要满足两个标准: 正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。 某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。 与 “活” 对应的是字是 “备”,备是备份,正常情况下对外是不提供服务的,如果需要提供服务,则需要大量的人工干预和操作,花费大量的时间才能让 “备” 变成 “活”。 代价很高,具体表现为: 系统复杂度会发生质的变化,需要设计复杂的异地多活架构。 成本会上升,毕竟要多在一个或者多个机房搭建独立的一套业务系统。 架构模式 ======== 1. 同城异区:: 关键在于搭建高速网络将两个机房连接起来,达到近似一个本地机房的效果。 架构设计上可以将两个机房当作本地机房来设计,无须额外考虑。 距离上一般大约就是几十千米, 通过搭建高速的网络,同城异区的两个机房能够实现和同一个机房内几乎一样的网络传输速度 两个不同地理位置上的机房,但逻辑上我们可以将它们看作同一个机房 这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。 缺点: 无法应对整个城市甚至更大范围的故障 2. 跨城异地:: 关键在于数据不一致的情况下,业务不受影响或者影响很小, a. 优点: 跨城异地虽然能够有效应对极端灾难事件 b. 缺点: “距离较远” 这点并不只是一个距离数字上的变化,而是量变引起了质变,导致了跨城异地的架构复杂度大大上升。 距离增加带来的最主要问题是两个机房的网络传输速度会降低 光速真空传播大约是每秒 30 万千米, 在光纤中传输的速度大约是每秒 20 万千米, 再加上传输中的各种网络设备的处理,实际还远远达不到理论上的速度。 除了距离上的限制,中间传输各种不可控的因素也非常多。 例如,挖掘机把光纤挖断、中美海底电缆被拖船扯断、骨干网故障等, 这些线路很多是第三方维护,针对故障我们根本无能为力也无法预知。 例如,广州机房到北京机房,正常情况下 RTT 大约是 50 毫秒左右, 遇到网络波动之类的情况,RTT 可能飙升到 500 毫秒甚至 1 秒, 更不用说经常发生的线路丢包问题,那延迟可能就是几秒几十秒了。 c. 场景分析: 重点还是在 “数据” 上,即根据数据的特性来做不同的架构。 如果是强一致性要求的数据,例如银行存款余额、支付宝余额等,这类数据实际上是无法做到跨城异地多活的。 d. 适合场景: 跨城异地多活适用于: 对数据一致性要求不那么高,或者数据不怎么改变,或者即使数据丢失影响也不大的业务 例如: 用户登录(数据不一致时用户重新登录即可)、 新闻类网站(一天内的新闻数据变化较少)、 微博类网站(丢失用户发布的微博或者评论影响不大) 这些业务采用跨城异地多活,能够很好地应对极端灾难的场景。 3. 跨国异地:: 主要是面向不同地区用户提供业务,或者提供只读业务,对架构设计要求不高。 跨国异地多活的主要应用场景一般有这几种情况: a. 为不同地区用户提供服务 例如,亚马逊中国是为中国用户服务的,而亚马逊美国是为美国用户服务的, 亚马逊中国的用户如果访问美国亚马逊,是无法用亚马逊中国的账号登录美国亚马逊的。 b. 只读类业务做多活 例如,谷歌的搜索业务,由于用户搜索资料时,这些资料都已经存在于谷歌的搜索引擎上面, 搜索结果基本相同,并且对用户来说,也不需要搜索到最新的实时资料, 跨国异地的几秒钟网络延迟,对搜索结果是没有什么影响的。 异地多活设计4大技巧 =================== .. important:: 异地多活设计的理念可以总结为一句话:采用多种手段,保证绝大部分用户的核心业务异地多活! .. note:: 跨城异地多活是架构设计复杂度最高的一种 Example:: 假设我们需要做一个 “用户子系统”,这个子系统负责 “注册”“登录”“用户信息” 三个业务。 为了支持海量用户,我们设计了一个 “用户分区” 的架构, 即正常情况下用户属于某个主分区,每个分区都有其他数据的备份, 用户用邮箱或者手机号注册, 路由层拿到邮箱或者手机号后,通过 Hash 计算属于哪个中心,然后请求对应的业务中心。 技巧 1: 保证核心业务的异地多活 ------------------------------ .. note:: 如果所有功能都要支持异地多活,实际上是挺难的,有的问题甚至是无解的。那这种情况下我们应该如何考虑 “异地多活” 的架构设计呢?答案其实很简单:优先实现核心业务的异地多活架构! 注册问题:: A 中心注册了用户,数据还未同步到 B 中心,此时 A 中心宕机,为了支持注册业务多活,可以挑选 B 中心让用户去重新注册。 一个手机号只能注册一个账号,A 中心的数据没有同步过来,如 B 中心让用户注册,后来 A 中心恢复了,就会发现数据有冲突 用户信息问题:: 用户信息的修改和注册有类似的问题,即 A、B 两个中心在异常的情况下都修改了用户信息,如何处理冲突? 由于用户信息并没有账号那么关键,一种简单的处理方式是按照时间合并,即最后修改的生效。 业务逻辑上没问题,但实际操作也有一个很关键的 “坑”:怎么保证多个中心所有机器时间绝对一致? 还有一种方式是生成全局唯一递增 ID,这个方案的成本很高, 因为这个全局唯一递增 ID 的系统本身又要考虑异地多活,同样涉及数据一致性和冲突的问题。 .. note:: 对于这个模拟案例来说,“登录” 才是最核心的业务,“注册” 和 “用户信息” 虽然也是主要业务,但并不一定要实现异地多活,主要原因在于业务影响不同。对于一个日活 1000 万的业务来说,每天注册用户可能是几万,修改用户信息的可能还不到 1 万,但登录用户是 1000 万,很明显我们应该保证登录的异地多活。对于新用户来说,注册不了的影响并不明显,因为他还没有真正开始使用业务。用户信息修改也类似,暂时修改不了用户信息,对于其业务不会有很大影响。而如果有几百万用户登录不了,就相当于几百万用户无法使用业务,对业务的影响就非常大了 技巧 2: 保证核心数据最终一致性 ------------------------------ .. note:: 异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活架构设计的核心。但异地多活架构面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。 解决方法参考:: 1. 尽量减少异地多活机房的距离,搭建高速网络 成本巨大,一般只有巨头公司才能承担(远超同城异区的高速网络) 2. 尽量减少数据同步,只同步核心业务相关的数据 以前面的 “用户子系统” 为例,用户登录所产生的 token 或者 session 信息,数据量很大, 但其实并不需要同步到其他业务中心,因为这些数据丢失后重新登录就可以再次获取了。 3. 保证最终一致性,不保证实时一致性 最终一致性在具体实现时,还需要根据不同的数据特征,进行差异化的处理,以满足业务需要。 例如: a. 对 “账号” 信息来说: 如果在 A 机房新注册的用户 5 分钟内正好跑到 B 机房了, 此时 B 机房还没有这个用户的信息, 为了保证业务的正确,B 机房就需要根据路由规则到 A 机房请求数据。 b. 而对 “用户信息” 来说: 5 分钟后同步也没有问题,也不需要采取其他措施来弥补, 但还是会影响用户体验,即用户看到了旧的用户信息,可通过后面的方法补偿 技巧 3: 采用多种手段同步数据 ---------------------------- .. note:: 数据同步是异地多活架构设计的核心 存储系统本身都会有同步的功能。例如:: 1. MySQL 的主备复制 2. Redis 的 Cluster 功能 3. Elasticsearch 的集群功能 在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求:: 1. 以 MySQL 为例,MySQL 5.1 版本的复制是单线程的复制, 在网络抖动或者大量数据同步时,经常发生延迟较长的问题,短则延迟十几秒,长则可能达到十几分钟。 而且即使我们通过监控的手段知道了 MySQL 同步时延较长,也难以采取什么措施,只能干等。 2. Redis 又是另外一个问题,Redis 3.0 之前没有 Cluster 功能,只有主从复制功能 而为了设计上的简单,Redis 2.8 之前的版本,主从复制有一个比较大的隐患: 从机宕机或者和主机断开连接都需要重新连接主机,重新连接主机都会触发全量的主从复制。 主机会生成内存快照,主机依然可以对外提供服务, 但是作为读的从机,就无法提供对外服务了,如果数据量大,恢复的时间会相当长。 .. note:: 存储系统本身自带的同步功能,在某些场景下是无法满足业务需要的。尤其是异地多机房这种部署,各种各样的异常情况都可能出现,当我们只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。 采用如下几种方式同步数据:: 1. 消息队列方式 对于账号数据,由于账号只会创建,不会修改和删除(假设我们不提供删除功能), 我们可以将账号数据通过消息队列同步到其他业务中心。 2. 二次读取方式 某些情况下可能出现消息队列同步也延迟了, 用户在 A 中心注册,然后访问 B 中心的业务,此时 B 中心本地拿不到用户的账号数据。 为了解决这个问题,B 中心在读取本地数据失败时, 可以根据路由规则,再去 A 中心访问一次(二次读取,第一次读取本地,本地失败后第二次读取对端) 这样就能够解决异常情况下同步延迟的问题。 3. 存储系统同步方式 对于密码数据,由于用户改密码频率较低,而且用户不可能在 1 秒内连续改多次密码, 所以通过数据库的同步机制将数据复制到其他业务中心即可,用户信息数据和密码类似。 4. 回源读取方式 对于登录的 session 数据,由于数据量很大,我们可以不同步数据; 但当用户在 A 中心登录后,然后又在 B 中心登录(如动车跨省时) B 中心拿到用户上传的 session id 后,根据路由判断 session 属于 A 中心,直接去 A 中心请求 session 数据即可; 反之亦然,A 中心也可以到 B 中心去获取 session 数据。 5. 重新生成数据方式 对于 “回源读取” 场景,如果异常情况下,A 中心宕机了, B 中心请求 session 数据失败,此时就只能登录失败, 让用户重新在 B 中心登录,生成新的 session 数据。 .. image:: https://img.zhaoweiguo.com/knowledge/images/architectures/availabilitys/different-live1.png 技巧 4: 只保证绝大部分用户的异地多活 ------------------------------------ .. note:: 某些场景下我们无法保证 100% 的业务可用性,总是会有一定的损失。我们要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的 0.01% 的用户的可用性,做一个完美方案,结果却发现 99.99% 的用户都保证不了了。 针对银行转账这个业务:: 无法做到 “实时转账” 的异地多活,但可以通过特殊的业务手段让转账业务也能实现异地多活。 例如,转账业务除了 “实时转账” 外,还提供 “转账申请” 业务, 即小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求, 然后后台异步发起真正的转账操作, 如果此时北京业务中心不可用,转账请求就可以继续等待重试; 假设等待 2 个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。 小明再登录上来就会看到转账申请失败,原因是 “余额不足”。 采取一些措施进行安抚或者补偿,例如:: 1. 挂公告 说明现在有问题和基本的问题原因, 如果不明确原因或者不方便说出原因,可以发布 “技术哥哥正在紧急处理” 这类比较轻松和有趣的公告。 2. 事后对用户进行补偿 例如,送一些业务上可用的代金券、小礼包等,减少用户的抱怨。 3. 补充体验 对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。 以银行 “转账申请” 为例,为了让用户不用确认转账申请是否成功, 我们可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果, 这样用户就不用时不时地登录系统来确认转账是否成功了。 异地多活设计 4 步走 =================== 第 1 步: 业务分级 ----------------- .. note:: 按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。 常见的分级标准有下面几种:: 1. 访问量大的业务 以用户管理系统为例,业务包括登录、注册、用户信息管理,其中登录的访问量肯定是最大的。 2. 核心业务 a. 以 QQ 为例,QQ 的主场景是聊天, QQ 空间虽然也是重要业务,但和聊天相比,重要性就会低一些 b. 用户管理系统 “登录” 业务符合 “访问量大的业务” 和 “核心业务” 这两条标准,因此登录业务是核心业务。 3. 产生大量收入的业务 同样以 QQ 为例,聊天可能很难为腾讯带来收益,因为聊天没法插入广告; 而 QQ 空间反而可能带来更多收益,因为 QQ 空间可以插入很多广告, 因此如果从收入的角度来看,QQ 空间做异地多活的优先级反而高于 QQ 聊天了。 第 2 步: 数据分类 ----------------- .. note:: 挑选出核心业务后,需要对核心业务相关的数据进一步分析,目的在于识别所有的数据及数据特征,这些数据特征会影响后面的方案设计。 常见的数据特征分析维度有:: 1. 数据量 这里的数据量包括总的数据量和新增、修改、删除的量。 对异地多活架构来说,新增、修改、删除的数据就是可能要同步的数据, 数据量越大,同步延迟的几率越高,同步方案需要考虑相应的解决方案。 2. 唯一性 唯一性指数据是否要求多个异地机房产生的同类数据必须保证唯一。 例如用户 ID,如果两个机房的两个不同用户注册后生成了一样的用户 ID,这样业务上就出错了。 数据的唯一性影响业务的多活设计,如果数据不需要唯一,那就说明两个地方都产生同类数据是可能的; 如果数据要求必须唯一,要么只能一个中心点产生数据,要么需要设计一个数据唯一生成的算法。 3. 实时性 实时性指如果在 A 机房修改了数据,要求多长时间必须同步到 B 机房, 实时性要求越高,对同步的要求越高,方案越复杂。 4. 可丢失性 可丢失性指数据是否可以丢失。 例如,写入 A 机房的数据还没有同步到 B 机房,此时 A 机房机器宕机会导致数据丢失, 那这部分丢失的数据是否对业务会产生重大影响。 例如: 登录过程中产生的 session 数据就是可丢失的,因为用户只要重新登录就可以生成新的 session; 而用户 ID 数据是不可丢失的,丢失后用户就会失去所有和用户 ID 相关的数据,例如用户的好友、用户的钱等 5. 可恢复性 可恢复性指数据丢失后,是否可以通过某种手段进行恢复, 如果数据可以恢复,至少说明对业务的影响不会那么大,这样可以相应地降低异地多活架构设计的复杂度。 例如: 用户的微博丢失后,用户重新发一篇一模一样的微博,这个就是可恢复的; 或者用户密码丢失,用户可以通过找回密码来重新设置一个新密码,这也算是可以恢复的; 而用户账号如果丢失,用户无法登录系统,系统也无法通过其他途径来恢复这个账号,这就是不可恢复的数据。 .. image:: https://img.zhaoweiguo.com/knowledge/images/architectures/availabilitys/different-live2.png 第 3 步: 数据同步 ----------------- .. note:: 确定数据的特点后,我们可以根据不同的数据设计不同的同步方案。 常见的数据同步方案有:: 1. 存储系统同步 这是最常用也是最简单的同步方式。 例如,使用 MySQL 的数据主从数据同步、主主数据同步。 优点: 使用简单,因为几乎主流的存储系统都会有自己的同步方案; 缺点: 这类同步方案都是通用的,无法针对业务数据特点做定制化的控制。 例如,无论需要同步的数据量有多大,MySQL 都只有一个同步通道。 因为要保证事务性,一旦数据量比较大,或者网络有延迟,则同步延迟就会比较严重。 2. 消息队列同步 采用独立消息队列进行数据同步 常见的消息队列有 Kafka、ActiveMQ、RocketMQ 等。 消息队列同步适合无事务性或者无时序性要求的数据。 例如,用户账号,两个用户先后注册了账号 A 和 B, 如果同步时先把 B 同步到异地机房,再同步 A 到异地机房,业务上是没有问题的。 而如果是用户密码,用户先改了密码为 m,然后改了密码为 n, 顺序必须一致。 因此 对于新注册的用户账号,我们可以采用消息队列同步了; 而对于用户密码,就不能采用消息队列同步了。 3. 重复生成 数据不同步到异地机房,每个机房都可以生成数据,这个方案适合于可以重复生成的数据。 例如,登录产生的 cookie、session 数据、缓存数据等。 .. image:: https://img.zhaoweiguo.com/knowledge/images/architectures/availabilitys/different-live3.png 第 4 步: 异常处理 ----------------- .. note:: 无论数据同步方案如何设计,一旦出现极端异常的情况,总是会有部分数据出现异常的。例如,同步延迟、数据丢失、数据不一致等。异常处理就是假设在出现这些问题时,系统将采取什么措施来应对。 异常处理主要有以下几个目的:: 1. 问题发生时,避免少量数据异常导致整体业务不可用 2. 问题恢复后,将异常的数据进行修正 3. 对用户进行安抚,弥补用户损失 常见的异常处理措施有这几类:: 1. 多通道同步 2. 同步和访问结合 3. 日志记录 4. 用户补偿 1. 多通道同步 ^^^^^^^^^^^^^ .. note:: 多通道同步的含义是采取多种方式来进行数据同步,其中某条通道故障的情况下,系统可以通过其他方式来进行同步,这种方式可以应对同步通道处故障的情况。 .. figure:: https://img.zhaoweiguo.com/knowledge/images/architectures/availabilitys/different-live3.png 考虑异常情况下,消息队列同步通道可能中断,也可能延迟很严重;为了保证新注册账号能够快速同步到异地机房,我们再增加一种 MySQL 同步这种方式作为备份。 多通道同步设计的方案关键点有:: a. 一般情况下,采取两通道即可,采取更多通道理论上能够降低风险,但付出的成本也会增加很多。 b. 数据库同步通道和消息队列同步通道不能采用相同的网络连接 否则一旦网络故障,两个通道都同时故障; 可以一个走公网连接,一个走内网连接。 c. 需要数据是可以重复覆盖的 即无论哪个通道先到哪个通道后到,最终结果是一样的。 例如,新建账号数据就符合这个标准,而密码数据则不符合这个标准。 2. 同步和访问结合 ^^^^^^^^^^^^^^^^^ .. note:: 这里的访问指异地机房通过系统的接口来进行数据访问。 同步和访问结合方案的设计关键点有:: a. 接口访问通道和数据库同步通道不能采用相同的网络连接 不能让数据库同步和接口访问都走同一条网络通道, 可以采用接口访问走公网连接,数据库同步走内网连接这种方式。 b. 数据有路由规则 可以根据数据来推断应该访问哪个机房的接口来读取数据。 例如,有 3 个机房 A、B、C,B 机房拿到一个不属于 B 机房的数据后, 需要根据路由规则判断是访问 A 机房接口,还是访问 C 机房接口。 c. 由于有同步通道,优先读取本地数据 本地数据无法读取到再通过接口去访问, 这样可以大大降低跨机房的异地接口访问数量,适合于实时性要求非常高的数据。 3. 日志记录 ^^^^^^^^^^^ .. note:: 日志记录主要用于用户故障恢复后对数据进行恢复,其主要方式是每个关键操作前后都记录相关一条日志,然后将日志保存在一个独立的地方,当故障恢复后,拿出日志跟数据进行对比,对数据进行修复。 为了应对不同级别的故障,日志保存的要求也不一样,常见的日志保存方式有:: a. 服务器上保存日志,数据库中保存数据 这种方式可以应对单台数据库服务器故障或者宕机的情况。 b. 本地独立系统保存日志 这种方式可以应对某业务服务器和数据库同时宕机的情况。 例如,服务器和数据库部署在同一个机架,或者同一个电源线路上,就会出现服务器和数据库同时宕机的情况。 c. 日志异地保存 这种方式可以应对机房宕机的情况。 .. note:: 应对的故障越严重,方案本身的复杂度和成本就会越高,实际选择时需要综合考虑成本和收益情况。 4. 用户补偿 ^^^^^^^^^^^ .. note:: 无论采用什么样的异常处理措施,都只能最大限度地降低受到影响的范围和程度,无法完全做到没有任何影响。无论多么完美的方案,故障的场景下总是可能有一小部分用户业务上出问题,系统无法弥补这部分用户的损失。但我们可以采用人工的方式对用户进行补偿,弥补用户损失,培养用户的忠诚度。 .. note:: 系统的方案是为了保证 99.99% 的用户在故障的场景下业务不受影响,人工的补偿是为了弥补 0.01% 的用户的损失。 常见的补偿措施有:: 送用户代金券、礼包、礼品、红包等, 有时为了赢得用户口碑,付出的成本可能还会比较大,但综合最终的收益来看还是很值得的。 实例-暴雪《炉石传说》2017 年回档故障:: 只要在 2017 年 1 月 18 日 18 点之前登录过国服《炉石传说》的玩家, 均可获得与 25 卡牌包等值的补偿,具体如下: 1000 游戏金币; 15 个卡牌包:经典卡牌包 x5、上古之神的低语卡牌包 x5、龙争虎斗加基森卡牌包 x5。 思考 ==== 关于阿里Oceanbase:: 支付宝为了底层支持异地多活,自己写了 Oceanbase,Oceanbase 写了 7~8 年了还没有完全代替 MySQL 余额和库存一般不做双写,目前 Oceanbase 通过 paxos 算法支持多机房写入,但实际性能我不太了解 文中的例子是多地都可以同时写,oceanbase 底层基于 paxos 算法, 从业务的角度看起来是多地都可以写,但本质上是通过一致性算法避免同时写, paxos 同时写会出现冲突,冲突就要重新发起写操作 oceanbase 的强一致分布式数据库可以使业务不需要考虑持久层的跨地域数据同步问题。 但应该付出的代价是单个请求的 rt 会变大,可用性也有降低, 所以对 rt 要求非常高的业务可能不会选择,其实还是对业务有影响的。 如果代价可以承受,业务端还要解决缓存的一致性问题, 流量切到其它可用区的压力是不是承受的住。可能还是需要部分业务降级。 所以分布式数据库不能完全做到业务无感知的异地多活 OceanBase 的架构是 “同城双机房 + 异地单机房” 共 3 个机房 5 个节点这种架构, 异地机房其实不能用来做完整的业务访问,因此 OceanBase 其实做不了异地多活的底层存储,而是同城双活。 异地多活 client 设计:: 每个机房都对外提供服务,都有不同出口 ip, 由 DNS 等负载均衡设备切换 (适应 web),或者端自己切换 (适应 app) 账户登陆这一块做多机房回源读取技巧:: 密码错误,让用户输入验证 这样做至少有这两个功能:1、人机校验;2、留充足时间给多地机房数据确认; 涉及到取舍时就需要参考公司的核心目标和愿景了:: 如果是和核心目标相关的优先级就高,和核心目标无关或关联度不大的优先级就低。 例: 2020年的滴滴 在以扩规模增利润作为核心目标时必须会优先处理司机端和线路规划相关的业务, 而对于增加成本降低利润的用户端和客服系统优先级肯定会比较低; 当事故频发引发公司战略转向到安全第一时, 原先一直没有得以优化的客服系统、警方查询对接系统优先级将会排到第一位。 在异地多活时排定优先级时也是如此了,牺牲什么保证什么,都是和公司最为关注的内容和目标挂钩的。 一个秒杀份额只有十万,要做跨城容灾,缓存如何设计:: 一般都会用份额分配,例如每个城市分配 5 万, 差别在于本地份额用完够怎么处理: 1. 一种策略是本地用完了这个城市就没有了; 2. 一种是用完了就到另外一个城市请求再分一些!