千亿数据的潘多拉魔盒:从分库分表到分布式数据库

引言

近年来,随着国内互联网行业的加速发展,以及摩尔定律的失效,千亿数据的潘多拉魔盒早已打开,传统的开源/商业关系数据库早已遇到了容量的瓶颈。而容量告警则不仅意味着业务发展收到影响,同时对现有系统的稳定性和可用性、可维护性,也带来极大的挑战。

从十年前起,淘宝等公司就遇到这类制约业务发展的技术问题,进而有了 TDDL 框架,2016 年当当网也发起了 Sharding-JDBC 项目,通过包装 JDBC,来屏蔽 MySQL 分库分表的逻辑,让业务系统像使用单机数据库一样方便。

后来,JDBC 封装框架逐渐演变到中间件,在 TDDL 的基础上,淘宝逐渐发展出来了 DRDS,在 Sharding-JDBC 转移到 Apache 和京东数科以后又孵化出来了 Sharding-Proxy,都是以一个虚拟的 MySQL Server 提供更透明和无侵入的客户端接入服务。其他的中间件,像 MyCat 和 DBLE 也方兴未艾。

另一方面,随着 Google 的 Spanner,阿里的 OceanBase 和 PolarDB,AWS 的 Aurora,PingCAP 的 TiDB,Cockroachlabs 的 CockroachDB 等商业或开源的技术作为代表,分布式数据库开始大规模兴起。这些技术试图通过一个直接的分布式数据库来解决上述问题,而不仅仅是类库或中间件,这种增强 MySQL/PGSQL 的间接方式。当然,分布式数据库本身的复杂度,是另外一个话题。

以上种种对于企业来说,都是试图通过采用类似 Apache ShardingSphere 这种分布式的数据库中间件、或者 CockroachDB 这种分布式数据库作为整体解决方案,增强数据库的吞吐能力,保证高可用和实时强一致性的同时,实现线性的水平扩展能力,在一定规模上提升企业信息系统的数据管理上限。本文将从这个整体的发展过程谈起,详细介绍每一个阶段技术的特点、解决的问题,适用的场景,带领大家了解千亿数据的秘密。

数据库的本质

数据库技术是本世纪 60 年代开始兴起的一门信息管理自动化的新兴学科,数据库是数据管理的产物。自从有计算机以来,就有了数据。计算机是用来执行程序的,而程序本身可以简化为“业务逻辑”+“数据存储”,分别对应这计算和状态(这两点很重要)。程序运算的输入参数和输出结果,都可以看做是数据。

计算逻辑需要高速运行,所以有了 CPU 元件。状态数据需要保存,所以有了磁带、磁盘设备。

最开始我们把数据存储在文件里,但明显地,裸文件本身对结构化的数据管理是非常不方便的。我们不能让文件帮我们分析数据格式对不对,也不能让文件对我们的数据进行一些简单的汇总聚合。我们需要一套能帮我们管理系统的底层软件,这就是数据库管理系统,也是我们一般意义上所说的“数据库”。

怎么算是很好的管理了数据呢?一般来说,需要很好的支持事务(Transaction)。

我们一般用 ACID 来解释事务:一个事务本质上有四个特点 ACID:

  • 原子性(Atomicity):一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作
  • 一致性(Consistency):事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态
  • 隔离性(Isolation):在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰
  • 持久性(Durability):一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中(即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态)

IT 行业的硬件随着摩尔定律的发展,CPU 的性能越来越高,磁盘容量也不断增加。

但是我们发现 CPU 执行一个周期只需要纳秒级别(ns,10 的-9 次方),而普通磁盘读取一次数据可能需要 2 毫秒级别(ms,10 的-3 次方)。中间差了 6 个数量级。为了解决这种性能不匹配,所以有了内存设备,作为 CPU(以及 CPU 本身的 L1/L2 缓存)和磁盘中间的性能缓冲,我们把数据从低速设备磁盘,批量加载到稍高速度的设备内存,然后交给超高速的 CPU 执行。(同样地,如果我们把我们的应用程序代码对应为 CPU 要执行的业务逻辑,那么数据库就是对应着磁盘,各种(本地的、分布式的)缓存就对应着内存。)

MySQL 的崛起与困境

从关系数据库讲起

1970 年,IBM 公司的研究员 E.F.Codd 在题为《大型共享数据库数据的关系模型》的论文中提出了数据库的关系模型,为关系数据库技术奠定了理论基础。然后就是商业关系数据库 Oracle 横空出世,随后 DB2,Sybase,SQLServer 等一批商业数据库兴起。

另一方面,从 1990 年代起,伴随着自由软件和开源运动,开源数据库也诞生和发展起来。

以瑞典 MySQL AB 公司开发的 MySQL、开源社区开发的 PostgreSQL 为代表的开源关系数据库系统(RDBMS)逐渐引领了潮流。特别是 2000 年以后,随着互联网的飞速发展,开源免费的 MySQL 数据库以及其高效 MyISAM 引擎、2006 年发布的 InnoDB 引擎逐步赢得人们的青睐,成为最流行的数据库和引擎。

