作者丨汤波
来源丨uee.me/cFgQC自 2008 年双 11 以来,在每年双 11 超大规模流量的冲击上,蚂蚁金服都会不断突破现有技术的极限。2010 年双 11 的支付峰值为 2 万笔/分钟,到 2017 年双 11 时这个数字变为了 25.6 万笔/秒。
支付宝海量支付背后最解渴的设计是啥?换句话说,实现支付宝高 TPS 的最关键的设计是啥?
LDC 是啥?LDC 怎么实现异地多活和异地灾备的?
CAP 魔咒到底是啥?P 到底怎么理解?
什么是脑裂?跟 CAP 又是啥关系?
什么是 PAXOS,它解决了啥问题?
PAXOS 和 CAP 啥关系?PAXOS 可以逃脱 CAP 魔咒么?
Oceanbase 能逃脱 CAP 魔咒么?
LDC 和单元化
这句话暗含的是强大的体系设计,分布式系统的挑战就在于整体协同工作(可用性,分区容忍性)和统一(一致性)。
单元化是大型互联网系统的必然选择趋势,举个最最通俗的例子来说明单元化。
我们总是说 TPS 很难提升,确实任何一家互联网公司(比如淘宝、携程、新浪)它的交易 TPS 顶多以十万计量(平均水平),很难往上串了。
这个例子带给我们一些思考:为啥几家互联网公司的 TPS 之和可以那么大,服务的用户数规模也极为吓人,而单个互联网公司的 TPS 却很难提升?
究其本质,每家互联网公司都是一个独立的大型单元,他们各自服务自己的用户互不干扰。
这就是单元化的基本特性,任何一家互联网公司,其想要成倍的扩大自己系统的服务能力,都必然会走向单元化之路。
以淘宝举例,这样之后,就会有很多个淘宝系统分别为不同的用户服务,每个淘宝系统都做到十万 TPS 的话,N 个这样的系统就可以轻松做到 N*十万的 TPS 了。
LDC 实现的关键就在于单元化系统架构设计,所以在蚂蚁内部,LDC 和单元化是不分家的,这也是很多同学比较困扰的地方,看似没啥关系,实则是单元化体系设计成就了 LDC。
系统架构演化史
几乎任何规模的互联网公司,都有自己的系统架构迭代和更新,大致的演化路径都大同小异。
最早一般为了业务快速上线,所有功能都会放到一个应用里,系统架构如下图所示:
随着业务发展,这个矛盾逐渐转变为主要矛盾,因此工程师们采用了以下架构:这是整个公司第一次触碰到分布式,也就是对某个应用进行了水平扩容,它将多个微机的计算能力团结了起来,可以完胜同等价格的中小型机器。
慢慢的,大家发现,应用服务器 CPU 都很正常了,但是还是有很多慢请求,究其原因,是因为单点数据库带来了性能瓶颈。
于是程序员们决定使用主从结构的数据库集群,如下图所示:其中大部分读操作可以直接访问从库,从而减轻主库的压力。然而这种方式还是无法解决写瓶颈,写依旧需要主库来处理,当业务量量级再次增高时,写已经变成刻不容缓的待处理瓶
不同功能的表放到不同的库里,一般对应的是垂直拆分(按照业务功能进行拆分),此时一般还对应了微服务化。
这种方法做到极致基本能支撑 TPS 在万级甚至更高的访问量了。然而随着相同应用扩展的越多,每个数据库的链接数也巨量增长,这让数据库本身的资源成为了瓶颈。
这个问题产生的本质是全量数据无差别的分享了所有的应用资源,比如 A 用户的请求在负载均衡的分配下可能分配到任意一个应用服务器上,因而所有应用全部都要链接 A 用户所在的分库,数据库连接数就变成笛卡尔乘积了。
在本质点说,这种模式的资源隔离性还不够彻底。要解决这个问题,就需要把识别用户分库的逻辑往上层移动,从数据库层移动到路由网关层。
这样一来,从应用服务器 a 进来的来自 A 客户的所有请求必然落库到 DB-A,因此 a 也不用链接其他的数据库实例了,这样一个单元化的雏形就诞生了。
蚂蚁单元化架构实践
蚂蚁支付宝应该是国内最大的支付工具,其在双 11 等活动日当日的支付 TPS 可达几十万级,未来这个数字可能会更大,这决定了蚂蚁单元化架构从容量要求上看必然从单机房走向多机房。
RZone(Region Zone):直译可能有点反而不好理解。实际上就是所有可以分库分表的业务系统整体部署的最小单元。每个 RZone 连上数据库就可以撑起一片天空,把业务跑的溜溜的。
GZone(Global Zone):全局单元,意味着全局只有一份。部署了不可拆分的数据和服务,比如系统配置等。
实际情况下,GZone 异地也会部署,不过仅是用于灾备,同一时刻,只有一地 GZone 进行全局服务。GZone 一般被 RZone 依赖,提供的大部分是读取服务。
CZone(City Zone):顾名思义,这是以城市为单位部署的单元。同样部署了不可拆分的数据和服务,比如用户账号服务,客户信息服务等。理论上 CZone 会被 RZone 以比访问 GZone 高很多的频率进行访问。
CZone 是基于特定的 GZone 场景进行优化的一种单元,它把 GZone 中有些有着”写读时间差现象”的数据和服务进行了的单独部署,这样 RZone 只需要访问本地的 CZone 即可,而不是访问异地的 GZone。
下面我们来根据数据性质的不同来解释支付宝的 CRG 架构。当下几乎所有互联网公司的分库分表规则都是根据用户 ID 来制定的。
再来分析下 GZone,GZone 之所以只能单地部署,是因为其数据要求被所有用户共享,无法分库分表,而多地部署会带来由异地延时引起的不一致。
这时蚂蚁架构师们问了自己一个问题——难道所有数据受不了延时么?这个问题像是打开了新世界的大门,通过对 RZone 已有业务的分析,架构师们发现 80% 甚至更高的场景下,数据更新后都不要求立马被读取到。
也就是上文提到的”写读时间差现象”,那么这就好办了,对于这类数据,我们允许每个地区的 RZone 服务直接访问本地,为了给这些 RZone 提供这些数据的本地访问能力,蚂蚁架构师设计出了 CZone。
在 CZone 的场景下,写请求一般从 GZone 写入公共数据所在库,然后同步到整个 OB 集群,然后由 CZone 提供读取服务。比如支付宝的会员服务就是如此。
支付宝单元化的异地多活和灾备
流量挑拨技术探秘简介
也不知道从哪一天开始,冷备在主备系统里代表了这台备用机器是关闭状态的,只有主服务器挂了之后,备服务器才会被启动。
为了做到每个单元访问哪些用户变成可配置,支付宝要求单元化管理系统具备流量到单元的可配置以及单元到 DB 的可配置能力。
那么对于应该在本 IDC 处理的请求,就直接映射到对应的 RZ 即可,如图箭头 1。
进入后端服务后,理论上如果请求只是读取用户流水型数据,那么一般不会再进行路由了。
然而,对于有些场景来说,A 用户的一个请求可能关联了对 B 用户数据的访问,比如 A 转账给 B,A 扣完钱后要调用账务系统去增加 B 的余额。
这时候就涉及到再次的路由,同样有两个结果:跳转到其他 IDC(如图箭头 3)或是跳转到本 IDC 的其他 RZone(如图箭头 4)。
RZ0* --> a
RZ1* --> b
RZ2* --> c
RZ3* --> d
①目前支付宝默认会按照地域来路由流量,具体的实现承载者是自研的 GLSB(Global Server Load Balancing):
https://developer.alipay.com/article/1889
大家自己搞过网站的化应该知道大部分 DNS 服务商的地址都是靠人去配置的,GLSB 属于动态配置域名的系统,网上也有比较火的类似产品,比如花生壳之类(建过私站的同学应该很熟悉)的。
②好了,到此为止,用户的请求来到了 IDC-1 的 Spanner 集群服务器上,Spanner 从内存中读取到了路由配置,知道了这个请求的主体用户 C 所属的 RZ3* 不再本 IDC,于是直接转到了 IDC-2 进行处理。
③进入 IDC-2 之后,根据流量配比规则,该请求被分配到了 RZ3B 进行处理。
④RZ3B 得到请求后对数据分区 c 进行访问。
⑤处理完毕后原路返回。
大家应该发现问题所在了,如果再来一个这样的请求,岂不是每次都要跨地域进行调用和返回体传递?
确实是存在这样的问题的,对于这种问题,支付宝架构师们决定继续把决策逻辑往用户终端推移。
比如,每个 IDC 机房都会有自己的域名(真实情况可能不是这样命名的):
IDC-1 对应 cashieridc-1.alipay.com
IDC-2 对应 cashieridc-2.alipay.com
支付宝灾备机制
支付宝 LDC 架构下的灾备有三个层次:
同机房单元间灾备
同城机房间灾备
异地机房间灾备
从上节里的图中可以看到每组 RZ 都有 A,B 两个单元,这就是用来做同机房灾备的,并且 AB 之间也是双活双备的。
同城机房间灾备:灾难发生可能性相对更小。这种灾难发生的原因一般是机房电线网线被挖断,或者机房维护人员操作失误导致的。
在这种情况下,就需要人工的制定流量挑拨(切流)方案了。下面我们举例说明这个过程,如下图所示为上海的两个 IDC 机房。整个切流配置过程分两步,首先需要将陷入灾难的机房中 RZone 对应的数据分区的访问权配置进行修改。
那么首先要做的是把数据分区 a,b 对应的访问权从 RZ0 和 RZ1 收回,分配给 RZ2 和 RZ3。
即将(如上图所示为初始映射):
RZ0* --> a
RZ1* --> b
RZ2* --> c
RZ3* --> d
RZ0* --> /
RZ1* --> /
RZ2* --> a
RZ2* --> c
RZ3* --> b
RZ3* --> d
[00-24] --> RZ0A(50%),RZOB(50%)
[25-49] --> RZ1A(50%),RZ1B(50%)
[50-74] --> RZ2A(50%),RZ2B(50%)
[75-99] --> RZ3A(50%),RZ3B(50%)
[00-24] --> RZ2A(50%),RZ2B(50%)
[25-49] --> RZ3A(50%),RZ3B(50%)
[50-74] --> RZ2A(50%),RZ2B(50%)
[75-99] --> RZ3A(50%),RZ3B(50%)
实际情况中,整个过程并不是灾难发生后再去做的,整个切换的流程会以预案配置的形式事先准备好,推送给每个流量挑拨客户端(集成到了所有的服务和 Spanner 中)。
异地机房间灾备:这个基本上跟同城机房间灾备一致(这也是单元化的优点),不再赘述。
蚂蚁单元化架构的 CAP 分析
回顾 CAP
①CAP 的定义
CAP 原则是指任意一个分布式系统,同时最多只能满足其中的两项,而无法同时满足三项。
所谓的分布式系统,说白了就是一件事一个人做的,现在分给好几个人一起干。
我们先简单回顾下 CAP 各个维度的含义:
②CAP 分析方法
先说下 CA 和 P 的关系,如果不考虑 P 的话,系统是可以轻松实现 CA 的。
而 P 并不是一个单独的性质,它代表的是目标分布式系统有没有对网络分区的情况做容错处理。
如果做了处理,就一定是带有 P 的,接下来再考虑分区情况下到底选择了 A 还是 C。所以分析 CAP,建议先确定有没有对分区情况做容错处理。
if( 不存在分区的可能性 || 分区后不影响可用性或一致性 || 有影响但考虑了分区情况-P){
if(可用性分区容忍性-A under P))
return "AP";
else if(一致性分区容忍性-C under P)
return "CP";
}
else{ //分区有影响但没考虑分区情况下的容错
if(具备可用性-A && 具备一致性-C){
return AC;
}
}
水平扩展应用+单数据库实例的 CAP 分析
另外一方面,单体应用由于业务功能复杂,对机器的要求也逐渐变高,普通的微机无法满足这种性能和容量的要求。
所以要拆!还在 IBM 大卖小型商用机的年代,阿里巴巴就提出要以分布式微机替代小型机。
所以我们发现,分布式系统解决的最大的痛点,就是单体单机系统的可用性问题。
要想高可用,必须分布式。一家互联网公司的发展之路上,第一次与分布式相遇应该都是在单体应用的水平扩展上。
也就是同一个应用启动了多个实例,连接着相同的数据库(为了简化问题,先不考虑数据库是否单点),如下图所示:
一方面解决了单点导致的低可用性问题。
另一方面无论这些水平扩展的机器间网络是否出现分区,这些服务器都可以各自提供服务,因为他们之间不需要进行沟通。
然而,这样的系统是没有一致性可言的,想象一下每个实例都可以往数据库 insert 和 update(注意这里还没讨论到事务),那还不乱了套。
单点就像单机一样(本例子中不考虑从库模式),理论上就不叫分布式了,如果一定要分析其 CAP 的话,根据上面的步骤分析过程应该是这样的:
分区容忍性:先看有没有考虑分区容忍性,或者分区后是否会有影响。单台 MySQL 无法构成分区,要么整个系统挂了,要么就活着。
可用性分区容忍性:分区情况下,假设恰好是该节点挂了,系统也就不可用了,所以可用性分区容忍性不满足。
一致性分区容忍性:分区情况下,只要可用,单点单机的最大好处就是一致性可以得到保障。
包括常说的 BASE (最终一致性)方案,其实只是 C 不出色,但最终也是达到一致性的,BASE 在一致性上选择了退让。
想象一下,如果没有数据库层,而是应用自己来保障数据一致性,那么这样的应用之间就涉及到状态的同步和交互了,ZooKeeper 就是这么一个典型的例子。
水平扩展应用+主从数据库集群的CAP分析
现实中,技术人员们也会很快发现这种架构的不合理性——可用性太低了。
写可用性可以通过 Keepalived 这种 HA(高可用)框架来保证主库是活着的,但仔细一想就可以明白,这种方式并没有带来性能上的可用性提升。还好,至少系统不会因为某个实例挂了就都不可用了。
分区容忍性:依旧先看分区容忍性,主从结构的数据库存在节点之间的通信,他们之间需要通过心跳来保证只有一个 Master。
然而一旦发生分区,每个分区会自己选取一个新的 Master,这样就出现了脑裂,常见的主从数据库(MySQL,Oracle 等)并没有自带解决脑裂的方案。所以分区容忍性是没考虑的。
一致性:不考虑分区,由于任意时刻只有一个主库,所以一致性是满足的。
可用性:不考虑分区,HA 机制的存在可以保证可用性,所以可用性显然也是满足的。
还真有尝试通过预先设置规则来解决这种多主库带来的一致性问题的系统,比如 CouchDB,它通过版本管理来支持多库写入,在其仲裁阶段会通过 DBA 配置的仲裁规则(也就是合并规则,比如谁的时间戳最晚谁的生效)进行自动仲裁(自动合并),从而保障最终一致性(BASE),自动规则无法合并的情况则只能依赖人工决策了。
蚂蚁单元化 LDC 架构 CAP 分析
①战胜分区容忍性
在讨论蚂蚁 LDC 架构的 CAP 之前,我们再来想想分区容忍性有啥值得一提的,为啥很多大名鼎鼎的 BASE(最终一致性)体系系统都选择损失实时一致性,而不是丢弃分区容忍性呢?
分区的产生一般有两种情况:
异地部署情况下,异地多活意味着每一地都可能会产生数据写入,而异地之间偶尔的网络延时尖刺(网络延时曲线图陡增)、网络故障都会导致小范围的网络分区产生。
前文也提到过,如果一个分布式系统是部署在一个局域网内的(一个物理机房内),那么个人认为分区的概率极低,即便有复杂的拓扑,也很少会有在同一个机房里出现网络分区的情况。
而异地这个概率会大大增高,所以蚂蚁的三地五中心必须需要思考这样的问题,分区容忍不能丢!
同样的情况还会发生在不同 ISP 的机房之间(想象一下你和朋友组队玩 DOTA,他在电信,你在联通)。
为了应对某一时刻某个机房突发的网络延时尖刺活着间歇性失联,一个好的分布式系统一定能处理好这种情况下的一致性问题。
那么蚂蚁是怎么解决这个问题的呢?我们在上文讨论过,其实 LDC 机房的各个单元都由两部分组成:负责业务逻辑计算的应用服务器和负责数据持久化的数据库。
想必蚂蚁的读者大概猜到下面的讨论重点了——OceanBase(下文简称OB),中国第一款自主研发的分布式数据库,一时间也确实获得了很多光环。
首先,就像 CAP 三角图中指出的,MySQL 是一款满足 AC 但不满足 P 的分布式系统。
试想一下,一个 MySQL 主从结构的数据库集群,当出现分区时,问题分区内的 Slave 会认为主已经挂了,所以自己成为本分区的 Master(脑裂)。
那么如何才能让分布式系统具备分区容忍性呢?按照老惯例,我们从”可用性分区容忍”和”一致性分区容忍”两个方面来讨论:
可用性分区容忍性保障机制:可用性分区容忍的关键在于别让一个事务一来所有节点来完成,这个很简单,别要求所有节点共同同时参与某个事务即可。
一致性分区容忍性保障机制:老实说,都产生分区了,哪还可能获得实时一致性。
换句话说,如何保障仍然只有一个脑呢?下面我们来看下 PAXOS 算法是如何解决脑裂问题的。
Paxos 要求任何一个提议,至少有 (N/2)+1 的系统节点认可,才被认为是可信的,这背后的一个基础理论是少数服从多数。
想象一下,如果多数节点认可后,整个系统宕机了,重启后,仍然可以通过一次投票知道哪个值是合法的(多数节点保留的那个值)。
这样的设定也巧妙的解决了分区情况下的共识问题,因为一旦产生分区,势必最多只有一个分区内的节点数量会大于等于 (N/2)+1。
通过这样的设计就可以巧妙的避开脑裂,当然 MySQL 集群的脑裂问题也是可以通过其他方法来解决的,比如同时 Ping 一个公共的 IP,成功者继续为脑,显然这就又制造了另外一个单点。
这句话越是理解越发觉得诡异,这会让人以为 PAXOS 逃离于 CAP 约束了,所以个人更愿意理解为:PAXOS 是唯一一种保障分布式系统最终一致性的共识算法(所谓共识算法,就是大家都按照这个算法来操作,大家最后的结果一定相同)。
PAXOS 并没有逃离 CAP 魔咒,毕竟达成共识是 (N/2)+1 的节点之间的事,剩下的 (N/2)-1 的节点上的数据还是旧的,这时候仍然是不一致的。
以下摘自维基百科:Paxos is a family of protocols for solving consensus in a network of unreliable processors (that is, processors that may fail).Quorums express the safety (or consistency) properties of Paxos by ensuring at least some surviving processor retains knowledge of the results.
②OceanBase 的 CAP 分析
上文提到过,单元化架构中的成千山万的应用就像是计算器,本身无 CAP 限制,其 CAP 限制下沉到了其数据库层,也就是蚂蚁自研的分布式数据库 OceanBase(本节简称 OB)。
在 OB 体系中,每个数据库实例都具备读写能力,具体是读是写可以动态配置(参考第二部分)。
实际情况下大部分时候,对于某一类数据(固定用户号段的数据)任意时刻只有一个单元会负责写入某个节点,其他节点要么是实时库间同步,要么是异步数据同步。
OB 也采用了 PAXOS 共识协议。实时库间同步的节点(包含自己)个数至少需要 (N/2)+1 个,这样就可以解决分区容忍性问题。
下面我们举个马老师改英文名的例子来说明 OB 设计的精妙之处:假设数据库按照用户 ID 分库分表,马老师的用户 ID 对应的数据段在 [0-9],开始由单元 A 负责数据写入。
这时候发生了分区(比如 A 网络断开了),我们将单元 A 对数据段 [0,9] 的写入权限转交给单元 B(更改映射),马老师这次写对了,为 Jack Ma。
而在网络断开前请求已经进入了 A,写权限转交给单元 B 生效后,A 和 B 同时对 [0,9] 数据段进行写入马老师的英文名。
假如这时候都允许写入的话就会出现不一致,A 单元说我看到马老师设置了 Jason Ma,B 单元说我看到马老师设置了 Jack Ma。
然而这种情况不会发生的,A 提议说我建议把马老师的英文名设置为 Jason Ma 时,发现没人回应它。
同样的,B 提出了将马老师的英文名改成 Jack Ma 后,大部分节点都响应了,所以 B 成功将 Jack Ma 写入了马老师的账号记录。
假如在写权限转交给单元 B 后 A 突然恢复了,也没关系,两笔写请求同时要求获得 (N/2)+1 个节点的事务锁,通过 no-wait 设计,在 B 获得了锁之后,其他争抢该锁的事务都会因为失败而回滚。
分区容忍性:OB 节点之间是有互相通信的(需要相互同步数据),所以存在分区问题,OB 通过仅同步到部分节点来保证可用性。这一点就说明 OB 做了分区容错。
可用性分区容忍性:OB 事务只需要同步到 (N/2)+1 个节点,允许其余的一小半节点分区(宕机、断网等),只要 (N/2)+1 个节点活着就是可用的。
极端情况下,比如 5 个节点分成 3 份(2:2:1),那就确实不可用了,只是这种情况概率比较低。
一致性分区容忍性:分区情况下意味着部分节点失联了,一致性显然是不满足的。但通过共识算法可以保证当下只有一个值是合法的,并且最终会通过节点间的同步达到最终一致性。
结语
个人感觉本文涉及到的知识面确实不少,每个点单独展开都可以讨论半天。回到我们紧扣的主旨来看,双十一海量支付背后技术上大快人心的设计到底是啥?
基于用户分库分表的 RZone 设计。每个用户群独占一个单元给整个系统的容量带来了爆发式增长。
RZone 在网络分区或灾备切换时 OB 的防脑裂设计(PAXOS)。我们知道 RZone 是单脑的(读写都在一个单元对应的库),而网络分区或者灾备时热切换过程中可能会产生多个脑,OB 解决了脑裂情况下的共识问题(PAXOS 算法)。
基于 CZone 的本地读设计。这一点保证了很大一部分有着“写读时间差”现象的公共数据能被高速本地访问。
剩下的那一丢丢不能本地访问只能实时访问 GZone 的公共配置数据,也兴不起什么风,作不了什么浪。
比如用户创建这种 TPS,不会高到哪里去。再比如对于实时库存数据,可以通过“页面展示查询走应用层缓存”+“实际下单时再校验”的方式减少其 GZone 调用量。
而这就是蚂蚁 LDC 的 CRG 架构,相信 54.4 万笔/秒还远没到 LDC 的上限,这个数字可以做到更高。
当然双 11 海量支付的成功不单单是这么一套设计所决定的,还有预热削峰等运营+技术的手段,以及成百上千的兄弟姐妹共同奋战,特此在这向各位双 11 留守同学致敬。
感谢大家的阅读,文中可能存在不足或遗漏之处,欢迎批评指正。
参考文献:
Practice of Cloud System Administration, The: DevOps and SRE Practices for Web Services, Volume 2. Thomas A. Limoncelli, Strata R. Chalup, Christina J. Hogan.
MySQL 5.7 半同步复制技术
https://www.cnblogs.com/zero-gg/p/9057092.html
BASE 理论分析
https://www.jianshu.com/p/f6157118e54b
Keepalived
https://baike.baidu.com/item/Keepalived/10346758?fr=aladdin
PAXOS
https://en.wikipedia.org/wiki/Paxos_(computer_science)
OceanBase 支撑 2135 亿成交额背后的技术原理
https://www.cnblogs.com/antfin/articles/10299396.html
Backup
https://en.wikipedia.org/wiki/Backup