近几年,随着移动互联网的发展、云计算的普及和各种新业务的出现,数据呈现爆发式增长,给整个业务系统带来了越来越大的挑战,特别是对于底层数据存储系统。完美的高可用系统,是所有公司最理想的追求。如果只从应用层和缓存层看高可用问题,是比较容易解决的。对于应用层来说,根据业务特点可以很方便地设计成无状态的服务,在大多数互联网公司中,在业务层的最上层使用动态DNS、LVS、HAProxy等负载均衡组件,配合Docker和Kubernetes实现弹性伸缩,能够很容易实现应用服务的高可用。对于缓存层来说,也有很多可选的开源方案来帮助解决,比如Codis、Twemproxy、Redis Cluster等等,如果对缓存数据的一致性和实时性要求不高,这些方案就可以很好解决缓存层面的问题。但对存储层来说,支持高可用非常困难。
在互联网架构中,最底层的核心数据存储一般都会选择关系型数据库,最流行的当属MySQL。大数据时代,大家渐渐发现传统的关系型数据库开始出现一些瓶颈:单机容量不能支撑快速增长的业务需求;高并发的频繁访问经常造成服务的响应超时;主从数据同步带来的数据不一致问题;大数据场景下查询性能大幅波动等等。
当前,数据库方案有了很多不一样的变化。首先,不同于早期的单机型数据库,在当下数据呈现爆发式增长,数据总量也从GB级别跨越到了TB甚至PB级别,远超单机数据库的存储上限,所以只能选择分布式的数据存储方案。其次,随着存储节点的增加,存储节点出问题的可能性也大大提高,光靠人工完全不现实,所以需要数据库层面保证自己高效快速地实现故障迁移。另外,随着存储节点的增加,运维成本也大大增加,对自动化工具也提出了更高要求。最后,新分布式数据库的出现,用户在OLTP数据库基本需求的基础上,对大数据分析查询的业务要求更高,在某种程度上OLTP和OLAP融合的新型数据库会是未来极具潜力的发展方向之一。
什么是高可用
Wikipedia的解释中,高可用即High Availability,一般通过SLA(Service Level Agrement)来衡量。这里从CAP角度来看待高可用问题。CAP是分布式系统领域一个非常著名的理论,由Berkerly的Brewer提出。该理论认为任何基于网络的分布式系统都具有以下三要素:
数据一致性(Consistence):等同于所有节点访问同一份最新的数据副本;
可用性(Availability):对数据更新具备高可用性;
分区容忍性(Partition tolerance):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A间做出选择。
三要素不能同时满足。但后来很多人将CAP解读为数据一致性、可用性和分区容忍性最多只能满足两个,这种解读本身存在一定的误导性,原因就在于忽略了特定条件。假想两个节点N1和N2,在某些场景下发生了分区(P)问题,即N1和N2分处分区的两侧。这时对于外部的写操作来说,如果允许任一节点可写的话就相当于选择了A,丧失了C。同样,如果为了满足C,那么写入操作就会失败,A就无法保证,所以存在分区问题时,无法同时保证A和C。虽然分区在局域网中出现的概率相对很低,但却无法避免,所以系统只能在CP和AP之间做出权衡。
当前有很多的NoSQL数据库,在CAP之间选择了AP,比如Amazon Dynamo和Cassandra,追求可用性,适当牺牲一致性,只实现最终一致性。这种选择允许短时间的数据不一致,并且可以交由用户自己来处理写入冲突,但是可以随时接受用户的读写请求。在这种场景下就需要特别注意数据不一致引起的各种奇怪问题,对于比较严肃的业务场景,比如订单、支付等,对事务和一致性要求比较高,这种AP类型的系统就不适用了。而且该系统放弃了SQL和ACID事务,给开发人员带来了更多的开发工作和额外的心智负担,很容易出现问题,所以NoSQL数据库牺牲一致性来获取服务的可用性,并没有彻底解决大数据时代数据库的高可用问题。
大数据时代,传统的关系型数据库必然会由单机扩展到分布式,追求数据一致性,所以必然会是一个CP类型的系统,像这种新型的、下一代的分布式关系型数据库,既具有传统单机数据库的SQL支持和ACID事务保证,又有NoSQL数据库的Scale特点,称为NewSQL数据库,包括Google的Spanner/F1、PingCAP的TiDB等等。但从CAP的角度看,选择CP并不意味着完全放弃了A,CP系统只是在某些产生分区的场景下不能实现100%的A,但完全可以通过有效的办法来实现高可用(HA)。由此可见,并不是CP系统就完全放弃了A,只不过在产生分区的场景下无法从理论上保证A,这是一个常见的误解。
澄清了CAP的问题,下面讨论如何打造高可用的数据库。数据库是一个非常大的概念,从传统单机SQL,到NoSQL,再到现在流行的NewSQL,这里面不同的实现方案实在太多,本文聚焦在关系型数据库,主要探讨最流行的MySQL数据库及其生态。最近几年,随着大家在分布式数据库领域的探索,出现了很多不同类型的解决方案,比如中间件/Proxy的方案,典型的比如TDDL、Cobar、Altlas、DRDS、TDSQL、MyCAT、KingShard、Vitess、PhxSQL等,还有一种新型的NewSQL数据库,比如Google Spanner/F1、Oceanbase、TiDB等。下面看下业界在打造高可用数据库方面新的技术进展,以及和传统方案选型的对比。
消除单点问题
为了实现数据库层面的高可用,必须要消除单点问题(SPOF)。存在单点服务的情况下,一旦单点服务挂掉,整个服务就不可用。消除单点问题最常用的方案就是复制(Replication),通过数据冗余的方式来实现高可用。
为什么必须要冗余?数据库本身是有状态的,不会像无状态的服务那样挂掉就可以重启,而数据库本身能够保证数据持久化,所以如果没有冗余副本,一旦数据库挂掉,只能等待数据库重启,在这段恢复时间服务完全不可用,高可用就无法保证。但如果有了额外的数据副本,高可用就变得可能了,主要能保证在检测到服务发生问题之后及时做服务切换。
对于MySQL来说,默认复制方式是异步的主从复制方式,虽然这种方案被很多的互联网公司所采用,但实际上这种方案存在一个致命问题——存在丢失数据的风险。数据传输经过网络,这也就意味着存在传输时延,那么对于异步复制来说,主从数据库的数据本身是最终一致性的,所以主库一旦出现了问题,切换从库极有可能会带来数据不一致的风险。
因为异步复制方式存在更大的问题,很多时候大家都会考虑用半同步复制方式Semi-Sync,这种数据复制方式在默认情况下会使用同步的数据复制方式,不过在数据复制压力较大的情况下,就会退化成异步的数据复制方式,所以依然会存在高可用问题。当然,也有人会选用完全同步的方式,但是这种复制方式在并发压力下会有明显的性能问题,所以也不常用。
那有没有一种数据复制方式,能同时保证数据的可靠性和性能?答案是有的,那就是最近业界讨论较多的分布式一致性算法,典型的是Paxos和Raft。简单来说,它们是高度自动化、强一致的复制算法。以Raft为例,Raft中基数个节点组成一个Raft Group,在一个Raft Group内,只要满足大多数节点写成功,就认为可以写成功了,比如一个3节点的Raft Group,只要保证Raft Leader和任意一个Raft Follower写成功就可以了,所以同步写Leader,异步写两个Follower,只要其中一个返回就可以,相比完全的同步方式,性能要好很多。所以从复制层面来看,Raft更像是一个自适应的同步+异步复制方案,同步和异步的最优选择通过Raft算法来保证。
庆幸的是,业界早已意识到这个问题,从最开始的Galera Cluster探索到前段时间微信开源的PhxSQL,再到最新MySQL官方发布的MRG(MySQL Group Replication),还有我们从0到1打造的开源分布式数据库TiDB,都在这方面进行了探索。大家的出发点基本相同,采用新的分布式一致性来替换传统的Master-Slave复制方式,不同的仅仅是大家选择的协议:TiDB选择了Raft,而PhxSQL和MRG选择了Paxos。
由此看出,新一代高可用的数据库必然会使用分布式一致性算法来实现数据复制,这已是业界的趋势。
自动故障恢复
有了数据复制,理论上来说,在一个数据库节点出现问题时就不用那么慌张,毕竟还有额外的数据副本存在。所以下面要做的就是尽早发现服务故障并快速恢复,也就是常说的Auto-failover。
从这个层面来看,目前基于主从的数据库复制方案基本上无法脱离运维,使用中间件/Proxy方案更会增加难度,毕竟人力运维是有上限的,所以选择这种方案,人力成本也是一个需要考虑的问题。Google之前在广告业务中也是使用的MySQL中间件方案,大约100个节点的规模,在这个量级下维护的复杂度和成本非常高。所以Google要做一个真正替换MySQL中间件的理想方案,这就有了后来的Google Spanner/F1,包括后来的TiDB,都采用了这种新的NewSQL架构,唯一不同的是,Google选择了Paxos,而TiDB选择了Raft。这种分布式一致性算法,除了提供优雅的复制方案,还可以提供高效的Auto-failover支持。
要想实现Auto-failover,首先需尽快检测到Fail情况。常用方式是通过LVS或者HAProxy之类的负载均衡组件,或者通过类似的Monitor进行远程监控,但对于网络来说,存在三种不同的状态:Success/Failure/Timeout,因为存在Timeout,Monitor的监控不完全准确,而且Monitor本身也会存在高可用问题,所以外部监控不一定完全靠谱,这也是需要考虑的问题。但是以分布式一致性算法Raft为例,Raft内部维护Raft Group,正常情况下都是Leader提供数据读写服务,当Leader出现问题时会自动从Follower中选择新的Leader出来。Raft通过内部的心跳来感知不同节点的状态,并且直接完成Auto-failover,所以Raft是高度自动化并且可以自恢复的。相比于检测再处理的算法,这种基于分布式一致性算法的Auto-failover能力更强,效率更高,当然速度也更快,基本上在秒级别就可以完成Leader更新,继续提供服务,而且是完全自动化的。
关于Auto-failover还有一个引申的跨数据中心多活问题。这基本上是所有分布式系统开发者心中的圣杯,金融级别的数据可用性和安全性。目前从纯软件方案来看,基本没有靠谱的方案,大多数人所谓的异地多活方案实际上底层仍是同步热备,而且很难在保证延迟的情况下同时保持一致性,但是基于Paxos/Raft的方案给多活提供了新的可能性。还是以Raft为例,只要一个Raft Group内的大多数节点复制成功,并在物理节点层面按照特定的方式部署,就可以在软件层面构建一个两地三中心的方案。举个例子,如果这个Raft Group内有三个节点,分别在北京、天津和上海的三个数据中心,对于传统的强一致方案,一个在北京发起的写入需要等待天津和上海的数据中心复制完毕,才能给客户端返回成功,但是对于Raft这样的算法,延迟仅仅在北京和天津数据中心之间,相比传统方案大大降低了延迟。虽然对于带宽的要求仍然很高,但这是未来在数据库层面上实现跨数据中心多活的一个趋势和可行方向,实际上Google分别位于美国西海岸、东海岸以及中部的Spanner数据中心,已经做到了跨地域的数据高可用。真正实现跨数据中心多活,就不用担心挖断光纤导致服务不可用之类的问题了。
在线扩容
随着数据库的数据量越来越大,Scale是不可避免的问题。对于数据库来说,技术层面最大的追求就在于如何不停服务地对数据库节点进行Scale操作,这是非常有挑战性的事情。以中间件/Proxy方案来说,很多时候不得不提前对数据量进行规划,把扩容作为重要的计划来做,从DBA到运维到测试到开发人员,很早之前就要做相关的准备工作,真正扩容时为了保证数据安全,经常会选择停服务来保证没有新的数据写入,新的实例数据同步后还要做数据的一致性校验。当然业界大公司有足够雄厚的技术实力,可以采用更自动化的方案,将扩容停机时间尽量缩短(但很难缩减到0),但大部分中小互联网公司和传统企业依然无法避免较长时间的停服务问题。TiDB完全实现了在线的弹性扩容,主要基于Placement Driver的调度和Raft算法。
Placement Driver是TiDB核心组件之一,时刻监控整个系统的状态,包括每个机器的负载和容量等。当加入一个新的节点时,它会感知到这个事件,并会触发其他负载较高的节点进行Balance操作,通过Raft算法的Config Change和Leader Transfer操作来让整个系统的负载平衡。对用户来说,有了这个特性体验会非常好。如果是电商用户,那么在促销活动之前(比如双11),提前增加数据库节点就可以支撑更高的业务压力,而当活动过后又可以移除掉多余的节点,又可以收缩回来,整个弹性伸缩过程非常平滑,基本就是几个简单的操作,其它一切都是高度自动化的,使用成本特别低。
当然这里面还有一个影响高可用的因素,就是对于一个Paxos或者Raft Group来说,如果数据量太大,在数据Balance或者Recover时就会有很长的数据传输和更新时间,所以将数据在线切分成比较小的数据块是不可或缺的操作,也就是常说的分裂(Split)操作。其中最困难的在于如何保证Split操作的原子性,并且让路由不一致的时间窗口尽可能缩短。TiDB完整实现了在线Split操作,内部处理了路由更新的重试操作,所以对于应用层来说基本上无感知。
在线表结构变更
数据量较大时,数据库的DDL操作也是一个需要注意的高可用问题。以常见的Add Column操作为例,在表规模很大的情况下通常会造成数据库锁表,导致数据库服务不可用。对于中间件/Proxy方案来说,因为依托于底层的单机MySQL数据库提供DDL支持,所以很难从根本上解决,只能依赖于第三方工具,比如Facebook和Percona的方案,当然这些方案也有本身的局限性。最近业界有了更好的进展,比如Github数据团队的方案gh-ost,处理表级别的Binlog,将原表的数据同步到新的临时表中,当数据追平时再进行一个数据库操作,将临时表命名为原表,这样一个AddColumn操作就完成了。这种方案依然要引入额外的组件,除了学习成本之外,也要考虑额外组件的高可用问题。但实际上Google的F1给我们提供了更好的实现参考,TiDB即是根据F1启发进行的研发,简单来说,就是通过把TiDB中DDL操作的状态设定为前向兼容的几个不同状态,中间严格保证不能跨越两个状态。为什么这样?因为整个TiDB集群是分布式的,没有办法把DDL操作实时通知给所有的TiDB节点,就会出现部分TiDB节点感知到了DDL变化,另一部分TiDB节点还没有感知到的情况,这样就可能导致数据不一致。比如对于一个Add Index的DDL,有一个节点先感知到了,然后对于插入数据就增加了一个Index,但是另外一个节点没有感知到,正好这个节点还有一个删除操作,所以就只把行数据删除了,但Index还留在里面,这样当使用Index查询这行时就会找不到数据。TiDB参考的算法是Google F1中一个非常经典的算法,感兴趣的可以看看这篇文章Online, Asynchronous Schema Change in F1。
大数据时代,新的业务类型和数据爆发式增长,给数据库带来了更大的挑战,新的方案层出不穷。本文主要从几个方面介绍打造高可用数据库的新技术进展,以及和传统技术方案的对比,抛砖引玉,希望能给整个技术社区带来一些参考和帮助。