互联网的高速发展,带来了很多的机遇和神话,也带来了很多的泡沫和危机。直接导致了长期采购商业数据库软件的昂贵成本变得不受欢迎,而采用开源免费方案,特别是 LAMP(Linux+Apache+MySQL+PHP)技术体系,则成为了不分大中小规模的各类公司的首选方案。(这一时期,做服务器硬件和操作系统的 Sun System 公司也因为 Java 语言和平台而逐渐发展壮大,并在 2008 年收购 MySQL。)

1999 年,Monty 成立了 MySQL AB 这家公司。2000 年,MySQL 公布了自己的源代码,并采用 GPL(GNU General Public License)许可协议,正式进入开源的世界。
2001 年至 2007 年是 MySQL 开源飞速发展的 7 年,尤其是在 2005 年 10 月发布了一个里程碑式的版本 MySQL5.0。 5.0 版本中加入了存储过程、服务器端游标、触发器、视图、分布式事务(Xa transactions)、查询优化器的显著改进以及其他的一些特性。这也为 MySQL5.0 之后的版本奠定了迈向高性能数据库的发展基础。
2008 年 1 月 16 号 Sun 收购了 MySQL,花了 10 亿美元。之后不久,2009 年 4 月 20 日 Oracle 收购了 Sun 公司。 随之 MySQL 就变成了 Oracle 旗下的一个产品,之后就是我们所熟悉的 MySQL5.5、5.6、5.7 这些版本了。

同时也是借助这个机会,互联网技术行业积累了大量的 MySQL 技术经验和数据库相关研究者。随着 2010 年后 MySQL 与 Sun 被 Oracle 收购,以及 MySQL 本身足够的功能完备性、成熟度和稳定性,各大公司开始去 IOE 化。其中的 O 就是 Oracle 数据库,普遍替换成 MySQL(比如 Alibaba),或者 PostgreSQL(比如 Apple)。

此时 MySQL 发展到了一个全盛时期。

这一时期,MySQL 的分支版本 MariaDB 也开始发展起来。嵌入式的 sqlite 数据库也在各种移动设备和浏览器中成为一个不可或缺的模块(也是现在安装量最大的数据库)。

单机数据库的问题分析

2010 年代以来,互联网技术发展的风起云涌,使得数据规模越来越大,一天数据增量从之前的几十 M,可能变成几百 G,甚至几个 T。这种新的常态,对单机数据库造成了极大的挑战:容量、性能、稳定性、高可用、维护成本,以及如何结合云基础设施。下面我们拿几个具体的场景来详细讲解。

MySQL 单表一般可以存多少数据

我们知道 MySQL 常用的 InnoDB 引擎(支持事务,有行级锁),使用的 B+树的索引结构,同时用固定大小的页来存储,数据表中的数据都是存储在页中的,所以一个页中能存储多少行数据呢?

假设一行数据的大小是 1k,那么一个页(innodb page size 一般为 16k)可以存放 16 行这样的数据。

InnoDB 存储引擎的最小存储单元是页,页可以用于存放数据也可以用于存放键值 + 指针,在 B+ 树中叶子节点存放数据,非叶子节点存放键值 + 指针。

索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而在去数据页中查找到需要的数据。为了跟磁盘 io 的交互次数 2-3 次就能找到一条记录,我们假设树不超过 3 层。

我们假设主键 ID 为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节,我们一个页中能存放多少这样的单元,其实就代表有多少指针,即 16384/14=1170。
那么可以算出一棵高度为 2 的 B+ 树,能存放 1170*16=18720 条这样的数据记录。
根据同样的原理我们可以算出一个高度为 3 的 B+ 树可以存放:1170 x 1170 x 16=21902400 (两千万)条这样的记录。

当然,如果单行数据远小于 1k,这个数量可以增加不少。同理,如果都是大宽表(比如几百个列),有很多 varchar(4096)、text、clob 类型的大文本数据,会导致单行数据平均几百 K,单表容量远小于这个数据。

读写比非常大的情况下怎么提升系统吞吐

互联网业务,特别是一些偏内容方面的系统,比如微博、咨询等,会有非常高的读写比(毕竟消费信息的人远大于创建产生信息的人数),很多情况下会达到几十比 1 的程度。

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。

进一步我们也可以使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。

读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题。 这包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。

虽然我们可以在业务系统的代码配置多个数据源,并根据自己的需要去把程序手动切换到对应的数据主库或某个从库。但是,显然这种机制不够方便,对应用的侵入性较强。

此外,我们也可以简单的封装一个中间层,根据执行的 SQL 是查询,还是插入、修改、删除,来判断,如果是前者就路由到从库执行,如果是后者就路由到主库执行。同时把多个从库使用 HAProxy+LVS 来虚拟成一个库。这样就可以对业务系统透明。但是这种简单粗暴的方式,有一些副作用。

一个典型的例子就是,在一个事务里:

  • 先在主库执行了 insert 操作,插入了一条订单信息,返回了订单 id
  • 然后执行一个 select 操作,但是可能因为读写分离被路由到了从库
  • 但是此时从库还没有来得及从主库同步 binlog,导致从库没有这条记录,所以 select 操作返回空
  • 整个过程就是:同一个事务里插入一条数据,然后查不到这条数据,这就违背了事务性

这样我们就需要继续在这个基础上做一些优化和封装,实现这个中间层中进一步优化这些具体场景的 case。

单库单表数据量过大导致的问题与应对

