我们继续看一下多活系统
我们前面介绍CDN DNS
就是为了接下来讲多活系统的一个铺垫
我们先看一下多活系统在整个进行中所谓的异地多活
大家经常也听过这个名词
我们在做的时候其实分为几个步骤
我们先概览性的看一下它有哪些知识点
第一就是说业务分级
我们一般来说是按照一定的标准
将业务分等级
挑选出最核心的业务和最核心的场景
因为只有核心业务和核心场景去设计异地多活
那么你才能降低整体方案的复杂度和实现成本
你不可能说你做异地多活
全部东西全部要多活
都要机房同时写 同时读
其实设计难度是非常困难的 对吧
所以说我们要根据像它的访问量级
因为你访问量越大
意味着我去做多活
它去分摊流量
那么带来的收益会越大
那么核心场景就不说了
比方说我们B站可能是核心是做播放的
播放就是我们核心场景
那么它的收益是比较大的
另外你像淘宝这种做收入型的一些业务
对钱比较敏感的业务
那么它是做多活是收益非常大的
你避免说考虑什么全部业务全部考虑多活
这样的话
你一开始的整体方案会非常复杂
所以我们一般是分阶段分场景去推进
第二就是说数据分类
当你挑选出你的核心业务场景以后
我们要去考虑对核心业务的一些数据进行分析
就看一下它原始数据的
这些特征分布是什么样的
它也是会影响到后面的一些方案设计
包括它的数据量是否是唯一的
对实时性要求高不高
可丢失性怎么样 能不能恢复
第三个就是我们考虑到你要做多活
你一定要考虑到你的数据分布在多个机房
机房之间的数据同步就变得非常关键
我们要根据它的不同的一些数据的特点来做方案
比方说我们可能直接使用MySQL的复制
MySQL Replication
我们可能会基于订阅MySQL的binlog
然后再去基于消息队列再来做复制
这都是很常见的一些方案
第四块其实是异常处理
因为你无论做多么完美的方案
你总可能会出现一些异常的情况
比方说我们在做数据同步的时候
可能会两个机房的网络就是不通
这个时候就会出现
数部分数据出现复制异常
面对这种同步异常数据丢失
甚至是数据不一致
我们应该去做一些假设
就大胆的去做假设
然后我们应该怎么去做一些方案去弥补这些问题
比方说我们遇到公网故障的时候
我们可以多通道对吧
有多网络出口
多通道去做数据同步
另外我们可能甚至可以考虑
我在同步去访问机房的RPC的时候
无法访问的时候是不是可以考虑降级
走异步的方式到我们的后台去执行
还有当我的业务真数据真的
某个机房彻底故障无法恢复的时候
有没有办法从日志记录或者其他的一些
补偿的手段去让我的系统可以继续去运行
这都是一些可能需要考虑的
那么多活其实
我们刚才讲了它不是整个体系的业务多活
而是要分成不同维度不同重要性的一个多活
就像我刚刚说的
我们的业务主要是以观看体验为主
那么淘宝它交易为主
可能是也买家维度去做的一些多活
像我们的这种观看类的业务
第一大前提就是浏览观看上的一些多活
我们也是参考了很多行业的一些经验
我们将资源分为三大类
我们后面也会看一下像蚂蚁饿了么
像苏宁等等
包括像微信他们的一些方案是什么
其实他们第一步都会把他们的资源进行分类
分成了几大类 我们先看一下
第一个叫Global资源
Global资源叫全局资源
也就是指你多个机房
比方说我们多个Zone去共享访问的资源
最典型的就是什么
是不是账号（业务）
或者说支付这种业务
它可能就是需要核心机房
甚至像库存对吧
库存你怎么去分
你肯定是集中式的去Global资源
并且这些资源可能会被其他的一些机房去访问
Global层面来说它是一个单写的核心机房
也就说它只能在一个地方去写
但是因为你虽然是单写
比方说我们注册账号
肯定是单写 你不可能去分到不同的机房
因为这样的话数据就冲突了
但是它可能会去多读
像我们的稿件
我们的稿件的话 你上传一个稿件
它可能是一个单写 写到一个机房
但是我是不是可以利用数据复制
把它写的好多个机房去
那么同时提供读
实现一个最终一致性的方案
那么这样的话其实Global资源是非常合适的
单写多读可能是合适的
第二种资源叫Multi Zone
Multi Zone指的是它可以按照一定的维度去进行切片
每一个Zone每一个机房它拥有是部分的数据
将一个大的数据分成好多个Shard
每个人负责一小片
我们最常见的一个sharding方法是什么
比方说按照用户维度去shard
比方说阿里的话可能是以买家的维度
用户A可能在Zone A 用户B可能在Zone B
那么这种好处是什么
为什么叫Multi Zone
它是多写多读不同维度的用户数据
它可以同时读写
那么再利用数据复制
比方双向复制
那么最终保证每个机房它都拥有全量的数据
那么你可以进行一些故障切换
当我某个机房不可用的时候
另外就是说Single Zone就是这个业务既不是多活的
然后它也不是Global需要被其他机房全部集体访问的
可能单机部署就足够了
我们当时在设计多活
就我们内部做视频网站设计多活的时候
核心围绕的是
我们要考虑像我们的PC APP的首页可观看
视频详情页可打开
账号可登录鉴权来开展
我可以登录鉴权
我可以打开首页
视频详细也可以打开可以播放
所以我们最适合的场景其实是单写多读
对于观看类的场景就是单写多读
因为相比读来说
我们的投稿行为或者说上传视频行为
它是一个频次比读少得多的场景
所以它特别适合Global资源
那么对于社区内类一些业务
比方说评论弹幕
我们可能会采用Multi Zone的一个思路
可以多读多写
这样的话它整个的scale out
它整个横向扩展的能力会变得非常强
我们再来看一下蚂蚁的
它的一个单元化的架构设计
单元化名词听过很多次
简单来说
就是让一个业务闭环在一个单元内去执行
尽量减少它的跨机房的一个调用
所以我们叫单元化的设计 单元化架构
那么它在这个图里面其实是一样的
跟我们前面讲的这种分为多种资源的
这种做法其实是一致
它也是分为一个叫GZone 叫Global Zone
那么Global Zone我们看一下在图里面
比方城市1里面有GZone A GZone B
有这两个全局的业务
那么它部署的是什么
叫不可拆分的数据和服务
比方我刚刚举的例子账号
账号是非常典型的
只能一个地方去单写
因为手机号邮箱这种东西很容易冲突的
你不可能多个机房同时去提供这个业务
这种叫不可分割的数据和服务
那么这些数据或者服务它会被谁依赖
被RZone依赖
我们待会讲RZone
我们先看GZone的话
你可以理解为
全局只有一组
数据仅有一份
那么它的可用性怎么保证
我们知道数据库
比方说我们用一个KV
它可能是底层基于Raft做了三个副本的
那么其实你挂任何一个节点可能还有两个副本
其实可用性是还不错的
除非你说它整个城市整个机房都故障掉了
甚至有些有些为了避免单个园区故障
它可能是在同城里面
一个园区可能是有两栋楼
其中一栋楼有一个副本
另外一栋楼有一个副本
我们在园区里面是两个副本
那么它在同城
另外一个稍微远一点的地方
然后再做一个副本
这样的话其实也是可以做成三副本
那么一个园区故障的话
其实你最终还有一份数据
所以说也是尽可能保证我们的数据安全
这种叫Global Zone
因为Global Zone它是单写
你不可能去跨到多个城市同时写
所以这个是第一个叫Global Zone
那么什么叫RZone
它叫Region Zone
RZone实际上
就是可以把用户的很多数据它拆成了很多片
现在我们看到这个图里面RZone注意
这里有RZone 1 2 3 4 5
我们把它拆成了五个分片
再看一下它底层这个地方
因为是蚂蚁分享的一个架构
他们其实底层用的OceanBase
那么OceanBase我们看一下
它这里是有OB1
用户因为有分成五片
所以我数据其实也是有五份的
那么我们看到这个城市1的两个副本
一共有OB1 2 3 4 5其实是全量数据
我们看城市1其实它是有两个副本的
这样的话部署上它其实分为五个分片
就是OB1 2 3 4 5
并且它是有几个副本
一共有五个副本
我们看一下城市1有两个副本
城市2有两个副本
城市3是有一个副本的
那么底层这五个副本其实是通过先写主副本
是这个地方比较深色的
标了一个橙色的话
它其实就是表示的是主副本
其他副本其实是基于Paxos实现的一致性
那因为每个RZone
RZone 1 RZone 2
每个RZone它都包含了一个自闭合的
它拥有的是自己完整的数据
那么它能完成所有的业务闭环
也就是说比方说我是做交易链路的
那么整个交易
这个人的交易它只在这个机房产生
只在这个分片里面产生
它不会去远程调用到多个机房
因为你可以想象一下
无论你的网光纤多么的速度快
即便是光速
如果你是构成了一个上千公里的传输
多多少少都是有延迟的
并且链路还有稳定性的一些问题
所以我们不要在多做多活的时候
假设跨机房的传输速度是无限快的
其实这个是很难做到的
只能说两个城市挨得非常近
你可以考虑一下
做这种同步复制
如果真的是很远上千公里的
我觉得是很难做到完全的异地
可以同时去提供多活的
然后最后我们看一下还有一个概念
叫CZone
CZone它也叫City Zone
因为我们刚刚说的GZone
Global Zone它只在一个城市部署
它是一个不可分割的不可拆分的数据和服务
我其他城市 城市2 城市3
它的RZone也会依赖这些数据
比方说我买家的数据可能会依赖卖家的数据
那么卖家数据你可能只在一个地方维护
我其他城市也要访问
我不可能每次都去跨网络来访问对吧
因为这样的话访问GZone的话
它的整个数据服务网络的延迟会非常大
那么怎么办
我们其实可以在每个城市部署一个叫什么
部署一个CZone
CZone它其实是作为GZone的一个只读的副本
注意它是一个只读的副本
是不是有点像我们上面的Global资源
就是Global Zone
也就是说Global Zone在一个机房单写
那么它复制到多个城市
最终提供的是一个什么
最终一致性的方案
写是只在一个地方写
复制到其他城市去
比方我产生一个交易行为
卖家的下单
整个交易闭环
那么买家数据是在我们的OceanBase
里面的OB1 OB2这几个片里面
但是因为你依赖卖家数据
卖家数据无法去切分了
那么怎么办
因为你买东西肯定是看到一个什么想买的
它可能无法去分了
我其实最终用比方说商品改价格改一些描述
它最终复制过来
可能有个一两秒的延迟
我觉得这个从业务角度来说
是相对来说可接受的
我们的淘宝卖家改完数据之后
可能你正在跟他进行交易
你们甚至还在聊天
他改了一些商品价格
改了一些商品的一些信息
可能你过个一两秒再刷一下
这个商品的信息可能就更新了
所以我的理解说这种核心场景来说
这种最终一致性其实是满足要求的
那么这个其实是蚂蚁的一个单元化的架构设计
从这个图里面其实
我们基本上能知道他的一些思路
我们再来了解几个术语
对于一个多核系统里面有几个核心的指标
一个叫RPO Recovery Point Object
表示的是什么呢
我们正常情况下是正常运行的
这个地方有个时间线
那么当我们产生一个灾难发生的时候
这个时间叫做RPO
表示的是机房级别故障时
未被同步的数据时长
有多少数据没同步到
你像考虑到MySQL这种特殊情况下
复制的延迟情况比较大
那么RPO可能还是分钟级的
你的数据复制延迟是很久的
正常情况下我们的RPO有可能是秒级的
在我们发生灾难的时候
你有多少数据还没有被同步过来
或者说需要数据延迟了多久
第二个叫RTO
Recovery Target Object
表示的是灾难发生到关键系统恢复
那么这些时间表示的是机房故障的时候
你的关键的流程或者说你的系统
切换恢复时间一般需要多久
比方可能是分钟级才能恢复
我要把流量导过去
然后我要让它变成一个主库等等
你要做一系列的操作可能需要好几分钟
还有一个叫什么Work Recovery Time
表示的是WRT 关键系统恢复以后
还有你的异常数据的一些恢复
你故障以后
因为在这个阶段
你可能有些数据还没同步完成
那么这些未同步的一些异常数据
可能你的系统
你的之前的机房可能又恢复了
可能有一些老数据没同步过来
现在又复制过来了
你之前那些数据可能又被篡改了
这时候可能就会产生一些数据复制的冲突
这些数据的冲突你怎么去修正它
从业务角度可能要做很多的一些补偿
或者说一些定损
或者是要分析到底要怎么去恢复
这些数据恢复成一个正常状态
这个可能需要花费更长时间
比方说可能上几个小时的
也跟你的原机房要多久恢复有一定关系
可能一般都是说小时级别的
那么了解这几个指标
我们在后面再来看
像别人的一些方案
这几个指标它都是怎么影响的
我们再看一下为什么要做多活
我们行业里面常见的一个分布式的架构
比方说我们单活系统指的是什么
一般来说就只有一个机房
一挂全挂
所有的应用和数据库都部署在同一机房
那么所有的服务均由同一机房来提供
那么它的容量其实是有限的
就是因为你的机房不可能无限扩展
比方我们一个机房就一栋楼
你的最终所有服务器都被你买掉了
你的所有机位都被你用掉了
你肯定就无法扩容了对吧
所以它的容灾能力只能说是节点级别
就是在我们集群内部的节点级别
你挂一个节点
你可能可以把流量倾斜到别的节点上
所以它的容灾级别其实就是一个机房内的
它的可用性能力其实还是靠我们就是机房内的
它的应用数据库的一个水平扩展
好 第二个其实叫双活
我理解整个架构的演进
其实也是先从单活演进到双活的
那么到双活其实最常见的做法
就是比方在上海你在A区域
比方你在杨浦有一个
你可能在另外一个地方
可能在闵行有一个
所以它就是有两个机房
这两个机房建设通常来说
我们从单活演进到多活的时候
通常都会把它变成同城的
相对距离不会太远的两个机房
那么你在做架构演进的时候不会那么复杂
所以我们看这个描述来说
简单来说是有两个同城机房一般是同城的
另外是应用无状态
在两个机房均部署
也就是你的应用各接受50%的流量
所以你一个机房放不下
你是不是两个机房就放得下了
但是注意数据库是只在一个机房部署的
也就是你的主库一般是只在某一个机房的
你的写库还是单点写
这个地方就有个跨机房的写了
但是因为你们是同城的
所以它的延迟相对来说可接受
但有些读的业务
因为你可以在你的同城的一个
机房做个备份的库
做个备库做个master slave
它是异步备份的
这样的话出现一些极端情况下
整个机房不可用
你的确是可以切
这种是可以切的
一开始你可能只负责一个读流量
这是一个做法
它承载的流量可能就是双机房能够承载的机器数量
我理解这种做法其实属于容灾能力
一个是机房内的容灾
也是你节点之间是可以挂掉一个可以去随便切的
还有就是机房间的容灾
当我们同城的主机房挂掉
你可以尝试切到备份的一个机房
那么它的好处就比之前单机房要好了很多
它的高可用能力
比方说它的应用可以跨机房水平扩展
数据库其实是可以水平扩展的
但是它是有些缺陷的
因为你只能单写
但是它读是可以两个机房去拓展的
当然它也是有上限的
因为你机房也是不可能无限大的
还有就是说你的双机房的双活能力
你的应用是可以部署在两边的
各承担一些流量
然后它有个坏处就是说
它可能会存在跨机房的数据库的访问
所以这个是双活架构
那么到了冷备的这种架构
我们看一下注意这个地方看这里是0%的流量
意味着正常情况下是不会有流量的
冷备的话可能是备份在一个很远的地方的机房
那么这种做法其实它也是两个机房的
但是应用无状态这两个机房虽然说可以都部署
但是数据库肯定还是一样的
是有一个主库那 边是冷的冷备
这种做法
跟上面的双活大部分是差不多的
也是机房内容灾和机房间容灾
但是它有些缺陷
他的这种做法
因为冷备的机房你如果要一刀切
那边的好多东西缓存数据库
cache都是冷的
可能会存在切过去扛不住的一个情况
所以他需要warm up需要流量慢慢预热
另外就是说备份的这些数据
因为冷备真的是需要很多初始化的一些过程
我觉得其实也是一种方案
但是对于这种城市间的一些机房灾难的话
可能是没办法就是不得已的一个办法了
这是行业里面常见的架构
对于多活来说
最终我要成长成一个多活系统
它也不是一蹴而就
基本上都是从单活到双活
然后再慢慢演进到多活这么一个阶段
我后面会举几个目前我看到的
行业里面做的比较好的几个案例
然后结合我们公司的内部的一些案例
我给大家讲一讲
我们先看一下饿了么他在演进过程中他是怎么做的
首先我们一样的
我们先了解一下整个饿了么的业务
因为大家可能用过
比方说你可以之前到中午的时候
到晚上的时候你可能会去点外卖
你可能要吃饭了
在整个业务过程中其实
有非常重要的几个角色
第一就是用户
用户这个时候他会查看指定位置附近的美食
然后选择下单去支付
第二个叫商家
商家他接单
接单以后他系统的安排骑手来配送
就是你下了单之后你肯定是买他什么东西了
然后他去找到一个骑手来帮你配送
从骑手角度来说
骑手需要尽快把食物送达到用户中
所以它其实是包含浏览下单
然后商户接单和物流配送这么几个步骤
这个还算是大家比较熟悉的一个饿了么的流程
所以我们讲多活其实一定要先搞清楚它的核心流程
大家可以想象一下饿了么它最核心的场景是什么
是不是
找外卖下单配送
就是下单
配单 其实就是指要制作美食
然后派送这个是它的核心的一个链路
那么这三个链路里面我们要做一个多活系统
我们刚才看到蚂蚁的方案里面讲到什么
讲到一个单元化的架构
那么单个订单的履单过程
尽可能是要在一个机房内完成的
如果我要部署一个上海机房
部署一个北京机房
你产生了很多跨机房调用
我们延迟是无法保障的
我的接口的响应时间是怎么保障
非常困难
所以这个原则是为了保证我们的接口实时性
那么履单过程中
不依赖另外一个机房的服务
才能保证最终没有延迟
这个是非常核心的一个点
在饿了么它这个架构里面
它其实也涉及了这些概念
每个机房成为一个ezone
不知道是不是饿了么的
饿e开头叫ezone
一个ezone包含了饿了么需要的各种服务
各种服务是什么
其实核心的就是包含它的核心交易链路
浏览下单商户接单
到物流配送的整个服务
所以它叫各种服务
其实指的核心的就是这些
一笔业务能够内聚在一个ezone中
那么一个订单设计的用户 商家
骑手都应该数据在相同的机房
注意你涉及的用户商家其实都应该在相应的机房
那么这样订单在各个角色之间流转速度是最快的
因为你不需要跨机房调用
也不会因为各种异常的情况导致超时
那么恰好饿了么它的业务
是非常典型的地域化的
我们叫强LBS的
通过合理的地域划分
你就可以实现一个业务内聚
比方说你在上海下单
你不可能去下单到苏州
你肯定是上海的下上海的
上海的对应的制作
商家和骑手肯定也是上海的
可能你会抬杠
你比方说我用户在北京
我要给我老婆在上海点一份
那么其实这个也没关系
因为你的履单过程你给别人点
那么你浏览的是什么
上海的这些美食
浏览的是这些指定位置的美食
然后你下单的当然也是下单到上海机房的
那么你的商家也是在上海
骑手也是上海 他不可能从北京
派个骑手去帮你派送
所以业务内聚非常关键
指的是一个订单在履行过程中
机房内注意一定是在一个机房
或者我们叫一个单元内完成一笔交易
一个请求 一次业务过程
这个是基本原则 第一个
我们看第二个点叫可用性优先
为什么
因为在饿了么这个场景里面
它其实发生故障的时候
切换机房的时候优先保证系统可用
因为叫什么
我们叫首先让用户可以下单吃饭
容忍有一些时间段内的数据不一致
比方说你订单刷一刷可能出不来的
因为最后有一小笔用户可能订单没有产生故障的时候
有一小笔用户的订单数据没有复制到别的机房
可能会在某些时间产生数据不一致
但是我这个时候尽可能让新用户
就是让那些没吃饭的用户赶紧可以下单买
让他完成后续交易
事后那些可能下完单状态不正确的人
我们就在事后去做补偿
因为这样的话其实对我的业务虽然是有损的服务
但是我的业务是尽可能先恢复
让大家可以后续流程可以赶紧去运转运作
既然你要保证每个机房可以直接切
是不是意味着每一个ezone其实都有全量业务
否则你这边机房一故障
你切的那个机房用户数据就完全没有了
所以说它一定是要做全量的业务数据复制的
那么当一个ezone失效以后
其他的ezone可以接管这些用户
所以他要做一个复制
用户在一个ezone下单的数据
会实时复制到其他的ezone
所以这个叫可用性优先
我们再看一下它第三个点叫保证数据正确
在确保可用的情况下
我们要对数据做保护 以避免错误
比方说在切换和故障的时候
如果发现某些订单的状态在两个机房不一致
比方说A机房订单是交易成功
那么在B机房可能订单处于什么
已下单未支付
那么其实就已经产生了一些数据不一致对吧
这个时候可能就要考虑锁定该笔订单
阻止对它的进行更改
保证数据正确 因为这时候产生了一些数据的冲突
到底我们要需要通过一些各种手段
去修正这些数据错误
其实是我们没办法
就是因为你在某些情况下
在切换或者故障的时候
一瞬间可能出现一些不一致
这个时候阻断它的变更
可能是阻断它的变更
然后让数据同步完成 继续提供服务
要么就是说如果两边同时都改了这个数据
因为一些异常情况下可能两边同时都改
这个时候就得什么数据冲突
就得我们人工去介入去恢复这个业务
这个叫第三个保证正确性
那么第四个叫业务可感
因为基础设施还没有强大到
可以抹去跨机房的差异
因为有些情况下可能会跨机房的一些调用
那么需要让业务感知多活的逻辑
因为我要去做一个准确的路由
路由到他用户应该在哪个机房内路由
那么所以我们业务可能要配合一些改造
比方说业务代码也要识别出
业务的数据的归属机房
归属到哪个ezone
那么只处理本ezone的数据
因为一些意外的原因
假设出了一些bug
不应该属于我机房的一些 比方北京用户
我的上海机房处理北京用户肯定不合适
要么就是把他接口请求失败掉
要么就是把这个请求我帮你转发到一个正确的机房
去完善业务的一个状态机
那么在出现数据不一致的时候
通过状态机去发现和纠正
这个其实是要做一些保护
能够感知到我的这些订单id之类的
这些数据能够路由到正确的地方
所以这四个点是饿了么设计的一个关键
我们经常搞清楚了
那么饿了么业务它其实是强LBS相关的
就是跟地理位置有关
我们其实在做划分的时候
就比较容易想到了
比方说饿了么他当时是在上海有一个机房
在北京有一个机房
我是不是可以让华东整个片区的数据
全部路由到我们的华东 上海的机房
我可以让华北的所有的用户的
数据全部路由到什么北京的机房
我们就要考虑用什么来做划分方法
也就是设计多活的Sharding Key
因为我有上海机房和北京机房
即便我在上海机房挂掉
我的北京用户是某个时刻
其实不受影响 可以正确下单的
我这个时候就想办法让我上海用户的
流量慢慢迁移到北京机房去
这个就是多活的优势
我的业务受损的时间就比较短
如果你只有一个机房一挂
整个中午都吃不了饭下不了单
这个影响我们叫什么故障率就非常之大
涉及到划分的话
实际上我们就要考虑对服务进行分区
让用户商户骑手
能够正确内聚到同一个ezone
比方北京机房还是上海机房
分区方案其实是整个异地多活的一个基础
它决定了之后所有的逻辑
所以一定要搞清楚你的业务形态
想清楚到底有哪一个Sharding Key
刚才说的饿了么
它的特点就是自然而然大家会想到用地理位置
也叫地理围栏
地理围栏可以理解为它的主体是按照省界来划分
再加上一些局部的微调
作为划分业务的单元
把地理位置上接近的用户商户骑手划到同一个ezone
就刚才说的我的华东
全部到上海机房
这样的话其实很容易想到
因为你的履单过程
其实就是跟地域强相关的
那么这样的话就是
你的整个订单的一个履单的过程
全部是在一个机房内完成 能够什么
保证最小的延迟
那么在某个机房出问题的时候
你还是可以切到其他机房去的
所以Sharding Key的设计是非常关键的
我觉得饿了么他这个做法其实非常有意思
我会先举饿了么这个例子也是有很大关系的
我们前面学习了DNS CDN
我们知道DNS的话
是让你路由到最近的接入节点
核心的路由逻辑怎么做
饿了么它的做法是什么呢
首先它是基于地理位置去划分规则
那么他又开发了一个叫做API路由的这么一个服务
叫统一流量路由层
那么这一层 API Router
我理解之前的边缘CDN可能不带任何业务逻辑
相当于我现在的 API Router带了一些业务逻辑
那么这个业务逻辑它要负责什么
实际上就是对客户端发过来的API调用进行路由
比方说通过DNS用户让我接入到上海的边缘节点
接入进来之后我连到CDN
这个CDN可能现在变成了API Router的模块
它实际上就要搞清楚你这一个订单
应该把流量导向到哪一个正确的ezone
因为我之前说的它是按地理位置划分好的
那么API Router
因为它是可以在饿了么这个场景里面
它部署在多个公有云上
那么用户就近接入公有云的API Router
还可以提升接入质量
所以我理解API Router
跟Edge CDN就是边缘CDN其实是一个道理
我们看一下你的Sharding Key是什么
是地理位置
那么我们看一下它其实还做了一些事情
像商户ID订单ID这些不带地理位置的一些请求
你访问API Router
我猜测它可能是在商户IP
订单ID里面打了一些特殊的标记
API Router可以识别出订单IID的一些信息
或者说这些订单ID有一些别的映射的方式
可能查表之类的
知道它应该去哪个机房
那么这个API Router可能是做了一个路由
那么这样的话你就可以知道这个订单ID
去北京还是去上海机房查询
这个是它的一个Sharding Key
我们再看一下
刚刚说的最基础的分流标签是地理位置
因为有了地理位置
你的API Router就能计算出一个正确的Shard归属
我们刚刚说的你去覆盖的区域
但是业务是很复杂的
你并不是所有的调用
都能关联到某个地理位置上
所有的接口所有的（请求）
都会带上一些地理信息
他们做法就是叫分层路由
就是核心路由肯定是由逻辑地理位置决定的
你比方下单的时候
你的地理位置决定订单ID生成在哪个机房
他们设计上也支持一些High Level的Sharding Key
那么这些Sharding Key
由API Router转换为核心的Sharding Key
就是说先把这些更高层级的一些Sharding Key
可能是订单ID商户ID
应该路由到哪个机房
减少了业务的改造工作量
也可以扩展出更多的分区方法
我们看一下
你看他的逻辑Sharding Key
包含地理位置 商户ID
骑手团队ID订单ID等等
各种Sharding Key
最终被转化为某个地理位置相关的一些数据
某个地理位置的相关数据
就会映射成一个Sharding ID
这个ID其实映射的就是一个机房
说白了最终把这些数据可以映射到某个机房去
最终可以路由到一个正确的机房
我觉得这个是饿了么它整个设计的关键的做法
饿了么这个做法我们终于搞清了
API Router跟边缘CDN的一个做法
所以我们看清楚了
他是在边缘CDN就决定了
根据你的订单ID你的地理位置信息
决定由去哪个机房去下正确的一个单
所以这个就是DNS CDN和多活的一个关系
边缘CDN或者说我们的动态CDN
饿了么里面叫API Router
它决定了你的路由应该去哪里
这个是非常关键的
除了饿了么
我们再讲一下阿里的多活
那么阿里的业务非常有意思
他们的设计上不能按卖家来划分
就是因为我假设有三个机房
如果我按卖家划分的话有点奇怪
要保障用户的一个下单体验
因为买家多
买家占大头
所以我们刚刚说你要设计多活的时候
一定要从你的业务场景
还有你的数据量考虑
买家的订单信息多恐怖
卖家肯定是偏少的
买家肯定是偏多的 对吧
所以按买家维度来进行数据切片
你如果按买家维度切片
我们再想一下 与买家相关的链路
那些单元是不是要做多活
你肯定是让买家的数据
比方在北京机房的我的依赖的那些数据
你不可能跨机房调用
因为肯定存在一些Global Zone
那么在阿里的场景里面
他们是让卖家的数据变成一个Global Zone
变成一个单写 单机房单写
然后再通过把卖家的数据复制到
各个的这些数据切片的单元化的机房里面
产生一个所谓的买家链路的一个闭环
比方下单这样单元内最大限度的一个封闭
避免跨机房的RPC
还有就是无法接受数据最终一致的单元
单点写 这个是一个
你像有一些场景
的确是无法接受数据的最终一致性
你下单这种逻辑肯定是不能接受最终一致性的
看下业务架构 这里有单元1 单元2还有单元n
我们看一下单元应用这写的是什么
买家相关的应用
因为我核心要搞定他的交易场景
所以你看单元化完成交易下单
那么本单元的买家维度数据
比方说订单信息等等
这些信息都是可以同时读写的
注意啊这个是不能最终一致的
因为你下达要立马可见
没有什么最终一致 要必须立马可见
所以分片其实就是
你的用户A在单元1同时读写
因为你只有一个机房
所以它一定是强一致的
你的用户2在单元2
同时你的买家数据也是在单元内闭环
所以它一定也是强一致的
所以这样来保证
不是所谓的最终一致它就是强一致
因为你的数据分片了
有用户分在A机房
或者B机房 或者C机房
我们因为下单要依赖一些商品信息 库存信息
我理解像商品信息
注意它这个地方是只读的
写了一个只读
我们刚刚讲蚂蚁的叫什么
叫CZone
其实就是Global Zone的一个只读副本
所以这个地方是有全量商品及买家数据的
你的整个闭环单元都是在里面的
所以你肯定要全量的商品及卖家数据
所以它是靠哪复制过来的
它是靠我们的中心机房
我们的中心应用包含什么
库存买家还有一些长尾的别的一些应用
这样的话我们看蓝色这条线头是卖家及商品数据
需要全量复制到这边的所有的一个单元里面
所以我们看卖家维度的数据读写
其实是路由到中心机房的
进行全量的一个读写的 然后再复制过来
所以这个是CZone
卖家和商品数据是什么
是一个GZone
CZone是为了加速RZone访问的
RZone就是买家的数据
那么CZone是卖家的数据
GZone是卖家的数据
CZone是作为GZone的一个只读的副本的
所以我们搞清楚这个业务场景
其实你就会觉得多活好像没这么难
你就搞清楚它为什么可以同时读写
它不会存在数据冲突
因为你操作的都是自己
你比方用户ID等于1的是在A机房
用户ID等于2的 他是在B机房
他操作的是自己的数据
他不会存在冲突
所以这个是阿里的一个做法
我们再看一下
实际上它最终我们看也是通过DNS CDN
来进行用户分流的
也就是说这个地方CDN里面
是不是CDN的nginx
或者说我们刚刚说的饿了么的API Router
它是存在一个路由逻辑的
可能是通过用户
用户可能是通过Cookie
然后你的CDN解析这个Cookie
发现你应该要去IDC-1
你发现另外一个用户可能要去IDC-3
所以说你们看这里是通过CDN来进行用户分流的
那么这里面注意阿里的架构和饿了么有什么区别
饿了么它因为是双机房
就是北京上海的相互之间的一个复制
那么在阿里的做法里面
IDC-1 2 3它除了IDC分片以外
还有个中心的一个机房
为什么有个中心机房
这样的话它的数据复制
IDC-1要复制到IDC-2
IDC-1还要复制IDC-3
因为你这样的话
假如我有七八个的话
我IDC-1要广播到剩下的6个机房
太复杂了
所以他会把数据同步到中心机房
交给中心机房再分发给其他的IDC
这样的话我们的整个复制链路
不会像对等网络一样这么复杂
这个是它中心机房的一个好处
另外就是说在阿里的交易场景里面
刚刚说的卖家的一些数据
库存的一些数据 它可能是强中心依赖的
包括像账号的一些数据
账号passport这种很中心依赖的
中心机房有这么一个好处
做数据复制的一个分发的
我们再回到之前蚂蚁的GZone
RZone CZone我们再看一下
那么他怎么做容灾
我们先看一下同城容灾
我们看这个图里面
比方说这个地方有个RZone
RZone是什么
刚刚说的它是负责某一个分片的
某一个用户分片的数据
同城容灾因为我们看一下在这个城市1里面
RZone 1和RZone 2
RZone 1在OceanBase上是有两个副本的
OB1和OB2
那么这个时候RZone1出现故障的时候
先看同城容灾能力
我们目标其实是
将RZone 1切换至同城容灾的RZone 2
把流量要切过去
那么在流量切过去以后
其实是要先做数据库的分片切换
也就是说之前是主副本
我再把这个OB1切成主副本
先要把副本提为主数据库
然后再把流量切到RZone 2
这样它完成一个效果是什么
RPO=0
注意RPO=0为什么
因为它底层OceanBase
是用Paxos来做数据复制的
我们知道它是强一致的
所以它数据不会丢Raft的或者Paxos
因为它是要保证至少是过半数
副本以上同步写入成功才返回
我理解蚂蚁的为什么它会去宣传这种做法
因为它是偏金融
金融业务 我们对数据安全要求高得多
可能会牺牲一定的延迟性
要保证数据绝对不丢
所以他其实做法是
在同城是有两副本
在另外一个城市
可能这两个城市 城市1城市2非常近
也是有两个副本
在异地一个冷备可能再有一个副本
虽然是五副本
意味着它至少要写三个副本成功才会返回
所以我们看城市1 城市2必须挨得近
否则它的latency无法保障
所以你看城市1两个副本有个好处什么
同城容灾是非常快的
并且是不丢数据的
因为我们是基于底层Paxos
或者Raft去做复制的
我们再看一下RTO是小于一分钟的
RTO指的是你的数据提升为主副本以后
我要让我的业务流量切换到恢复
可能要做一些少量的预热
那么它可能需要一些时间
那么这样的话分钟级去恢复
我觉得可能是可以做到的
这叫同城的一个容灾
这样底层是依靠Raft或者Paxos做的
是不是所有的业务形态都要这样去做法
我觉得金融类的业务可能是需要的
但是我们很多互联网业务可能是
通过MySQL的异步复制做的
就像我们刚刚看到支付宝的
看阿里的业务里面
可能淘宝的业务它可能是异步复制的
我们注意这个地方可能是异步复制的
因为我的买家数据最终一致
那么对于我的每个单元要去切换的时候
单元1单元2去切换的时候
可能是通过中心机房复制过去的
那么这样的话
数据可能是存在一定的延迟的
不敢直接切的
可能跟金融业务还是稍稍有些区别
对于异地容灾来说
我们同样以RZone 1出现故障为例
这个时候OB1有5个副本
这个时候在城市2是有一个副本的
我其实可以找到
分片1在RZone 3副本可以切换为主副本
就这个地方切换为主副本
完成后将RZone 1的流量切换到RZone 3
实现了异地容灾
那么这个异地容灾它的好处是什么
它的过程就是RPO=0
它没有数据任何丢失也不需要恢复
那么RTO是小于一分钟的
一分钟也是可以（满足）
异地的一个城市可以把这个业务拉起来
注意 虽然这个上面吹得非常好
但是一定要注意
这个是需要牺牲写入的延迟性的
五个副本组成的Paxos或者Raft
是必须要过半数副本写入成功才能返回的
所以城市1城市2必须挨得非常近
另外要接受一定的延迟
这个延迟可能是几十毫秒内
因为你可以想象一下
它城市1城市2因为非常近
我们假设它的复制在5毫秒内
那么这样的话其实你的整个
latency也是会增加一些的
它是有一定牺牲的
异地容灾城市1城市2是非常近的
苏州到上海
但是我们出现过
比方整个华东区域网络全部瘫痪的情况下
我另外一个城市3的副本可能在北京
那么这种情况下你要让它恢复
它其实是有一定代价
这个时候他的RPO能不能等于0呢
能做到
但是你的数据写到上海 苏州
北京都成功才返回
大家可以想象一下延迟是很高的
所以基本上我相信没人会这么做
因为这个网络的延迟是很难保障的
第二
如果在一些不得已的情况下
真的要让北京的机房业务恢复
所以我们看一下它的异地容灾里面
它其实也提了一个点
数据副本弱同步
这个时候RPO就要大于0了
一定是存在没有同步完成的一个情况
我们知道Raft是过半数同步成功
所以你是要保证这两个城市里面两个副本成功
这个城市2有一个副本成功
那么城市3你勉强追上就行了
我不会保证你一定要写入成功
我这个接口才会返回
所以你可以理解为它就有点像一个异步复制
所以它的RPO要大于0
所以这个是蚂蚁的一个做法
我觉得这个还是蛮实际的
那么我们再看一下在蚂蚁的模块里面
它如何做流量路由的
其实这个也非常好
那么流量路由一样的
也是通过CDN
核心的就是要搞定用户ID的信息
和对应的Zone机房的信息
怎么去路由
我看到他是把信息植入到Cookie里面
供我们的路由模块来做精准路由
比方说这个地方用户uid等于68
目标Zone等于RZone 03
我们看第一步A1
A1首次访问
首次访问肯定没有路由信息
肯定Cookie是空的
那么他随机访问到任何一个机房
注意这个地方是
DNS解析挂载LB-1和LB-2
这个边缘LB-1和LB-2可能就是个边缘CDN
DNS可能也可以解析到直接进入到那个机房
我们先随机进入任何一个机房
因为你没有Cookie
这个时候我们看一下
这是A1 那么第二步叫A2
A2无Zone信息
那么网关路由到本Zone
接入路由LB-1
它通过接入路由路由的本Zone
那么A3是什么
业务系统根据uid计算出来的目标Zone
也就是说一开始用户注册的时候
其实就决定了你要去哪个机房
好 我根据uid计算你应该去哪个Zone
也就是它有一个路由表
那么并写入到Cookie
也就是我这个时候会把数据写入到Cookie里面
再转发通过我的RouteClient
再转发HTTP请求到目标Zone
刚刚说的他要去RZone 03
你就把流量转发到这里了
所以我们看到这里面有两个角色
一个叫ZoneClient
一个叫RouteClient
ZoneClient责路由计算
RouteClient负责流量转发
好我们再看一下用户
这个用户可能请求就结束了
那么第二次因为Cookie
用户的这一次的Session内
就是他这一次登陆成功的Session内
他再次请求另外一个接口
那么因为这个时候Cookie已经有了
那么假设我们看B1请求他到哪
它又到了上海机房
那么B2因为Cookie里面携带目标Zone的信息
所以它在接入路由层直接路由到RZone 03了
就是B2的请求就结束了
我们再看它的C1请求
用户的Session内
他再次请求的这个cookie
我们看看他携带的信息去哪
又打到了另外一个机房
这个可能是北京的
那么这个时候我们发现
第二步Cookie内携带本Zone
发现他要找到本机房
所以他之后本机房就直接路由了
好 我们再看D1的请求
D1请求用户Session内
再次请求 Cookie中携带Zone信息
同样的通过接入路由直接路由到本Zone
所以我们看它是通过DNS
然后到CDN的一个LB-1
通过接入路由
做了一个路由的转发
所以它其实本质上你可以理解为就是
机房的一个路由 和跨机房的一个路由
这两个大家还是要看一下
所以这个是阿里的一个做法
我理解就是蚂蚁它其实前段时间
也是开源的OceanBase
他刚刚说的要做到数据的强一致
可以随便切
其实五副本 要三副本保证强一致
不可能五副本全部都写入成功
五本保证三副本强一致
我理解他最好的做法是什么
同城两副本必须写入成功
然后再来一个离得最近的一个IDC-2
写入一个副本成功就赶紧返回
这个是他的一个做法
也是蛮好的一个做法的
我们看完阿里的多活
我们再来看一下苏宁的多活
方案其实是大同小异的
我们看一下首先一样的
苏宁的整个方案里面
你看他以前的这种
同城多活里面也是两个应用
就是双活的时候都是两个机房部署应用
然后写全部要路由过来
然后读是可以读这个的
基本上大家都是这个做法
我们再来看一下他做了一些什么样的抽象
他第一个叫Cell
业务可封闭的收敛最小执行分片
其实分片就是一个业务用户的一部分数据
在某一个分片里面
业务对请求空间按照一定维度
苏宁可能是按门店 按会员划分分片
我们叫单元化 所以它叫Cell
有一个叫LDC的概念 逻辑数据中心
由多个业务可封闭的Cell组成的一个单元
拥有独立的基础设施
以及网络出口
我理解的LDC
有点像刚刚说的RZone的概念
就是一个业务闭环单元
那么还有个PDC物理数据中心
指的是物理上独立的一栋建筑
这个就是一个物理机房了
那么AZ可用区指的是
拥有独立故障隔离空间
一般来说Available Zone
指的是有独立的网络设施和电力设备
但它是由相邻的几个物理数据中心
可能是两栋楼在一个园区里面
因为两栋楼园区可能是独立的网络电力
那么我们一般叫AZ
它可能是挨的比较近
还有就是Region
它是很大很远的一个地理区域
有多可用区组成的一个集合
Region之间可能是非常远
物理上可能都是非常远的
这个是我们看到它的一些概念了
好
再来看一下苏宁多活它内部是怎么说的
第一个它叫分片服务
分片服务 是不是像刚刚讲的RZone
或者叫饿了么的ezone
对应的数据仅在某个Cell内存在
其他Cell不与交叉或者共享
比方说会员服务订单
刚刚说的订单下单
这些就是很典型的一个分片服务
好 还有一个叫什么
共享服务
共享服务是不是听上去有点像刚刚说的GZone
Global Zone
那么所有Cell拥有相同的数据相互共享
比方价格商品
商品是不是有点像卖家数据
所以它需要Global Zone 它可能需要单写
然后通过CZone来做共享的读
我们看到它有个叫索引服务
在苏宁里面可能是做搜索检索的
我觉得它本质上也是类似于共享服务
可能核心有一个地方写
然后其他地方是只读的去查询的
注意这个地方有个很关键的
它有个叫竞争的控制服务
注意它的这个地方是有个叫
竞争控制服务的
就是各个Cell相互操作
同一个数据是为了保证数据一致性
那么需要在同一个数据中心进行控制
有点像我们刚刚说的阿里的中心机房
它比方说库存的扣减
你库存其实是多个机房同时去抢库存的
你无论怎么分片
对于卖家的库存来说
你肯定是要集体去争用的
包括像用户注册
所以这个地方其实就是它的一个主数据中心
它的核心数据中心
但是我们看一下苏宁它的里面
又搞了一个叫竞争Proxy服务
在LDC#1 LDC#0
LDC#2
它都有个叫竞争Proxy服务 它干嘛呢
用于竞争服务前的前置服务
这个是非常有意思的一个设计
我们一直在想库存扣减
到时候我在秒杀某一个商品的时候
会不会出现库存
我去扣减的时候
跨机房调用到中心机房
然后全部是变成了一个跨网络调用的
我们之前是不是讲过可用性设计的时候
我们讲过一个什么思路
限流的时候要Quota
以前是要一个配额 变成批量拿Quota
是不是通过批量拿Quota
会减少请求Redis的次数
同样的思路 你去拿库存的时候
你是不是可以先调拨一些库存
比方说你可能虽然你买只买一个
我是不是一次调拨个100个库存过来
假设我一共有1000个库存
我每次请求调拨50个调拨100个调过来
我本地消耗完了
再去请求我的数据中心
再去把库存再怎么调配过来
那么这样的话先本地消耗
本地的Cell本地单元消耗完
再去集中的机房
主数据中心再把它拖过来
这个思路其实大大减少了跨机房的调用
以及请求主数据中心
尤其是这种Global资源的一些争用
我觉得这个思路是非常好的
因为我们刚刚说的CZone中是为了读
GZone写都要去中心机房写的
这样的话通过库存的一个调拨服务
大大减少了向主数据中心的写请求
苏宁多活里面我觉得最有意思的一个点
这个也是值得大家可以去参考和设计的
另外就是说苏宁多活里面
为了确保高可用
以及任何一个机房故障都可以被接管
所以说所有数据中心都是要包含全量数据的
那么主数据的变更是实时同步到
各个数据中心的
数据中心之间的延迟
相对数据中心内部的延迟肯定是要较大的
所以数据中心之间的同步一般是采用异步复制
你无论机房多么的近
其实都是存在一定延迟
尤其你是跨几千公里的话肯定是更远的
那么在机房出现故障的极端情况下
将出现少量的数据未同步到其他数据中心
针对此类故障
在机房恢复后可能需要对未同步的数据
进行一些人工的修复
所以你看苏宁的做法是什么
是异步复制
然后在另外一个之前故障的机房恢复的时候
你可能之前有些数据没同步
这些时候就产生一些数据的写入冲突
那这些需要怎么办
需要人工介入去对业务进行修复
所以苏宁的做法是做了一定的牺牲的
那么我们看之前蚂蚁做法是通过Paxos
Raft在跨城市间去做一个强同步的
这样的话去牺牲的是什么
牺牲的是可用性
其实牺牲的是整个数据延迟
它的latency会非常高
并且它的可用性会变得比较差一些
所以其实就是trade off
要看你的业务场景能接受到什么程度
比方金融类你无法接受
我必须要各个就近的两个城市
必须都存在三副本都成功
那么就要牺牲延迟
这个是没办法的
那么再看Facebook
其实我们讲了很多大家的一些做法
但是一直没提到他们在cache上怎么处理的
我这个地方就摘了
《Scaling memcache at Facebook》
这篇paper里面的一章 核心就是要看一下
Facebook的一致性它是怎么做的
我们刚刚看到有这样的
同城这个APP会把写操作到这个DB
那这个时候就尴尬了
你写操作过来
我这边的cache怎么办
谁来刷
你写在这边 我的cache一读还是个脏的
那么这个其实就是个典型的
cache一致性的问题 怎么保障
我们看Facebook在这个场景里面怎么解决的
第一步Web Server它会set一个
remote marker
设置一个标记在Memcached里面
第二步把这个数据真的是forward到主库
Master DB
写到这个主库
这个时候你想象一下
如果你不按照他的方法
你是直接把写到主库
你去读缓存肯定是脏的
不管你是写缓存 删掉缓存也好
因为我要invalid这个cache
我要覆盖这个cache或者删掉缓存
这个时候你很尴尬
你去读数据库从库的时候
这个数据还没复制过来
你这个时候缓存不就脏了吗
我们再看看为什么就要靠Facebook这个方案
我们看看他怎么搞的
第一步刚刚说的
要设置一个远程标记位
第二步把写操作搞到主库里面去
第三步是什么呢
Delete from memcache
因为你写操作要产生了
所以你第三步肯定是删除缓存
把这个缓存删掉
注意这个时候
我要去产生一个cache miss的时候
因为我会第一步先判断这个标记存在
这个标记如果存在
我就会去回源到Master DB去读
所以我们看一下Read miss的一个path
If marker set read from master DB
所以说如果这个标记存在
产生一个写行为
我的cache miss要去主库读
这样的话就不会读到从库的脏数据了
如果你这个标记不存在
我就可以读从库
这个标记什么时候不存在
当我的第四步数据库复制过来的时候
我解析从库的binlog
发现这个数据复制过来了
我再把marker把标记给删掉
它是通过这样的remote marker的机制
保证数据一致性的
也就是说用户刚做的写操作
我会尽可能通过回原到远程的Master DB
来保证它的cache的一致性
我觉得这个思路非常巧妙
有些地方是可以借鉴的
其实还有另外一种思路
这个是在Facebook另外一篇paper
《TAO》里面讲到一种机制
我就不展开讲了
大家可以做一个扩展的阅读
Facebook的TAO这个系统它其实是
Facebook的一个Graph图存储
可以看一下
它里面也有个思路非常有意思
可以值得参考
好
我们最后再来看一下微信朋友圈的异地多活
微信朋友圈跟其他几个做法又有一些不一样
微信的朋友圈它的做法
它其实是本地写 本地读
比方说我们看一下小王是在国内
海外有一个叫Kate的用户
一样的就是中国北方是靠上海
中国南方是靠深圳
然后在美洲与欧洲是加拿大机房
香港是覆盖东南亚 南亚
中东和非洲的
可能有这么4个机房
数据复制可能是通过专线或者是公网
类似像UDP这种去同步的
这么一个做法
我看一下它的核心思路
比方说小王发的朋友圈
用户对他朋友圈有些评论
那么其实它是本地的一个读写
这个上海机房 小王用户他在上海
始终都读的是上海机房的数据
它不会去跨机房读
比方说小王对朋友圈发了一个评论
也是一样的
只会在这个地方写 在这个地方读
那么这个地方产生的一个写
会同步到加拿大
同步到香港 同步到深圳
它会去做一个数据复制
那么这样的操作
它会导致一个非常有意思的问题
叫因果一致性
我们先看一下什么叫因果一致性
首先因果一致性我们了解
因在果之前
先产生原因才有结果
那么同样的像消息发送在消息收取之前
你必须先发送才能收取
那就像现实生活中一样
一件事会顺序的导致另一个事情发生
或者说某个节点读取的一些数据
然后写入的一些结果
那么另一个节点读取其写入的内容
并依次写入一些其他的内容等等
那么这些其实都是存在一个因果关系的
这些因果依赖的操作链
定义了系统的因果顺序
即先发生什么
什么在什么之前发生
有点像我们之前介绍Go的happens before
一个系统服从因果关系所规定的顺序
我们就说它是因果一致性
比方说朋友圈某条状态的评论
以及对评论的答复
也是一条评论
它们是构成一个因果关系的
朋友圈这个场景其实
不存在因果关系回复的这种评论
可能有些乱序的问题不是那么严重
因为你是有一个回复关系的
所以说要保证不同数据中心之间的因果一致性
来保证一个用户在刷朋友圈的时候
不会出现看到评论所对应的答复
却看不到答复对应的评论
你看到好奇怪看到了一个答复
但是之前原始的评论不见了
它在后面你就很奇怪了
我们举例来说的话
比方说我们上海的评论副本
小王我刚刚说了 微信的设计
因为它是一个全局广播
全局广播去做复制的
但是用户的整个写操作是在本机房
微信它其实是个是一个很social的
就是跟社交网络有关系的
小王注册的时候在上海
那么他的数据可能就全部在上海
小王发的朋友圈数据也在上海
那么他写评论回复评论肯定也在上海
如果是小王的朋友圈可能大部分都在上海的话
他回复朋友圈那些数据可能也是在上海的
这些数据可能会异步复制到其他的机房
因为这个很可以接受最终一致
他没有那么高的要求
我们看到这个数据
比方说有一个评论是香港的评论的机房
Mary回复了一个说这是哪里
这个数据会被复制到上海机房
小王看到以后他就会回复说
Mary这是梅里雪山
这个是异步复制的
我们再看一下这个复制过程
这个数据可能又被复制到加拿大机房
可能也是一样的 这是哪里
这是梅里雪山
但是我们再看一下什么叫顺序错乱
因为由于网络在不同副本间
复制数据的延迟
或者是中断等等
导致两条消息在同步到Kate的加拿大机房的时候
出现了一些乱序
以前的顺序是Mary说这是哪里
这是因
果是小王说Mary 这个是梅里雪山
这个是因果关系
Kate是在加拿大
如果出现了一些顺序错乱
Kate应该在加拿大
这个时候她看到的是什么
小王回复Mary这里是梅里雪山
但是下一条评论是说Mary这是哪里
是不是这个时候因果顺序就错乱了
就很尴尬
或者说中间的某个时刻只能看到一条
小王说Mary这是梅里雪山这一条消息
为什么会出现这种情况呢
我们看一下它怎么复制过来的
有可能复制的加拿大消息延迟了
因为可能丢包或者各种原因丢数据丢失了
或者说延迟了丢了
但是小王发的Mary这里是梅里雪山
上海到加拿大同步成功了
是不是有可能会出现先看到Mary这是雪山
然后结果香港到同步又好了
这个时候收到一个这是哪里
产生了一个因果不一致的问题
所谓的因果错乱就是这么导致的
我们看一下怎么办 怎么去优化
微信朋友圈的解决方案也比较有意思
首先它第一个做法就是叫
每条评论都有一个唯一且递增的数字ID
这样的话背后肯定是有个ID生成器的
每个数据中心都有一个这样的入口来
获取一个本机房内唯一且递增的ID
这样的话你在数据复制的时候ID不会冲突
其实要全局还要唯一
第二个每条新评论的ID都必须
比本地已经见过的最大ID要大
确保因果关系
这个什么意思
其实有点像一个逻辑时钟
逻辑时钟大家可以去搜一下
就可以搜一致性逻辑时钟
这个是有一个叫消息传递的一个概念
就是数据复制过来
A到B数据复制过来
那么比方说复制过来之后 ID等于5
那么我接下来产生的评论ID一定要比5大
这样来保证因果一致
举个例子 我们在香港发了一个评论等于2
在香港的数据中心刚发表完等于2的评论
并且我这个时候上海也有人发的一些评论
可能发的147它是有个顺序的对吧
这个时候数据复制过来
那么我把这个消息合并以后就
变成1247了 变成1247
这时候如果我还要在香港发一个评论
那么这个ID一定要比之前看到的1247都要大
所以我们看到这里面出现了一个ID=8
也就是说一定要比之前看到的
最大的ID 还要大 来确保因果关系
因为你要1247复制过来以后
一定产生下一个评论
一定要比之前看到的都要大
它一定是在后面的
它不能插到中间去 这个是第二个
那么第三个是什么呢
要广播本地看到的所有评论和新评论到其他IDC
相同ID的评论会合并排重
也就是说这个地方叫Vector clock向量时钟
为什么要采用这样一个做法
我们看一下它也是有原因的
因为本地域下用户针对同一条朋友圈的状态有评论时
该地域会负责申请一个全局ID
然后将这个评论的事件广播到其它数据中心
这是我们之前知道的
但是注意这个过程需要合并所有看到的序列
就是说你当前看到的所有序列12478
你要把完整的评论再冗余
同步复制一份到加拿大
为什么要把合并的这些事件全部广播一次
因为这样的话才能保证在同一条状态的
所有当前最新事件整体被广播出去
否则此时香港IDC如果只广播8的话
如果你只广播最新的这条评论的话
前面的序列在广播中丢失了
前面丢失以后
你如果只看到8是不是会产生逻辑错误
这个时候正确的顺序是12478对吧
如果我只广播8到加拿大
有可能是8先出现 再看到1247
所以它是要把当前看到的
一个快照的整体顺序同步过去
为什么朋友圈会这样做
虽然数据冗余交互是很大的
但是我们知道有个前提
一般朋友圈下的一个评论列表不会太多
所以这个做法是非常取巧的
它假设的就是说
同一个朋友圈下的评论列表不会太多
可能就几个几十个
所以这样的话看到的顺序大家都是绝对一致的
其实非常有意思的
我们可以再想象一下
8如果单独复制出去
先到这边 假设我们1247
还没复制过来 8先到这边
或者是28先到这边
然后你再把147复制过去
它整个逻辑就错了
28有可能是个果
为什么是8是果
因为8是看到1247之后回复的8
如果你的28先到这边
你的1247再过来
就是因果错乱
它就会产生数据因果不一致
所以它是要靠全量冗余广播
这个是不是最好的做法
不是
其实我们如果能实现一个
因果复制存储一致性的这样的一个数据库
或者说用这样的一个思路
其实也可以解决这个问题的
我们是不是考虑给每一个上海 香港 加拿大
的每一个评论ID三个人都带上版本号
基于三个人的版本号再来排序
可能可以还原出一个正确顺序
这个我后面找机会我们再来讲的一个事情
大家也可以想一想
其实是有专门一篇论文去
解决这个问题的
相对来说复杂的多
就是朋友圈这个做法其实还不是最佳解
但是它的场景里面我觉得是比较取巧
并且是比较好实现的一个做法
讲完这么多
我们再来看一下我们当时自己
在做账号的一个多活体系是怎么去演进的
我们在做多活的时候
一定避免陷入一个思维误区
我要保证所有的业务异地多活
其实很难做到
我们举个例子
比方说我们做一个业务层
你拆成了三个Zone A中心 B中心 C中心
可能是一个注册业务
A中心注册一个用户
那么可能数据还没同步到B中心
这个时候如果A中心宕机了
为了支持注册业务多活
你会把流量切到B中心去
让用户重新注册
但是看上去很容易就支持做活
但是你仔细思考一下
如果一个手机号只能注册一个账号
如果A中心的数据还没有同步过来
B中心无法判断这个手机号是否重复
那么B中心如果让用户注册
后来A中心回复了
发现数据冲突 你觉得好解决吗
实际上是无法解决的
因为两个账号分配的UID都不一样
你不可能还去把他们两个UID进行账号数据合并
这个其实是非常复杂的
所以注册账号不能说挑选最后一个生效的
所以如果B中心
不支持本来属于A中心的用户进行注册
似乎注册业务又变成了空谈
就好像这个账号多活怎么做
所以账号你就不要想着它是一个多活系统
它就是一个Global Zone
它是一个无法分割的服务或者是数据
所以你不要设想所有业务多活
那么我们再想一下业务场景
你请问是注册多 还是登录多 还是鉴权多
是不是鉴权最多
登录其次 注册最少
那么我们其实要做的是什么
登录和鉴权多活
我们就要做账号多活的时候
就要考虑几个
第一 减少数据同步
用户登录的时候它产生了一个token
或者session这种信息
数据量很大
它不需要同步到其他的业务中心
因为这些数据丢了可以重新登录
可以重新生成
我们刚刚讲数据同步的时候讲过几个思路
异常处理
数据是多通道同步
同步和异步访问
日志记录 补偿
我们再看一下数据同步
数据存储是 存储系统同步利用MySQL复制
第二就是说利用消息队列同步
第三是重复生成
那么用户token session
它属于重新登录可以重新生成的
所以这种东西是不是可以考虑不复制
减少数据的复制
其实也是一种好事
所以这个其实是第一个
就是减少数据同步
那么有时候你会想问了
某些情况下可能出现消息队列同步延迟
MySQL复制延迟
用户在A中心注册了
然后访问B中心的业务
结果B中心拿不到用户信息
那么怎么解决
B中心在读取本地失败的时候是不是可以降级
直接回到什么
根据路由规则再去A中心访问一次
对吧
所谓的二次读取
就是第一次先优先读本地
本地读不到
再降级去读对端
所以这个叫二次读取
这就像token 你登录成功以后
或者说你注册以后
你发现注册的信息
在你机房还没同步过来 你无法登录
因为登录需要账号和密码
你同步到机房以后账号密码不存在
你是不是降级回你的中心机房
看一下是不是登录成功
另外就是说对于登录的session数据
由于数据量很大
我们可以不同步数据
用户A在中心登录以后
可能又在B中心
流量路由的漂到了B中心
然后你发现B中心用户的session id查不到
session id内部是有一些特殊的特征
比方某两位标记是哪在哪个机房的
你是不是也可以二次读取
你流量才能到B机房的
可以把你路由到正确的机房去
那么A中心也可以到B中心
去拿session数据去登录
反过来也是一样的
这样的话尽量去避免一些数据同步
因为可能存在一些异常case
但是你可以兜底去处理掉
好我们再看一下最终一致性
那么A机房注册一个用户
业务上不要求能够
几十毫秒内就全部同步成功
因为正常情况下可能几秒钟
甚至几分钟同步可能就够了
所以最终一致性在具体的体现时候
还是要根据不同的数据特征
来进行差异化的处理
我就像我刚刚说的
你账号信息可能注册成功以后
突然来B机房访问不到
你就可以路由回核心机房去查
还有一个问题就是用户信息
你可能是单中心写的
我们所谓的Global Zone单写
但是你可能复制到A B两个机房
从中心机房拿到原始数据复制有些延迟
用户玩游戏的时候登进去看到的是
一个过期的用户信息
有影响吗
怎么解决
老实话 它还是会产生用户体验上的一些影响
可能过几秒钟用户昵称就正确了
无法彻底解决这个问题
只能说牺牲一定的用户体验
这个其实是没办法的
你不要想所有机房全部是强一致
基于这两个思路
我们当时各个云厂商阿里云
腾讯云各种云厂商都会部署不同的游戏
我们当时也是遇到个问题
我们上海的机房的账号系统
避免云厂商来直接跨机房调用这些登录
注册肯定还是在我们核心机房注册
但是像登录 鉴权
我们避免说跨机房调用
我们当时就是把账号信息同步到各个云厂商
当然这些账号或者密码信息
都做了一些混淆的处理
就不是跟原始的这边的架构一样
避免说要出现一些泄露
同时我们的登陆的这种token也是
就直接存在本地的云厂商的上面
我们一般会让游戏正确路由到云机房里面去
那么它的鉴权的判定
其实就是在本地的一个机房鉴权
那么当然我也支持我们刚才说的二次读取
漂错的机房的话
也可以把它漂回来
漂到正确的一个机房里面去查session
然后我们也支持
像用户的登录密码错误
我们可能降级去回核心机房
因为改密码其实是个低频需求
或者说用户的信息注册还查不到
我们可能就要回核心机房
所以这个就是我们当时做完鉴权
或者是登录是做了个多活
这套上完之后
其实这种专线故障 网络故障
对我们游戏的业务影响就变得非常小
所以你看我们在设计上就做了一些所谓的
trade off做的一些取舍
这个是第一块
我们再看一下账号的一个稿件多活
那么这个就更简单了
因为我们刚刚讲过我们整个稿件体系
它其实就是一个典型的Global Zone
就是单写
我们最终做法就是
让我们的核心机房去负责写
那这样就有个问题
就是我们的slave在别的机房
它的数据肯定是有延迟的
还有一个问题
因为我们默认两个应用是要承担100%流量的
各承担50%
我的数据复制过来的时候
我们还是要涉及到缓存的预热和刷新的问题
我们当时的做法是比较简单
就是利用MySQL的复制来做的
然后MySQL复制以后
我们又通过订阅从库的binlog
来把这个缓存给刷新掉
注意我们是通过订阅binlog的
就是通过订阅从库的binlog
把缓存刷新掉和预热的
因为写操作是在这边
但是我的异步复制
我是这样来预热缓存的
它是需要全量的数据 都热的
它是长尾的访问请求的
所以你看不同的系统做法其实是会有些区别
我们可能为什么没有那种架构
写操作是到这个机房
我们为什么不直接把它转发到master
因为知道要识别这写操作
并且路由到正确的master DB
我们是要做所谓的Data Access Layer
可能要做一个中间件或者这样一个SDK来识别
它是个写库写操作
然后把它路由到正确的主库上
这样的一个操作
第一对我们的代码有些侵入
或者说基础库有些改造
第二就是说虽然你这么做了
但是你得参考我们刚才讲
Facebook的cache同步一致性的做法
所以对我们当时的业务改造
其实有些大
我们的做法是在CDN
在我们的边缘CDN发现他是接口是个写操作
就完全把他无脑的扔到这个机房去
那就不在我们的应用层再把流量漂过来
然后再把DB写再转发到这边了
而是在接口层面在CDN层面就把他路由过来了
这样的坏处可能需要精细化的配接口
哪些接口去哪个机房
这其实是有些运维工作量的
更长远的一些解决方案 我也在思考
我觉得当时Facebook
TAO存储就是他的图存储
里面有些思路值得我们借鉴
可能对中间件就有一些要求
大家就期待我后续的一些分享
或者说这些有更好的方案的话 我也会提出来
所以你看像苏宁的多活
我们看到它虽然有这种同城的做法
APP直接把写流量写的主DB
但是他没有重点去提他的cache一致性怎么保证
因为你想象一下你这边一个写操作过去
这边的本地的缓存怎么办
你是不是又要通过Facebook的
这个办法去解决访问Memcached的一致性的问题
这其实都是有些工作量的
我们的做法其实也是取巧
就是在接口层面直接把它路由到一个正确的地方
那么这边因为你对于稿件的一个写操作来说
它到审核再到上线可播其实需要很长的时间
对UP主来说他是有预期的
那么对于观看者来说
他其实也是有预期的
所以这个地方数据延迟是没关系
但是我们不能让UP主上传稿件的时候
投稿写节操作和cache这个不一致
肯定是不能出现这个状态不一致的
这个其实就是我今天整个多活系统的一些介绍了
我们reference其实包含的内容非常多
我把各个厂商的一些做法都放进来了
包括还有很多外面别人写的一些思路
我建议大家都认真去看一看
其实你搞清楚了DNS CDN
它就是为你的流量路由调度来服务的
那么你再搞清楚多活
核心就是Sharding Key按什么业务去划分
最后再搞清你的核心业务场景
以及数据同步应该怎么做
以及我们应该在每一层做一些流量保护
比方说在API Gateway要做流量保护路由
避免一些意外的流量进来
然后路由到正确机房
那么在CDN可能也要做这个事情
Edge CDN要做这个事情
API Gateway要做这个事情
在我们数据库的DAL写操作层里
可能也要兜底去做这个事情
整个多活其实成本还是非常高的
你像我们这个做法其实花的时间是非常短的
可能改一两个星期就上线了
能满足我们的业务需求
写单写 但是读多活
通过Global Zone加CZone
这样的话
其实也是可以多个机房同时提供读服务的
我会倾向说我们的评论弹幕
有点像微信的朋友圈这种做法
就是本地读 本地写
但是要搞定因果一致性
所以后续也可以再看我的后续的一些分享
今天整个第11周的课程就到这里了
好 感谢大家