“异地多活”是互联网系统的一种高可用部署架构,而“单元化”正是实现异地多活的一个解题思路。说起这个话题,不得不提两个事件:一件是三年多前的往事,另一件就发生今年的杭州云栖大会上。
从“挖光缆”到“剪网线”
2015年5月27日,因市政施工,支付宝杭州某数据中心的光缆被挖断,造成对部分用户服务不可用,时间长达数小时。其实支付宝的单元化架构容灾很早就开始启动了,2015年也基本上成型了。当时由于事发突然,还是碰到很多实际问题,花费了数小时的时间,才在确保用户数据完全正确的前提下,完成切换、恢复服务。虽然数据没有出错,但对于这样体量的公司来说,服务不可用的社会舆论影响也是非常大的。
527这个数字,成为蚂蚁金服全体技术人心中悬着那颗苦胆。我们甚至把技术部门所在办公楼的一个会议室命名为527,把每年的5月27日定为技术日,来时刻警醒自己敬畏技术,不断打磨技术。
经过几年的卧薪尝胆,时间来到2018年9月。云栖大会上,蚂蚁金服发布了“三地五中心金融级高可用方案”。现场部署了一个模拟转账系统,在场观众通过小程序互相不断转账。服务端分布在三个城市的五个数据中心,为了感受更直观,把杭州其中一个数据中心机柜设置在了会场。工作人员当场把杭州两个数据中心的网线剪断,来模拟杭州的城市级灾难。
网线剪断之后,部分用户服务不可用。经过26秒,容灾切换完成,所有受影响的用户全部恢复正常。这个Demo当然只是实际生产系统的一个简化模型,但是其背后的技术是一致的。这几年来,其实每隔几周我们就会在生产环境做一次真实的数据中心断网演习,来不断打磨系统容灾能力。
从大屏幕上可以看到,容灾切换包含了“数据库切换”“缓存容灾切换”“多活规则切换”“中间件切换”“负载均衡切换”“域名解析切换”等多个环节。异地多活架构是一个复杂的系统工程,其包含的技术内涵非常丰富,单场分享实难面面俱到。本场是微服务话题专场,我们也将以应用层的微服务体系作为切入点,一窥异地多活单元化架构的真面目。
去单点之路
任何一个互联网系统发展到一定规模时,都会不可避免地触及到单点瓶颈。“单点”在系统的不同发展阶段有不同的表现形式。提高系统伸缩能力和高可用能力的过程,就是不断与各种层面的单点斗争的过程。
我们不妨以一个生活中最熟悉的场景作为贯穿始终的例子,来推演系统架构从简单到复杂,所遇到的问题。
上图展示的是用支付宝买早餐的情景,当然角色是虚构的。
最早支付宝只是从淘宝剥离的一个小工具系统,处于单体应用时代。这个时候移动支付当然还没出现,我们的例子仅用于帮助分析问题,请忽略这个穿帮漏洞。
假设图中的场景发生在北京,而支付宝系统是部署在杭州的机房。在小王按下“支付”按钮的一瞬间,会发生什么事情呢?
支付请求要从客户端发送到服务端,服务端最终再把结果返回客户端,必然会有一次异地网络往返,耗时大约在数十毫秒的数量级,我们用红色线表示。应用进程内部会发生很多次业务逻辑运算,用绿色圈表示,不涉及网络开销,耗时忽略不计。应用会访问多次数据库,由于都在部署在同一个机房内,每次耗时按一毫秒以下,一笔支付请求按10次数据库访问算(对于支付系统来说并不算多,一笔业务可能涉及到各种数据校验、数据修改)。耗时大头在无可避免的用户到机房物理距离上,系统内部处理耗时很小。
到了服务化时代,一个好的RPC框架追求的是让远程服务调用像调本地方法一样简单。随着服务的拆分、业务的发展,原本进程内部的调用变成了网络调用。由于应用都部署在同一个机房内,业务整体网络耗时仍然在可接受范围内。开发人员一般也不会特别在意这个问题,RPC服务被当成几乎无开销成本地使用,应用的数量也在逐渐膨胀。
服务化解决了应用层的瓶颈,紧接着数据库就成为制约系统扩展的瓶颈。虽然我们本次重点讨论的是服务层,但要讲单元化,数据存储是无论如何绕不开的话题。这里先插播一下分库分表的介绍,作为一个铺垫。
通过引入数据访问中间件,可以实现对应用透明的分库分表。一个比较好的实践是:逻辑拆分先一步到位,物理拆分慢慢进行。以账户表为例,将用户ID的末两位作为分片维度,可以在逻辑上将数据分成100份,一次性拆到100个分表中。这100个分表可以先位于同一个物理库中,随着系统的发展,逐步拆成2个、5个、10个,乃至100个物理库。数据访问中间件会屏蔽表与库的映射关系,应用层不必感知。
解决了应用层和数据库层单点后,物理机房又成为制约系统伸缩能力和高可用能力的最大单点。
要突破单机房的容量限制,最直观的解决办法就是再建新的机房,机房之间通过专线连成同一个内部网络。应用可以部署一部分节点到第二个机房,数据库也可以将主备库交叉部署到不同的机房。
这一阶段,只是解决了机房容量不足的问题,两个机房逻辑上仍是一个整体。日常会存在两部分跨机房调用:
- 服务层逻辑上是无差别的应用节点,每一次RPC调用都有一半的概率跨机房
- 每个特定的数据库主库只能位于一个机房,所以宏观上也一定有一半的数据库访问是跨机房的
同城跨机房专线访问的耗时在数毫秒级,图中用黄色线表示。随着微服务化演进如火如荼,这部分耗时积少成多也很可观。
改进后的同城多机房架构,依靠不同服务注册中心,将应用层逻辑隔离开。只要一笔请求进入一个机房,应用层就一定会在一个机房内处理完。当然,由于数据库主库只在其中一边,所以这个架构仍然不解决一半数据访问跨机房的问题。
这个架构下,只要在入口处调节进入两个机房的请求比例,就可以精确控制两个机房的负载比例。基于这个能力,可以实现全站蓝绿发布。
“两地三中心”是一种在金融系统中广泛应用的跨数据中心扩展与跨地区容灾部署模式,但也存在一些问题。异地灾备机房距离数据库主节点距离过远、访问耗时过长,异地备节点数据又不是强一致的,所以无法直接提供在线服务。
在扩展能力上,由于跨地区的备份中心不承载核心业务,不能解决核心业务跨地区扩展的问题;在成本上,灾备系统仅在容灾时使用,资源利用率低,成本较高;在容灾能力上,由于灾备系统冷备等待,容灾时可用性低,切换风险较大。
小结一下前述几种架构的特点。直到这时,微服务体系本身的变化并不大,无非是部署几套、如何隔离的问题,每套微服务内部仍然是简单的架构。
架构类型 | 优势 | 问题 |
---|---|---|
单体应用 | 网络开销小 | 扩展性差,维护困难 |
单机房服务化 | 解耦,可扩展 | 容量受限,机房级单点 |
同城多机房阶段一 | 突破单机房容量瓶颈 | 非必要的跨机房网络开销大 |
同城多机房阶段二 | 非必要的跨机房网络开销小;机房级容灾能力 | 城市级单点 |
两地三中心 | 异地容灾能力 | 网络耗时与数据一致性难两全 |
蚂蚁金服单元化实践
蚂蚁金服发展单元化架构的原始驱动力,可以概括为两句话:
- 异地多活容灾需求带来的数据访问耗时问题,量变引起质变
- 数据库连接数瓶颈制约了整体水平扩展能力,危急存亡之秋
第一条容易理解,正是前面讨论的问题,传统的两地三中心架构在解决地区级单点问题上效果并不理想,需要有其他思路。但这毕竟也不是很急的事情,真正把单元化之路提到生死攸关的重要性的,是第二条。
到2013年,支付宝核心数据库都已经完成了水平拆分,容量绰绰有余,应用层无状态,也可以随意水平扩展。但是按照当年双十一的业务指标做技术规划的时候,却碰到了一个棘手的问题:Oracle数据库的连接不够用了。
虽然数据库是按用户维度水平拆分的,但是应用层流量是完全随机的。以图中的简化业务链路为例,任意一个核心应用节点C可能访问任意一个数据库节点D,都需要占用数据库连接。连接是数据库非常宝贵的资源,是有上限的。当时的支付宝,面临的问题是不能再对应用集群扩容,因为每增加一台机器,就需要在每个数据分库上新增若干连接,而此时几个核心数据库的连接数已经到达上限。应用不能扩容,意味着支付宝系统的容量定格了,不能再有任何业务量增长。别说大促,可能再过一段时间连日常业务也支撑不了了。
单元化架构基于这样一种设想:如果应用层也能按照数据层相同的拆片维度,把整个请求链路收敛在一组服务器中,从应用层到数据层就可以组成一个封闭的单元。数据库只需要承载本单元的应用节点的请求,大大节省了连接数。“单元”可以作为一个相对独立整体来挪动,甚至可以把部分单元部署到异地去。
单元化有几个重要的设计原则:
- 核心业务必须是可分片的
- 必须保证核心业务的分片是均衡的,比如支付宝用用户ID作分片维度
- 核心业务要尽量自包含,调用要尽量封闭
- 整个系统都要面向逻辑分区设计,而不是物理部署
在实践上,我们推荐先从逻辑上切分若干均等的单元,再根据实际物理条件,把单元分布到物理数据中心。单元均等的好处是更容易做容量规划,可以根据一个单元的压测结果方便换算成整站容量。
我们把单元叫做Regional Zone。例如,数据按100份分片,逻辑上分为5个Regional Zone,每个承载20份数据分片的业务。初期可能是部署成两地三中心(允许多个单元位于同一个数据中心)。随着架构的发展,再整单元搬迁,演化成三地五中心,应用层无需感知物理层面的变化。
回到前面买早餐的例子,小王的ID是12345666,分片号是66,应该属于Regional Zone 04;而张大妈ID是54321233,分片号33,应该属于Regional Zone 02。
应用层会自动识别业务参数上的分片位,将请求发到正确的单元。业务设计上,我们会保证流水号的分片位跟付款用户的分片位保持一致,所以绝大部分微服务调用都会收敛在Regional Zone 04内部。
但是转账操作一定会涉及到两个账户,很可能位于不同的单元。张大妈的账号就刚好位于另一个城市的Regional Zone 02。当支付系统调用账务系统给张大妈的账号加钱的时候,就必须跨单元调用Regional Zone 02的账务服务。图中用红线表示耗时很长(几十毫秒级)的异地访问。
从宏观耗时示意图上就可以比较容易地理解单元化的思想了:单元内高内聚,单元间低耦合,跨单元调用无法避免,但应该尽量限定在少数的服务层调用,把整体耗时控制在可接受的范围内(包括对直接用户体验和对整体吞吐量的影响)。
前面讲的是正常情况下如何“多活”,机房故障情况下就要发挥单元之间的容灾互备作用了。
一个城市整体故障的情况下,应用层流量通过规则的切换,由事先规划好的其他单元接管。
数据层则是依靠自研的基于Paxos协议的分布式数据库OceanBase,自动把对应容灾单元的从节点选举为主节点,实现应用分片和数据分片继续收敛在同一单元的效果。我们之所以规划为“两地三中心”“三地五中心”这样的物理架构,实际上也是跟OceanBase的副本分布策略息息相关的。数据层异地多活,又是另一个宏大的课题了,以后可以专题分享,这里只简略提过。
这样,借助单元化异地多活架构,才能实现开头展示的“26秒完成城市级容灾切换”能力。
关键技术组件
单元化是个复杂的系统工程,需要多个组件协同工作,从上到下涉及到DNS层、反向代理层、网关/WEB层、服务层、数据访问层。
总体指导思想是“多层防线,迷途知返”。每层只要能获取到足够的信息,就尽早将请求转到正确的单元去,如果实在拿不到足够的信息,就靠下一层。
- DNS层照理说感知不到任何业务层的信息,但我们做了一个优化叫“多域名技术”。比如PC端收银台的域名是cashier.alipay.com,在系统已知一个用户数据属于哪个单元的情况下,就让其直接访问一个单独的域名,直接解析到对应的数据中心,避免了下层的跨机房转发。例如上图中的cashiergtj.alipay.com,gtj就是内部一个数据中心的编号。移动端也可以靠下发规则到客户端来实现类似的效果。
- 反向代理层是基于Nginx二次开发的,后端系统在通过参数识别用户所属的单元之后,在Cookie中写入特定的标识。下次请求,反向代理层就可以识别,直接转发到对应的单元。
- 网关/Web层是应用上的第一道防线,是真正可以有业务逻辑的地方。在通用的HTTP拦截器中识别Session中的用户ID字段,如果不是本单元的请求,就 forward到正确的单元。并在Cookie中写入标识,下次请求在反向代理层就可以正确转发。
- 服务层RPC框架和注册中心内置了对单元化能力的支持,可以根据请求参数,透明地找到正确单元的服务提供方。
- 数据访问层是最后的兜底保障,即使前面所有的防线都失败了,一笔请求进入了错误的单元,在访问数据库的时候也一定会去正确的库表,最多耗时变长,但绝对不会访问到错误的数据。
这么多的组件要协同工作,必须共享同一份规则配置信息。必须有一个全局的单元化规则管控中心来管理,并通过一个高效的配置中心下发到分布式环境中的所有节点。
规则的内容比较丰富,描述了城市、机房、逻辑单元的拓扑结构,更重要的是描述了分片ID与逻辑单元之间的映射关系。
服务注册中心内置了单元字段,所有的服务提供者节点都带有“逻辑单元”属性。不同机房的注册中心之间互相同步数据,最终所有服务消费者都知道每个逻辑单元的服务提供者有哪些。RPC框架就可以根据需要选择调用目标。
RPC框架本身是不理解业务逻辑的,要想知道应该调哪个单元的服务,信息只能从业务参数中来。如果是从头设计的框架,可能直接约定某个固定的参数代表分片ID,要求调用者必须传这个参数。但是单元化是在业务已经跑了好多年的情况下的架构改造,不可能让所有存量服务修改接口。要求调用者在调用远程服务之前把分片ID放到ThreadLocal中?这样也很不优雅,违背了RPC框架的透明原则。
于是我们的解决方案是框架定义一个接口,由服务提供方给出一个实现类,描述如何从业务参数中获取分片ID。服务提供方在接口上打注解,告诉框架实现类的路径。框架就可以在执行RPC调用的时候,根据注解的实现,从参数中截出分片ID。再结合全局路由规则中分片ID与逻辑单元之间的映射关系,就知道该选择哪个单元的服务提供方了。
后记
本文着重介绍了蚂蚁金服异地多活单元化架构的原理,以及微服务体系在此架构下的关键技术实现。要在工程层面真正落地单元化,涉及的技术问题远不止此。例如:数据层如何容灾?无法水平拆分的业务如何处理?
蚂蚁技术团队会坚持走技术开放路线,后续还会以不同的形式分享相关话题,也欢迎各位读者留言探讨。
长按关注,获取分布式架构干货
欢迎大家共同打造 SOFAStack https://github.com/alipay