传统的将数据集中存储至单一数据节点的解决方案,在容量、性能、可用性和运维成本这三方面已经难于满足互联网的海量数据场景。我们在单库单表数据量超过一定容量水位的情况下,索引树层级增加,磁盘 IO 也很可能出现压力,会导致很多问题。

从性能方面来说,由于关系型数据库大多采用 B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。

从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。

从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。

举几个例子:

1、无法执行 DDL,比如添加一列,或者增加索引,都会直接影响线上业务,导致长时间的数据库无响应。

2、无法备份,与上面类似,备份会自动线 lock 数据库的所有表,然后到处数据,量大了就没法执行了。

3、影响性能与稳定性,系统越来越慢,随时可能会出现主库延迟高,主从延迟很高,且不可控,对业务系统有极大的破坏性影响。

当单表数据过大时,我们就需要进行拆分。常见的拆分有几种情况:

垂直分片

  • 垂直拆分(拆库):例如拆分所有订单的数据和产品的数据,变成两个独立的库,这种方式对业务系统有极大的影响,因为数据结构本身发生了变化,SQL 和关联关系也必随之发生了改变。原来一个复杂 SQL 直接把一批订单和相关的产品都查了出来,现在这个 SQL 不能用了,得改写 SQL 和程序。先查询订单库数据,拿到这批订单对应的所有产品 id,再根据产品 id 集合去产品库查询所有的产品信息,最后再业务代码里进行组装。
  • 垂直拆分(拆表):如果单表数据量过大,还可能需要对单表进行拆分。比如一个 200 列的订单主表,拆分成十几个子表:订单表、订单详情表、订单收件信息表、订单支付表、订单产品快照表等等。这个对业务系统的影响有时候可能会大到跟新作一个系统差不多。对于一个高并发的线上生产系统进行改造,就像是给心脑血管做手术,动的愈多,越核心,出现大故障的风险越高。所以,我们一般情况下,尽量少用这种办法。

水平分片

  • 水平拆分(按主键分库分表):水平拆分就是直接对数据进行分片,有分库和分表两个具体方式,但是都只是降低单个节点数据量,但不改变数据本身的结构。这样对业务系统本身的代码逻辑来说,就不需要做特别大的改动,甚至可以基于一些中间件做到透明。比如把一个 10 亿条记录的订单单库单表(orderDB 库 t_order 表)。我们按照用户 id 除以 32 取模,把单库拆分成 32 个库 orderDB_00..31 ;再按订单 id 除以 32 取模,每个库里再拆分成 32 个表 t_order_00..31 。这样一共是 1024 个子表,单个表的数据量就只是 10 万条了。一个查询如果能够直接路由到某个具体的字表,比如 orderDB05.t_order_10 ,那么查询效率就会高很多。一般情况下,如果数据本身的读写压力较大,磁盘 IO 已经成为瓶颈,那么分库比分表要好。分库将数据分散到不同的数据库实例,使用不同的磁盘,从而可以并行提升整个集群的并行数据处理能力。相反的情况下,可以尽量多考虑分表,降低单表的数据量,从而减少单表操作的时间,同时也能在单个数据库上使用并行操作多个表来增加处理能力。
  • 水平拆分(按时间分库分表):很多时候,我们的数据是有时间属性的,所以自然可以按照时间维度来拆分。比如当前数据表和历史数据表,甚至按季度,按月,按天来划分不同的表。这样我们按照时间维度来查询数据时,就可以直接定位到当前的这个子表。更详细的分析参考下一个小节。

虽然数据分片解决了容量问题,以及部分性能、可用性以及单点备份恢复等问题,但分散的架构在获得了收益的同时,也引入了新的问题。

面对如此散乱的分库分表之后的数据,应用开发工程师和数据库管理员对数据库的操作变得异常繁重就是其中的重要挑战之一。他们需要知道数据需要从哪个具体的数据库的分表中获取。

另一个挑战则是,能够正确的运行在单节点数据库中的 SQL,在分片之后的数据库中并不一定能够正确运行。例如,分表导致表名称的修改,或者分页、排序、聚合分组等操作的不正确处理。

跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。 在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。

通过分类处理提升数据管理能力

随着我们对业务系统、对数据本身的进一步了解,我们就会发现,很多数据对质量的要求是不同的。

比如,订单数据,肯定一致性要求最高,不能丢数据。而日志数据和一些计算的中间数据,我们则是可以不要那么高的一致性,丢了不要了,或者从别的地方找回来。

同样地,我们对于同样一张表里的订单数据,也可以采用不同策略,无效订单如果比较多,我们可以定期的清除或者转移(一些交易系统里有 80%以上是的机器下单然后取消的无意义订单,没有人会去查询它,所以可以清理)。

如果没有无效订单,那么我们也可以考虑:

  • 最近一周下单但是未支付的订单,被查询和支付的可能性较大,再长时间的订单,我们可以直接取消掉。
  • 最近 3 个月下单的数据,被在线重复查询和系统统计的可能性最大。
  • 超过 3 个月、3 年以内的数据,查询的可能性非常小,我们可以不提供在线查询。
  • 3 年以上的数据,我们可以直接不提供任何方式的查询。

