“异地多活”分布式存储系统设计和实现
2020-04-29叶廷东
李 丹,叶廷东
(广东轻工职业技术学院 信息技术学院, 广州 510300)
0 引言
随着人类进入移动互联网(互联网+)时代,新的业务形态层出不穷,应用程序对后台性能的要求也越来越高。传统的单机、单点、关系型数据库早已不能满足业界需要,因此很多公司把数据存储在分布式NoSql数据库中[1]。 最简单的分布式存储,就是在单一机房,通过多服务器组成一个集群等模式来实现。
然而,为了更进一步缩短用户访问时间,一般让用户就近接入,所以分布式后台服务都跨机房部署,通过接入层的某种路由算法,可以把用户请求分配到就近的机房。这样可以大大缩短数据的传输距离,从而提升了用户体验。
而且,异地部署可以有容灾的好处,如果整个大片区的网络不通或者机房宕机,另外一个片区的机房可以无缝的接管所有的服务请求,从而提高整个后台服务的可靠性[2]。
伴随着后台业务服务的跨机房部署,存储也需要跨机房部署,怎么能使部署在不同机房存储中的数据保持同步,且不影响分布式服务的可用性,是一项巨大的挑战。本文从分布式系统的基础理论入手,分析了分布式存储的特性、应用场景和技术挑战,利用开源的Redis存储, RabbitMQ消息队列等技术,搭建了一个满足最终一致性,可用性和分区容忍性的“异地多活”分布式存储系统。
1 异地多活的概念
异地多活数据中心是现在传统大型数据中心的发展趋势。“异地”相对于同城而言,一般不在同一城域;“多活”是区别于一个数据中心、多个灾备中心的模式,前者是多个数据中心都在运行中,所以称为“多活”, 且互为备份。后者是一个数据中心投入运行,另外一个多个数据中心备份全量数据,平时处于不工作状态,只有在主机房出现故障的时候才会切换到备用机房。冷备份的主要问题是成本高,不跑业务,当主机房出问题的时候,也不一定能成功把业务接管过来[3]。
异地多活主要有如下好处[4]:
1)服务端离用户更近,接入层可以把用户请求路由到离用户最近的机房,减少了网络传输距离,大大提升了用户访问性能和体验。
2)异地快速容灾:如果整个机房(比如整个大区)挂掉或者网络瘫痪,另外一个异地机房能无缝单独提供服务,用户完全无感知,大大提高了服务的可靠性。
2 分布式系统的CAP理论
在分布式的环境下,设计和部署系统时主要考虑下述3个重要的核心系统需求。
1)一致性(Consistency):所有节点在同一时间具有相同的数据。
2)可用性(Availability):保证对每个请求的成功或者失败都有响应。
3)分区容错性(Partition Tolerance):分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。
上述3 个重要的核心系统需求又简称为 CAP需求[5],在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以上三点, 如图1所示,三个核心系统特性没有交叠的区域。即在构建分布式系统的时候,一致性,可用性,分区容忍性,这三者只可以同时选择两样[6]。
图1 CAP理论分布式系统的三特性
不幸的是,分区容错性是实际运营的分布式系统所必需的。设想下,谁能保证系统的各节点永远保持网络联通?一旦网络出现丢包,系统就不可用。而保证分区容错性最基本的要求是数据要跨数据中心存储。所以需要在一致性和可用性中二选其一。
为什么强一致性和高可用性不能同时满足? 假如需要满足强一致性,就需要写入一条数据的时候,扩散到分布式系统里面的每一台机器,每一台机器都回复ACK确认后再给客户端确认,这就是强一致性。如果集群任何一台机器故障了,都回滚数据,对客户端返回失败,因此影响了可用性。如果只满足高可用性,任何一台机器写入成功都返回成功,那么有可能中途因为网络抖动或者其他原因造成了数据不同步,部分客户端独到的仍然是旧数据,因此,无法满足强一致性。
选择一致性,构建的就是强一致性系统,比如符合ACID特性的数据库系统。选择可用性,构建的就是最终一致性系统。前者的特点是数据落地即是一致的,但是可用性不能时时保证,这意思就是,有时系统在忙着保证一致性,无法对外界服务。后者的特点是时时刻刻都保证可用性,用户随时都可以访问,但是各个节点之间会存在不一致的时刻。
需要注意的是最终一致性的系统不是不保证一致性,而是不在保证可用性和分区容错性的同时保证一致性。最终我们还是要在最终一致性的各节点之间处理数据,使他们达到一致[7]。
3 异地多活分布式存储系统的挑战
众所周知,如果数据只是保存在单一节点,就没有一致性的问题;但是单机房存储连最基本的“分区容忍性”就保证不了。而对于“异地多活”系统而言,数据必然是跨数据中心存储的。保存在异地机房NoSql数据库中的数据要做到可用、并且尽可能一致,因为理论上,任何一个机房在任何时刻要给每一个用户提供服务,这就给分布式存储系统带来如下挑战[8]:
1)网络延迟&丢包:异地多活系统由于要跨数据中心存储, 而跨数据中心的路途遥远导致的弱网络质量,数据同步是非常大的挑战;
2)一致性:用户可能几乎同时在两个机房写入数据,怎么保写入的数据不冲突并一致。
4 异地多活分布式存储系统的设计
鉴于异地多活系统的上述挑战,及分布式系统的CAP定理,本文设计的分布式存储系统满足了分区容错性和可用性,实现了最终一致性。
满足分区容错性,分布式数据在异地存储,任何一个机房挂掉或者跨区域网络不通,单机房可以立即提供服务。 对分区错误的容忍性可以达到100%。
满足可用性,分布式存储本地机房写成功后就返回给用户,不等待远端机房是否写成功。
为了满足最终一致性,引入消息中间件进行多地域数据分发,消息中间件可以确保消息不丢失[9]。并对写入的数据附上时间戳,通过时间戳的记录和比较机制,确保两边同时写入的数据不冲突。同时,引入一致性校验和补偿机制,数据最终一致性得到进一步的保证。
4.1 系统框架
如图2 所示,在性能方面,由于redis的卓越表现,选择redis作为数据的承载[10],在一个机房中部署多个redis,组成一个集群,满足一个机房对数据的读写需求。为了解耦业务层和存储redis,在redis之上引入一个proxy层,业务通过proxy,按一定的hash策略访问redis[11]。对于多机房分布高可用方面的需求,在proxy层实现数据在多机房间的互相同步机制,提供最终一致性。在多机房网络通信方面,数据同步以消息的形式发送。为了保证不丢消息,本文选择用RabbitMQ作为中间件发送。
图2 系统架构图
4.2 实现方案
4.2.1 引入中间代理层redisProxy节点
业务进程通过redisProxy读写本地redis。redisProxy对key进行hash,访问其对应的redis。同时,redisProxy节点做到无状态,按组管理,一个组内部署多个redisProxy,组成集群。集群内的redisProxy可以方便地水平扩展,业务系统无感知。
业务进程通过配置文件指定需要访问哪个组的redisProxy。另外,redisProxy节点也对redis的读写情况、访问质量等做统计和监控。
4.2.2 写冲突问题的解决机制
本设计支持对数据的put(写覆盖)操作,而支持对数据的写覆盖,必然带来写冲突的问题,即两个机房同时对同一个Key写入数据,因为时序问题,两个机房最终呈现的结果可能不一致。为了解决此问题,我们对存储在系统中的key,维护一份元数据,目前维护的有key的版本,即key写操作的时间戳[13]。对key的写操作(插入、更新、删除),需要将操作发生的时间和本地记录的key对应的时间戳版本做比较,比版本更新(更晚)的操作将被执行,更旧(更早)的操作将被丢弃。
Redis支持的数据类型比较丰富。除了最基本的string类型,通过上述方法,能够直接支持外,对set、sorted set、hash结构,通过做一些转换,也能够支持。转换方式如下:为set、sorted set、hash结构中存储的每个成员维护一份时间戳版本,对key做写操作时,需要对操作涉及的每个成员做时间戳比较,以决定是执行还是放弃。基于时间戳版本的写操作流程如图3所示。
图3 写操作流程图
时间戳机制能够工作的一个前提是服务器之间同步系统时间。一般线上服务器都有同步系统时间,机器之间系统时间误差一般不超过1 s,为毫秒(ms)级别。这个能满足互联网生产环境对存储系统最终一致性的要求。同时,为了减少时间冲突,对一个key的读写,我们hash到一台机器上执行。
4.2.3 引入第三方消息队列,增强同步消息传递的可靠性
redisProxy需要保证带有时间戳的写操作能够同步到其他组。为了增加同步消息的可靠性,本设计通过引入一个第三方队列来满足对同步的可靠性要求。RabbitMQ是我们本文选定的方案, RabbitMQ实现了高级消息队列协议(AMQP),RabbitMQ消息中间件有着完善的可靠性机制并且使用方便[14]。通过RabbitMQ对同步消息的持久化、集群部署及mirrored queue等机制,实现写操作的可靠同步。
4.2.4 平滑的升级扩容机制
为实现升级扩容时部署一个新的组,我们利用redis的主从同步获取‘旧’的最近未更新的数据,利用RabbitMQ的同步获取‘新’的最近变化的操作。通过两者的结合,使新组的数据与已有组一致。
4.3 系统节点
4.3.1 业务节点(redis client)
App业务进程,由配置文件指定通过哪个组的redisProxy访问存储系统。对redisProxy的访问,通过轮询的方式来均衡负载。为了接口使用方便友好,redis client提供类似于redis的接口。
4.3.2 redisProxy
访问代理层,负责对redis读写访问。对外提供网络协议接口访问存储系统。底层存储用redis,将数据存在内存中。内部对数据按key进行hash分片,每片存储在一个redis中。写操作带上时间戳,通过RabbitMQ同步到其他组。写操作成功发送到RabbitMQ即认为同步成功,返回。因此,状态系统各集群间实现的是最终一致性。
4.3.3 RabbitMQ
第三方消息队列,负责将redisProxy的写操作可靠地同步到其他组。通过配置,将同步队列持久化,防止RabbitMQ重启后消息丢失[15]。一套状态系统内,部署多个RabbitMQ,组成集群,防止RabbitMQ单点失败。配置mirrored queue,使同步队列在集群中有多个镜像,进一步提高可靠性。
4.3.4 redis
4.4 交互流程
4.4.1 读数据流程
读数据流程如图4所示。
图4 读数据流程图
1)业务节点发消息给RedisProxy节点,此消息是基于TCP的网络消息,由业务自定义。
2)RedisProxy节点收到业务读请求后,按照Key Hash到某个Redis本地分片。
3)执行标准的Redis命令,从本地redis读取数据。
4)标准Redis命令的回包。
5)业务收到Redis命令的回复后,返回给业务节点一个回包(此回包也是由业务定义的基于TCP的网络消息)
4.4.2 写数据流程
写数据流程如图5所示。
图5 写数据时序图
1)业务节点发消息给RedisProxy节点,此消息是基于TCP的网络消息,由业务自定义。
2)RedisProxy把写操作请求同步给RabbitMQ;
3)RedisProxy节点收到业务读请求后,按照Key Hash到某个Redis本地分片。
4)步骤4.1、4.2、4.3、4.4为一个事务操作,通过比较操作的时间戳和本地保存的时间戳来决定是否执行本次操作,以避免写冲突,确保两边数据一致。
4.4.3 同步远端数据流
同步远端数据流如图6所示。
1)本地RabbitMQ从远端RabbitMQ收到写同步消息;
2)推送同步消息到本机房的RedisProxy节点;
3)RedisProxy节点处理推送过来的写请求,按key Hash到某个本地Redis分片(步骤3.1); 步骤3.2、3.3、3.4、3.5是一个事务操作,通过比较操作的时间戳和本地保存的时间戳大小来决定是否执行本次操作,以避免写冲突,确保两边数据一致。
图6 同步远端数据
4.5 容灾设计
4.5.1 redisProxy服务器宕机
redisProxy多台机集群化部署提高可用性。如果某台服务器宕机,其redisProxy服务器可以继续提供服务。
4.5.2 RabbitMQ服务器宕机
同步消息队列持久化,同时RabbitMQ在多台机集群化部署,同步消息在集群中有多个镜像。如果某台服务器宕机,集群中的其他RabbitMQ可以继续提供服务。宕机恢复后的RabbitMQ,追赶上其他RabbitMQ后可以继续提供服务。
4.5.3 Redis服务器宕机
当做整个集群不可用,切换到另一个状态系统集群。当机器恢复后,按升级扩容的策略对待,重新部署该组状态系统。等追赶上其他集群后,可开始对外提供服务,将业务流量切换到本集群。
4.5.4 集群机房网络不可用
切换业务流量到其他集群,继续提供服务。等机房恢复后,通过RabbitMQ获取其他集群中存储着的同步消息,本地回放,追赶数据。同步队列处理完,即已追赶上其他集群,此时可将业务流量切换回,对外提供服务。
4.6 升级扩容
如图7所示,升级扩容的步骤如下:
1)在新机房部署redis从库,同步现有机房的数据。
2)在新机房部署RabbitMQ,同步并存储更新操作。
3) 在redis主库中写入一测试数据,测试从库是否同步上。
4) 第三步中的测试数据,如果已同步到从库,将从库提升为主库,同时启动redisProxy,开始回放RabbitMQ中的同步消息。
图7 回放同步消息
4.7 一致性校验和补偿
通过脚本,定期随机抽取一批key,比较在各集群之间的数据是否一致。如发现不一致,人工介入校正,根据业务特性做一致性补偿等措施。
5 模拟性能测试
用C++编写模拟测试代码访问redisProxy对系统进行性能测试,跨机房部署,分别从两个机房对多个key进行读写。用Redis-set、Redis-get 分别表示Redis写、Redis读命令。 通过代码分别统计出redisProxy的平均处理时延, 吞吐量,和数据一致性时延等指标。
环境搭建:异地两个机房 RedisProxy部署在单台服务器,CPU: E5-2620/2.10 GHz,内存: 32G, 操作系统 ubuntu 12.04。异地两个机房,分别部署4个Redis 实例,Key-Value 通过键名hash到不同的redis实例。
实验1:测试系统的吞吐量和系统处理时延。
分别以每秒1个、 100个、 1 000个, 5 000个、10 000个读写不同的key请求, 逐步增大系统的请求数, 观察系统的CPU指标和系统延时。
结果分析: 如图8和图9所示,系统时延和CPU随着请求数增加而缓慢增加。 当请求数接近达到1万/秒时,系统延时和CPU利用率明显增大,所以单redisProxy进程部署的QPS接近1万。
图8 系统处理时延与请求数的关系
图9 CPU利用率与请求数的关系
实验2:在保持系统80% QPS情况下,对某一个key反复进行写操作,写完之后分别在两个机房实时读,记录两个机房不同数据结构数据一致的平均时间差。
结果分析:如图10所示,实验结果可以看出,不同数据结构的数据同步时间有略微差别,但是都在1s以内,可以满足工业级数据同步要求。
图10 系统一致性时间
6 结束语
跨机房的存储设计是业界的难点,根据CAP理论,根本做不到同时满足一致性要求和可用性的系统。本文根据分布式存储应用的特点,选择了满足可用性和最终一致性。 通过多机集群,异地部署等保证可用性;通过开源的可靠消息队列技术和定时一致性校验&补偿技术来确保最终一致性。经过压测,系统基本能满足工业级互联网应用吞吐量和可用性的要求。 NoSql技术、Web应用的规模和使用模式的发展,为分布式存储和相关领域的发展带来了新的契机。近年来,业界开始研究CRDT(Conflict-Free Replicated Data Type)数据结构,CRDT是各种基础数据结构最终一致算法的理论总结,能根据一定的规则自动合并,解决冲突,达到强最终一致的效果[16]。这可以作为本课题的以后研究方向。