由InfoQ的内容品牌《技术团队访谈录·第二季--异地多活数据中心项目的来龙去脉》改编而来。
概述
在当今的互联网业内,不少人对“单元化”这个词已经耳熟能详。很多大型互联网系统,诸如阿里系的淘宝、支付宝、网商银行等,都已经实现了单元化架构,并从中获益匪浅。还有更多的公司,或在规划着自己的系统架构向单元化演进,或已经在单元化的建设过程中。
单元化架构能给系统带来什么样的能力,又会带来哪些额外的成本,该不该决定做单元化,要做的话应该怎么做。本文用蚂蚁金服支付宝系统的单元化架构建设实践,为大家勾勒一下单元化的肢体骨架和细枝末节。
单元化技术是阿里异地多活方案的内部代号,同城多活是第一步,主要解决双11购物的大流量问题,涉及到数据库垂直、水平分库、数据库复制、数据库一致性、应用层多活、接入层流量控制等一系列技术。核心设计思想是从C端用户角度考虑,将用户核心业务的闭环链(比如购物闭环),内聚到一个单元,用户在C端操作的一个完整过程,相关数据都能在一个单元内完成,避免跨单元获取数据。这里的一个单元可以是一个机房,也可以是一个机房内的某一组子系统群。
单元化系统里面是一个能完成所有业务操作的自包含集合,在这个集合中包含了所有业务所需的全部服务,以及分配给这个单元的数据、资源。单元化架构的外在形态是把一个单元作为系统部署的基本单位,在全站所有机房中部署数个单元,每个机房里的单元数目不定,任意一个单元都部署了系统所需的所有的应用,数据则是全量数据按照某种维度划分后的一部分。如下是单元化的部署形态:
在传统的服务化架构下(如下图),服务是分层的,每一层使用不同的分区算法,每一层都有不同数量的节点,上层节点随机选择下层节点。当然这个随机是比较而言的。
传统的服务化架构,为伸缩性设计,上层节点随机选择下层节点
与其不同的是,在单元化架构下,服务虽然分层划分,但每个单元自成一体。按照层次来讲的话,所有层使用相同的分区算法,每一层都有相同数量的节点,上层节点也会访问指定的下层节点。因为他们已经在一起。
单元化架构,为性能和隔离性而设计,上层节点访问指定下层节点
为什么要用单元化
在性能追求和成本限制的情况下,我们需要找到一种合适的方法来满足服务需求。在传统的分布式服务设计,我们考虑的更多是每个服务的可伸缩性,当各个服务独立设计时你就要在每一层进行伸缩性的考虑。这是服务化设计(SOA)流行的原因,我们需要每个服务能够单独水平扩展。
但是在摩尔定律下,随着硬件的不断升级,计算机硬件能力已经越来越强,CPU越来越快,内存越来越大,网络越来越宽。这让我们看到了在单台机器上垂直扩展的机会。尤其是当你遇到一个性能要求和容量增长可以预期的业务,单元化给我们提供另外的机会,让我们可以有效降低资源的使用,提供更高性能的服务。
总体而言,更高性能更低成本是我们的主要目标,而经过单元化改造,我们得以用更少(约二分之一)的机器,获得了比原来更高(接近百倍)的性能。性能的提升很大部分原因在于服务的本地化,而服务的集成部署又进一步降低了资源的使用。
当然除了性能收益,如果你做到了,你会发现还有很多收益,比如更好的隔离性,包括请求隔离和资源隔离,比如更友好的升级,产品可以灰度发布等。
单元化,最初是用来解决双11数据库连接不够的问题提出的一种解决方案。早在 2011 年,支付宝系统就开始对核心数据库做水平拆分,而更早之前,对多个关键业务数据库的垂直拆分就已完成。到了 2013 年时,几乎所有支付宝核心数据库,都完成了水平拆分,拆分维度为用户,拆分为 100 个数据分区。此时系统的部署模式是这样的:
同一个应用的所有节点,都会连接这个业务的所有数据分库,每个分库上部署了若干数据分区。任意一个应用节点都可能接收到来自任意用户的业务请求,然后再根据数据分区规则,访问对应分库的数据。这个架构帮助支付宝系统撑过了 2012 年双 11,却无论如何过不了 2013 年大促了,原因在于数据库连接不够用了。主流的商业数据库,连接都不是共享的,就是说一个事务必须独占一个连接。而连接却又是数据库非常宝贵的资源,不能无限增加。当时的支付宝,面临的问题是不能再对应用集群扩容,因为每加一台机器,就需要在每个数据分库上新增若干连接,而此时几个核心数据库的连接数已经到达上限。应用不能扩容,意味着支付宝系统的容量定格了,不能再有任何业务量增长,别说大促,很可能再过一段时间连日常业务也支撑不了了。如果 OceanBase 在当时已经成熟,可以很好的解决 Sharding 带来的连接数瓶颈问题,因为 OceanBase 是分布式数据库,连接可以共享。然而那时并没有一个合适的经过验证的共享连接数据库产品,因此单元化成为了最好的也是唯一的解决办法。
判断业务是否可以单元化
由上面什么是单元化的问题可以看出,单元化架构要求系统必须具备的一项能力:数据分区,实际上正是数据分区决定了各个单元可承担的业务流量比例。数据分区(shard),即是把全局数据按照某一个维度水平划分开来,每个分区的数据内容互不重叠,这也就是数据库水平拆分所做的事情。仅把数据分区了还不够,单元化的另外一个必要条件是,全站所有业务数据分区所用的拆分维度和拆分规则都必须一样。若是以用户分区数据,那交易、收单、微贷、支付、账务等,全链路业务都应该基于用户维度拆分数据,并且采用一样的规则拆分出同样的分区数。比如,以用户 id 末 2 位作为标识,将每个业务的全量数据都划分为 100 个分区(00-99)。
有了以上两个基础,单元化才可能成为现实。把一个或几个数据分区,部署在某个单元里,这些数据分区占总量数据的比例,就是这个单元能够承担的业务流量比例。选择数据分区维度,是个很重要的问题。一个好的维度,应该:粒度合适。粒度过大,会让流量调配的灵活性和精细度受到制约;粒度过小,会给数据的支撑资源、访问逻辑带来负担。足够平均。按这个维度划分后,每个分区的数据量应该几乎一致。以用户为服务主体的系统(To C),比如支付宝,通常可以按照用户维度对数据分区,这是一个最佳实践。
如何实施单元化
实际中,单元化很难有实施到完美的模型,因为数据这里,就存在很多分类的数据,不能满足完美的单元化。而数据分类问题,也是影响单元化最后质量好坏的关键。
1、数据归类
多数据中心系统数据需要解决3类数据的问题:业务层可分区数据、高频全局数据、低频全局数据。” ,数据分类是单元化的设计前提,因为各种类型的数据,在数据复制和垂直、水平设计时,同步方案不一样。
可分区数据
能够按照选择好的维度进行分区的数据,真正能被单元化的数据。这类数据一般在系统业务链路中处于核心位置,单元化建设最重要的目标实际上就是把这些数据处理好。好比订单数据、支付流水数据、帐户数据等,都属于这一类型。这类数据在系统中的占比越高,总体单元化的程度就越高,若是系统中所有都是这样的数据,那咱们就能打造一个完美单元化的架构。不过现实中这种状况存在的可能性几乎为零,由于下面提到的两类数据,或多或少都会存在于系统当中。
全局数据,不被关键链路业务频繁访问
不能被分区的数据,全局只能有一份。比较典型的是一些配置类数据,它们可能会被关键链路业务访问,但并不频繁,所以即便访问速度不够快,也不会对业务性能形成太大的影响。由于不能分区,这类数据不能被部署在经典的单元中,必须创造一种非典型单元用以承载它们。
全局数据,须要被关键链路业务频繁访问
乍看与上面一类类似,但二者有一个显著的区别,便是否会被关键链路业务频繁访问。若是系统不追求异地部署,那么这个区别不会产生什么影响;但若是但愿经过单元化得到多地多活的能力,这仅有的一点儿不一样,会让对这两类数据的处理方式大相径庭,后者所要消耗的成本和带来的复杂度都大幅增长。
究其缘由是异地部署所产生的网络时延问题。根据实际测试,在网络施工精细的前提下,相距约 2000 千米的 2 个机房,单向通讯延时大约 20ms 左右,据此推算在国内任意两地部署的机房,之间延时在 30ms 上下。假如一笔业务需要 1 次异地机房的同步调用,就需要至少 60ms 的延时(请求去,响应回)。若是某个不能单元化的数据须要被关键业务频繁访问,而业务的大部分服务都部署在异地单元中,网络耗时 60ms 的调用在一笔业务中可能有个几十次,这就是说有可能用户点击一个按钮后,要等待数秒甚至数十秒,系统的服务性能被大幅拉低。
这类数据的典型代表是会员数据,对于支付宝这类 To C 的系统来说,几乎所有的业务都需要使用到会员信息,而会员数据却又是公共的。因为业务必然是双边的,会员数据是不能以用户维度分区的。支付宝的单元化架构中,把单元称之为 “zone”,并且为上面所说的3类数据分别设计了三种不同类型的 zone:
RZone(Region Zone):最符合理论上单元定义的 zone,每个 RZone 都是自包含的,拥有自己的数据,能完成所有业务。
GZone(Global Zone):部署了不可拆分的数据和服务,这些数据或服务可能会被RZone依赖。GZone 在全局只有一组,数据仅有一份。
CZone(City Zone):同样部署了不可拆分的数据和服务,也会被 RZone 依赖。跟 GZone 不同的是,CZone 中的数据或服务会被 RZone 频繁访问,每一笔业务至少会访问一次;而 GZone 被 RZone 访问的频率则低的多。
RZone 是成组部署的,组内 A/B 集群互为备份,可随时调整 A/B 之间的流量比例。能够把一组 RZone 部署的任意机房中,包括异地机房,数据随着 zone 一块儿走。
GZone 也是成组部署的,A/B 互备,一样能够调整流量。GZone 只有一组,必须部署在同一个城市中。
CZone 是一种很特殊的 zone,它是为了解决最让人头疼的异地延时问题而诞生的,能够说是支付宝单元化架构的一个创新。CZone 解决这个问题的核心思想是:把数据搬到本地,并基于一个假设:大部分数据被建立(写入)和被使用(读取)之间是有时间差的。
把数据搬到本地:在某个机房建立或更新的公共数据,以增量的方式同步给异地全部机房,而且同步是双向的,也就是说在大多数时间,全部机房里的公共数据库,内容都是同样的。这就使得部署在任何城市的 RZone,均可以在本地访问公共数据,消除了跨地访问的影响。整个过程当中唯一受到异地延时影响的,就只有数据同步,而这影响,也会被下面所说的时间差抹掉。
时间差假设:举例说明,2 个用户分属两个不一样的 RZone,分别部署在两地,用户 A 要给用户 B 做一笔转账,系统处理时必须同时拿到 A 和 B 的会员信息;而 B 是一个刚刚新建的用户,它建立后,其会员信息会进入它所在机房的公共数据库,而后再同步给 A 所在的机房。若是 A 发起转账的时候,B 的信息尚未同步给 A 的机房,这笔业务就会失败。时间差假设就是,对于 80% 以上的公共数据,这种状况不会发生,也就是说 B 的会员信息建立后,过了足够长的时间后,A 才会发起对 B 的转账。
经过对支付宝 RZone 业务的分析发现,时间差假设是成立的,实际上超过 90% 的业务,都对数据被建立和被使用之间的时间间隔要求很低。余下的那些不能忍受时间差的业务(即要求数据被建立后就必须立刻可用,要不就干脆不能访问),则必须进行业务改造,忍受异地访问延时。
2、数据复制
数据复制指不同机房间数据库复制, 数据复制基本会有如下几种方案:
基于mysql的原生复制,做主主架构,两边都开启写入。
基于PXC galera类似的数据库集群方案
基于otter+canal、Apache Pulsar的开源数据同步方案
自研数据复制方案
前两种合适在机房内的数据库复制,不合适地域级跨机房复制,而阿里开源的otter+canal方案是创业企业较常用的跨机房复制方案。
如果将单元化一系列难点作排名,数据库复制是排前3的,因为多活解决的最大难点就是数据复制带来的延迟。目前数据库复制的中间件很多,比如阿里otter+canal方案、Apache Pulsar等。一般的公司如果强调业务的快速迭代,支持用开源的中间件方案实施,当然,熟练使用中间件的成本和代价也很高。如果有研发能力,可以考虑自研实现。数据同步这里,总体上需要基于mysql等存储的私有协议,基于协议读取bin_log数据,然后再使用队列传输到另一个数据中心恢复数据。
参考链接:
http://www.javashuo.com/article/p-cqpxldqd-kt.html
https://www.kancloud.cn/infoq/toptech02/47787
https://blog.csdn.net/csdnlijingran/article/details/100544416