这样的话,我们就可以采取一定的手段去优化系统:

  • 定义一周内下单但未支付的数据为热数据,同时放到数据库和内存;
  • 定义三个月内的数据为温数据,放到数据库,提供正常的查询操作;
  • 定义 3 个月到 3 年的数据,为冷数据,从数据库删除,归档到一些便宜的磁盘,用压缩的方式(比如 MySQL 的 tokuDB 引擎,可以压缩到几十分之一)存储,用户需要邮件或者提交工单来查询,我们导出后发给用户;
  • 定义 3 年以上的数据为冰数据,备份到磁带之类的介质上,不提供任何查询操作。

我们可以看到,上面都是针对一些具体场景的 case,来分析和给出解决办法。我们知道没有能解决一切问题的银弹,那么通过在各种不同的场景下,都对现有的技术和手段进行一些补充,我们就会逐渐得到一个复杂的技术体系。

MySQL 的开源生态体系

MySQL 的整体架构分为如下几个部分:

(1)MySQL 向外提供的交互接口(Connectors)

(2)管理服务组件和工具组件(Management Service & Utilities)

(3)连接池组件(Connection Pool)

(4)SQL 接口组件(SQL Interface)

(5)查询分析器组件(Parser)

(6)优化器组件(Optimizer)

(7)缓存主件(Caches & Buffers)

(8)插件式存储引擎(Pluggable Storage Engines)

(9)物理文件(File System)

其中我们可以看到,接入层是非常灵活的,支持所有的编程环境和技术平台。而插件式存储引擎则也是一个非常大的优势,各种特定用途的引擎,被不断的创造和加入进来。前者是 MySQL 被广泛使用的基础,后者是 MySQL 功能强大,可以适用到各种场景的基石。

另一方面 MySQL 自身也在逐步发展:

  • 随着 5.7 的发布和普遍使用,不断增强 InnoDB 引擎的线程池和缓冲池,多源多线程复制能力,新引入 JSON 数据类型等。
  • MySQL 版本在 5.7 以后直接升级到了 8.0.x,2018 年发布了第一个 GA 版本 8.0.11。在 8.0 中,终于引入了窗口函数,以及通用表达式 CTE ,以及使用 JSON_TABLE() 打通了 JSON 数据和关系表。

NoSQL 运动的百花齐放

随着数据规模的增大,以及对数据使用场景的细分,人们越来越意识到,关系数据库在很多场合下并不是最佳选择。这种思想一旦解放了我们的大脑,随之而来的就是各种各样的数据库,我们统称之为 NoSQL 数据库(虽然有一些我个人不认为应该归于数据库)。

为什么出现 NoSQL

NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是 SQL”, 泛指非关系型的数据库。传统的关系数据库基于关系代数和元组(Tuple)操作,这样他们被称为结构化数据,具有固定且一般来说不轻易改变的模式(Schema),例如 MySQL 里表的结构。

早期随着缓存的发展,比如 10 年前最早的 TC/TT,到后来的 Memcached,Redis,渐渐地我们用 key-value 结构在内存里操作经常使用的数据。

然后我们发现如果使用列式存储,要比行式存储能有更大容量,同时可以很好的处理宽表(以为按列打散成 k-v 结构以后,一行数据有 200 列和只有 1 列是一样的),即所谓的数据的局域性。这就产生了 BigTable,Hbase 等基于列族的数据库。

同时我们发现还有很多半结构化的数据,比如文档类型的数据,它们具有一定的结构,但是这些结构可能经常变化,随时可以需要调整 schema,所以就出现了文档数据库,例如 MongoDB 和 CouchDb。

还出现了处理社交网络里的人与人之间关系这种典型的图论应用的图数据库,代表有 Neo4j 等;以及专门处理监控指标等时序数据的时序数据库,例如 influxDB,openTSDB 等等(这两种也有人把它们和分布式数据库合称为 NewSQL)。

走向分布式的小试牛刀

摩尔定律失效

1965 年,英特尔联合创始人戈登·摩尔提出以自己名字命名的「摩尔定律」,意指集成电路上可容纳的元器件的数量每隔 18 至 24 个月就会增加一倍,性能也将提升一倍。

到今天已经 50 多年了。摩尔定律过去是每 5 年增长 10 倍,每 10 年增长 100 倍。而如今,摩尔定律每年只能增长几个百分点,每 10 年可能只有 2 倍。硅芯片逼近物理和经济成本上的极限,摩尔定律结束了。硅谷创业教父 Steve Blank 曾撰文指出,严格来说,「摩尔定律」其实在十年前就已经失效,只是消费者没有意识到。

而随着摩尔定律的失效,带来的一个效应就是,单机性能的瓶颈也已经出现。想要进一步增加单机的性能,需要的成本已经不是线性的,而是几何倍数的增加。这样,就导致了需要我们去思考,如何用廉价的普通机器,甚至云上的虚拟机集群,通过分布式的方式,来达到同样的增强整体性能和容量的目的。

举个例子就是:假如让你去找 10 个 2.5 米的巨人来做一件事儿,明显不如让你去找 20 个 1.8 米的普通人来干这件事儿经济实惠。

分布式崛起

摩尔定律的失效,强迫我们去寻找新的突破口,这个突破口就是分布式技术。

典型地,在底层硬件上,当单个 CPU 制程到 10ns 以内级别的级别,由于量子效应的影响会逐渐更大,导致制程到头了。这样单个 CPU 无法改进,我们就看到了各种各样的多核技术,把多个 CPU 做到一起,让它们协同工作。同时为了更好的利用内存,我们可以给不同的 CPU 以不同的内存(包括缓存)。这就是所谓的 NUMA 架构。我们可以把它看做是一个分布式技术在硬件的应用。

同样地,在软件的层面,我们把一个系统的各个部分也拆解成不同的单元,独立运行,也就形成了分布式系统,进而发展出来了分布式服务,分布式文件系统,分布式消息队列,分布式事务,分布式数据库等等。

当原先的一个单机系统,变成了由很多个不同机器节点组成的复杂分布式系统以后,整个系统的环境就会变得复杂。整体间的协调,管理,就成为了一个比较大的挑战。

这方面,google 在分布式领域的三篇经典论文(DFS/MapReduce/Bigtable),某种程度上解决了分布式文件系统、分布式计算和数据存储的一些常见问题。

谈谈 CAP 不可能三角形

分布式环境

分布式环境与单机环境最大的区别在于:单机是一个由 底层系统 (操作系统,HAL,BIOS 以及固件) 层层包裹的整体。从应用角度看,组成这台机器的硬件要么(看上去)都在正常状态,要么都不在状态,不需要过分关注。

在单机环境,操作系统及 BIOS 既不会向软件报告有一个 CPU 或内存被拔出插槽,也不会报告某条系统总线发生超时 —— 应用也不需要设计成在这种情况下还要继续工作。

分布式环境 天生就需要处理部分失效。原因很简单:分布式 = 多个节点。概率论告诉我们:节点数越多,单个节点发生意外故障的概率就越大。而且,这里并没有 分布式操作系统 帮助我们处理这种意外。

因此,在分布式环境,应用要负责处理节点故障带来的部分失效。早在 2000 年,Eric Brewer 教授就提出,分布式系统的部分失效遵循一个定理:

CAP 定理

缩写 CAP 代表 C onsistency(一致性), A vailability(可用性)和 P artition tolerance(分区容忍性)。注意:这里的 Consistency 与 ACID 里的数据一致性(C)概念不一样。这里指分布式节点拥有相同的数据副本。

CAP 定理断言,任何一个分布式系统都不可能同时满足 C-A-P 三个特性:

C -- 从每个节点读到相同的数据。 A -- 从任何位置发起的读写请求都能够成功返回。 P -- 即使出现分区(部分隔离),也能响应请求。

这是可以证明的。设想一下,当一个分布式系统发生了部分失效:



系统被故障分隔成了两个区域:写入区域(1)的数据无法传播到区域(2),而产生在区域(2)的读请求也不能转发到区域(1)。

这时候,如果让左边区域(1)的数据写入成功 —— 优先保证可用性,则发生在区域(2)的请求读不到最新的数据,违反了一致性。

反之,如果让区域(1)无法写入数据,或者彻底阻止外部请求访问区域(2)—— 则保证了数据一致性,但是却损失了可用性。

显然,要同时保证一致性和可用性,区域(1)和区域(2)必须能够互相通讯。

这里产生了一个 困境 :分布式环境的应用必须在 C-A-P 三个特性之间做出选择。一旦意外故障导致部分节点无法通讯,应用必须在可用性(A)和一致性(C)之间有所取舍。

分布式环境下的故障是常见的。前面提到,故障风险会随着分布式节点数的增加而增加。不管这种故障发生在整个机房还是一个节点,或者仅仅只出现了几个网络丢包 ——它都会导致时间或长或短的部分隔离(P)。

因此,分布式系统必定经常要在 C-A 间做出选择。

以上 CAP 解释归我在阿里时对面工位的小伙伴:长源。

分布式事务

事务的 ACID 特性在分布式环境下也会遇到相同的问题。如果出现部分隔离(P),一个需要访问区域另一侧数据的 ACID 事务,要么选择回退(Rollback),要么选择等待。回退会让事务直接失败,损失了可用性。而等待会让事务无法结束,使得那些由隔离性规定的、在事务结束时才能访问的数据无法访问(例如:没有释放锁)。总之,两个选择都会带来可用性问题。

问题的核心就像前一篇文章说的那样:ACID 的最终目标是保证数据的一致性。这样,在分布式环境中,因为 CAP 定理的限制,满足 ACID 的数据库系统只能选择 C-A 或者 C-P。这样,要么系统无法容忍分布式环境的部分隔离(P),要么在发生部分隔离时必须放弃可用性。

这是一个 分布式困境

数据库与 CAP

基于对一致性 C、可用性 A、分区容忍性 P 的权衡与取舍,就产生了很多不同的数据库:

  • CA:MySQL、SQLServer、PostgreSQL、Oracle
  • CP:HBase、MongoDB、Redis
  • AP:Cassandra、CouchDB、Riak

注意:这只是一个大致的分类,实际使用场景可能复杂得多,我们也可以调整一些参数和用法,讲某类数据库用到另一种类型的场景上。

架构演进的步步为营

从单机到分布式,从分库分表到中间件,这个架构演进的过程,我们可以通过例子来解释。

从单机到集群

拿一个电商系统作为例子,来简单说明分布式技术的发展。假如我们有一个类似于淘宝的电商系统,使用 Java 开发,MySQL 作为数据库,Tomcat 作为 web 服务器。最开始数据量很小,我们跑一个 Tomcat 和一个MySQL 就可以支撑用户访问。

过了一段时间,量大了,Tomcat 出现了瓶颈,我们可以部署 3-5 台 Tomcat 服务器,前面用 Nginx 做负载均衡。这时候我们就有了一个 web 服务器集群。需要注意,此时还不是分布式的,这个集群里的每个机器都是无状态和对等的,大家彼此没有明确的分工。

又过了一段时间,数据库的读压力太大,我们又加了 3 台从库,做了读写分离,从而降低读的压力。对于简单的场景,我们可以直接在系统里,比如 Spring 环境下,配置多个数据源,在具体的业务 service 方法上配置不同的数据源,或者通过一个路由进行切换。

分布式服务化与垂直拆分

再过一段时间的发展,数据量非常大了,我们做了分布式服务化,拆出来了用户中心 UC,订单交易中心 TC,产品中心 IC 等,每个中心有自己的 web 服务器和数据库。这本质上是一种垂直拆分,拆完了以后,每个中心可以独立维护,演进,只要各个中心之间的接口不变,中心内部的各种重构,技术更变升级,都是可以在内部解决的。

虽然这种办法解决不了单库单表数据量太大的问题,比如订单数据表超过了 10 亿条记录,但是这种拆分,就为我们进一步的架构演进,特别是水平拆分提供了便利性。

分库分表与水平拆分

参考前面对分库分表的分析,当单表数据库过大,我们可以拆分数据库和表了。此时我们可以在 Java 环境里引入 TDDL 或者 Sharding-JDBC 之类的框架。在我们的业务代码侧,通过配置特定的分库分表规则,以及对分布式事务的控制,在保证数据一致性的情况下,提升数据库整体集群的容量,保证稳定性和性能。

分库分表框架是在业务系统测,通过封装 JDBC 接口,增强数据库本身的能力。

优势:

  • 可以支持各种常见的数据库,比如 MySQL、PostgreSQL,还有 SQLServer、Oracle、DB2 等等。
  • 较低的性能损耗,因为业务系统还是直接连到目标数据库,这样没有多余的调用步骤。

缺点:

  • 对业务有侵入性,需要开发人员直接处理和维护分库分表逻辑。
  • 不支持非 Java 环境。

数据库中间件的选择

当我们有大量的业务系统需要接入这套已经分库分表过的数据库集群的时候,每一套业务系统就都需要自己维护这套配置规则。特别地,如果有一些系统是非 Java 的,例如 Python、C#或者 Golang 的,那么 TDDL 或 Sharding-JDBC 就无法使用了。由于这类框架本身的实现复杂度,也不便于使用 Python,C#重写一遍。

这时候,我们就需要考虑使用数据库中间件了。

比如原本是 Sharding-JDBC 的项目,我们可以很方便的讲配置规则迁移到 Sharding-Proxy。

然后把非 Java 的系统,只要是能访问正常的 MySQL 数据库,比如 C#的 ADO.NET ,就可以把整个分库分表的集群,当做是一个单一的数据库来操作。

数据库中间件,作为一个增强的数据库代理,本身已经看起来非常像是一个数据库了。

使用数据库中间件,而不是从头搞一套数据库,在于可以复用 MySQL 等数据库固有的能力,以及站在巨人肩膀上,能够长期拥有 MySQL 本身发展的红利。

优势:

  • 使用方便,可以对业务系统透明,开发人员无需了解分片配置等。
  • 跨语言平台,异构语言开发的业务系统都可以方便使用。

劣势:

  • 目前只支持部分开源数据库,比如 MyCat/DBLE 只支持 MySQL,Sharding-Proxy 支持 MySQL 和 PostgreSQL。
  • 性能损耗略大,一般来说,大概会有 5%的 QPS 损失,延迟也会因为网络多一跳而增加 1-3ms。

随着数据库中间件的进一步发展,我们把内置的分片策略配置好,事务和存储都重新实现,其实就发展成了下一个阶段,分布式数据库。

当然,还有一条可以发展的路径,就是结合下一代云原生的服务网格 Service Mesh,发展成为一套数据库网格的解决方案 Database Mesh。

分布式数据库的风起云涌

Google 作为全球最厉害的互联网技术公司之一,不仅发起了分布式系统的技术,同时也是分布式数据库的先驱。2009 年,Google 提出了 Spanner 方案,并随后实现了全球化的分布式关系数据库 F1,用于其广告系统。

Spanner 基于 Paxos 实现多副本,基于 TrueTime API 实现了分布式事务,基于快照实现事务隔离级别,基于 tablet 实现了底层的 k-v 存储。后续的一系列分布式数据库,都有 Spanner 的影子。

当我们谈分布式数据库的时候,我们在谈什么

我们现在聊一聊,分布式数据库,解决什么问题,不解决什么问题。

分布式数据库,首先是一个完备的数据库,支持事务,可线性扩展,高可用。

  • 所以,它必须强一致性,支持事务语义,保证数据不丢失,不出错。
  • 然后通过自动分片等策略实现可以线性的扩容,使用大量的机器集群。
  • 接着是高可用,这一块需要通过多副本来实现(paxos/raft),副本越多,就意味着当少量副本丢失或宕机以后,整个系统对外提供的服务不受影响。

多副本+强一致性,就意味着,越想要高可用,就需要同时写入尽量多的数据副本,这样对于单次操作的延迟就会比单机数据库要高。并且,我们经常为了灾难恢复,会使用一些数据分散的策略,例如我们把所有的数据分三副本:

  • 第一个副本在本城市数据中心 A 的 A1 机架;
  • 第二个副本在本城市数据中心 B 的 B1 机架,保证数据中心 A 出现问题时这一份数据不丢;
  • 第三个副本放到临近省份的某城市的数据中心 C 的 C1 机架,保证本城市数据都出问题时不丢。

这样,随着数据的可靠性提升,这三个副本的距离较远,网络之前的操作就会延迟较高。

所以,分布式数据库一般不解决单次数据请求的性能问题(低延迟),它解决的是主要是容量以及相关问题。

当然,随着分布式数据库的发展,我们可以通过计算、存储分离,多副本的优化操作等,提升性能。

分布式数据库的几个关注点

一般来说,分布式数据库关注点如下:

  • 容量:体现在线性的水平扩展性上;
  • 性能:计算、存储分离;
  • 一致性:分布式事务;
  • 高可用:基于 paxos 或 raft 的多副本机制;
  • 易用性:支持某种 SQL 协议接入;
  • 伸缩性:支持云原生,比如通过 k8s 之类的容器调度,增加或减少节点。

下面我们介绍两个流行的分布式数据库。

CockroachDB 介绍

CockroachDB 是一个分布式关系型数据库,主要设计目标是可扩展,强一致和高可靠 。CockroachDB 旨在无人为干预情况下,以极短的中断时间容忍磁盘、主机、机架甚至整个数据中心的故障 。 CockroachDB 采用完全去中心化架构,集群中各个节点的地位完全对等,同时所有功能封装在一个二进制文件中,可以做到尽量不依赖配置文件直接部署。

CockroachDB 的设计目的是为了实现以下目标:

  • 让人的操作更轻松。 这意味着更少的人工操作和更多的自动化操作,并且对开发人员来说简单易懂。
  • 提供业界领先的一致性,即使在大规模部署中也是如此。 这意味着启用分布式事务,以及消除最终一致性问题和 stale reads 的痛苦。
  • 创建一个"always-on"的数据库,该数据库所有节点都可接收读写操作,而不会产生冲突。
  • 允许在任何环境中灵活部署,而无需与任何平台或供应商联系。
  • 支持熟悉的工具来处理关系数据(例如 SQL)。

随着这些功能的融合,我们希望 CockroachDB 可以让团队轻松构建全球化的、可扩展的、灵活的云服务。

CockroachDB 整体架构

Store 架构与多副本机制

CockroachDB 在机器上使用两个命令开始启动:

对于集群中的所有初始节点,通过带有--join 标志的 cockroach start 命令启动,因此该进程知道它可以与之通信的所有其他机器 cockroach init 执行集群的一次性初始化 一旦 cockroach 进程运行,开发人员就能通过 SQL API 与 CockroachDB 进行交互,我们已经在 PostgreSQL 之后建模。 由于所有节点的对称行为,你可以向任何节点发送 SQL 请求; 这使得 CockroachDB 非常容易与负载均衡器集成。

收到 SQL RPC 后,节点会将它们转换为在我们的分布式 kv 存储上使用的操作。 当这些 RPC 开始用数据填充集群时,CockroachDB 会根据特定算法开始在节点之间分配数据,将数据分解为 64MiB 的块,我们称之为 range。 每个 range 都被复制到至少 3 个节点,以确保可用。 这样,如果节点发生故障,你仍然可以获得可用于读取和写入的数据副本,以及将数据复制到其他节点。

如果一个节点接收到一个它无法直接服务的读或写请求,它会找到能够处理该请求的节点,并与它进行通信。这样你不需要知道数据位于哪里,CockroachDB 会为你跟踪数据,并为每个节点启用对称行为(symmetric behavior)。

对 range 内的数据所做的任何更改都依赖于一致性算法,以确保其大多数副本同意提交更改,确保业界内领先的隔离保证并为你的应用程序提供一致的读取,无论你与哪个节点进行通信。

TiDB 介绍

TiDB(“ Ti”代表 Titanium)是一个开源的 NewSQL 数据库,它支持混合事务处理和分析处理(HTAP)工作负载。它与 MySQL 兼容,具有水平可伸缩性,强一致性和高可用性。TiDB 可以部署在本地或云中。

TiDB 整体架构

TiDB 平台由三个关键组件组成:TiDB 服务器,PD 服务器和 TiKV 服务器。此外,TiDB 还提供 TiSpark 组件以满足复杂的 OLAP 要求,并提供 TiDB Operator 来简化在云上的部署和管理。

  • TiDB server 负责 SQL 的接入和解析处理,定位 TiKV 的地址和读写数据
  • PD server 负责存储集群的元数据,调度和负载均衡(数据迁移,Raft 选举等)
  • TiKV server 封装了 RocksDB,负责数据存储

TiDB 将数据划分为区域,每个区域代表一个数据范围,默认情况下大小限制为 96M。每个区域都有多个副本,每组副本称为筏组。在筏组中,区域负责人执行数据范围内的读取和写入任务。区域领导者由布局驱动程序(PD)组件自动自动调度到不同的物理节点,以分配读写压力。

两个分布式数据库的对比

整体来看,CockroachDB 和 TiDB 的概念和层次、实现都很像。

  1. 都是 Go 语言开发的;
  2. 存储都是基于 RocksDB 封装的 k-v 结构,64M 的块大小;
  3. 都有基于 SI 和 SSI 的事务隔离级别;
  4. 都做了查询优化;
  5. 都有 UI 界面,以及对 Prometheus 和 Grafana 的支持;
  6. 都基于 Raft 实现了多副本;
  7. 都支持自动扩容等等。

同时两者也有很多差别。

  1. CockroachDB 对外是 PostgreSQL 协议,TiDB 是 MySQL 协议;
  2. CockroachDB 是单进程,运行一个 demo 非常方便,起三个进程即可;TiDB 是多进程,demo 需要跑 docker 里,十几个进程;
  3. 事务上,CockroachDB 使用两阶段提交,基于 HLC;TIDB 采用 Google Percolator 事务模型,使用 TSO 时钟;
  4. CockroachDB 没有管理节点,TiDB 使用从 spanner 借鉴来的 PD 管理集群;
  5. 目前的应用场景里,CockroachDB 偏 OLTP,TiDB 偏 OLAP。

下一代数据库解决方案

除了分布式数据库以外,下一代数据库解决方案还有一个更加让人期待的路线,数据库网格。

数据库网格(Database Mesh)

在过去的 2 年里,服务网格(Service Mesh)绝对是一个大家耳熟能详的热门话题。不同于常见的 Apache Dubbo 或者 Spring Cloud 微服务方案,在服务网格里,我们把所有对服务的通信、控制等基础设施和策略,从业务系统中抽离出来,下层到更基础的组件中(我们叫 Sidecar)。这样,我们就把系统划分成了业务服务所在的数据平面,以及控制节点所在的控制平面。

同样地,我们把这些概念应用到数据库领域,就产生了一个新组件,我们可以先叫它 Sharding-Sidecar。这样业务系统就可以不用去管理数据是什么协议,具体在什么地方,而仅仅需要把所有的数据都当做是本地数据,向 Sharding-Sidecar 组件申请使用即可。所有跟数据库本身相关的配置,所有的数据质量、安全、流控和治理的策略,也不需要业务系统直接感知。

相应地,我们也可以把 Sharding-Sidecar 加入到数据面板,把我们的分布式数据库治理和 UI 加入到控制面板。同时,我们可以把对业务系统完全透明的数据库节点比如一批 MySQL 实例,作为一个新的层,叫存储面板。

此时,一个完备的云原生的数据库中间件解决方案,就产生了,我们称之为 数据库网格(Database Mesh)

下一代的开源解决方案路线图

以 Apache ShardingSphere 为例,我们聊一下开源的下一代数据库解决方案。

Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立,却又能够混合部署配合使用的产品组成。 它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

Apache ShardingSphere 定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它通过关注不变,进而抓住事物本质。关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。

Apache ShardingSphere 5.x 版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。 目前,数据分片、读写分离、多数据副本、数据加密、影子库压测等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,仍在不断增加中。

ShardingSphere 已于 2020 年 4 月 16 日成为 Apache 软件基金会的顶级项目。

由上图所示,我们规划了 6 个层次,来共同实现一个完整的 MySQL 开源数据库生态体系:

  • Level 1:MySQL 数据库本身具有的各种能力,并且随着 MySQL 自身的发展,我们一直可以通过集成 MySQL 而拥有这种红利;
  • Level 2:Sharding-JDBC 框架,支持 JVM 平台环境下的各种语言(Java/Groovy/Scala/Kotlin),通过在业务系统在封装 JDBC 接口从而增强 MySQL 的能力,也支持多种数据库;
  • Level 3:Sharding-Proxy 中间件,作为独立的 MySQL 代理服务运行,支持 MySQL 和 PostgreSQL,对业务系统透明,可以在各种不同语言平台下使用,业务无侵入;
  • Level 4:Sharding-Scaling 组件,支持 MySQL 的动态扩容和数据自动迁移,使得我们可以平滑地对既有系统进行升级改造;
  • Level 5:Sharding-Sidecar 组件,作为数据库网格的一个关键组件,旨在能屏蔽所有的数据库配置和数据管理,让业务系统可以专注于业务实现,而无需考虑任何数据的问题;
  • Level 6:Sharding-Engine 平台,通过将整个系统的模块化,实现可插拔平台,即时通过水平的封层,和垂直的功能模块,我们把整个 Sharding 体系拆解为一个个的小 Cell 单元。进而无论是对终端用户,而是数据库中间件领域的二次开发者,都可以像玩乐高玩具一样,选择一定数据的单元,甚至自己实现一些小单元,选择一些要保留的层,抽调一些不需要的层,在这个强大的引擎上,通过自有组合和搭配,实现一套完全贴合自己数据需要的中间件、甚至是分布式数据库。

目前已经实现了前 4 个层次,正在朝着第五个和第六个层次进发,并且已经取得了一定的进展,本项目的终极目标是实现一个大一统的、适用于绝大多数数据库/数据服务场景下的整体解决方案。

不是总结的总结

林林总总写了近 2 万字,很多地方还意犹未尽,没说太清楚。

分布式的话题实在太大,可以写几百本的大部图著作。

本文先就此打住,细水长流,下篇文章我们再继续交流。

编辑于 2023-03-04 22:50 ・IP 属地北京

文章被以下专栏收录