一、分库分表的背景

在谈论数据库架构演变和优化时,我们经常会听到分片、分库分表(Sharding)这样的关键词,在很长一段时间内,在各个公司、各种技术论坛里都很热衷谈论各种分片方案,尤其是互联网非常普及的 MySQL 数据库。但对笔者来说,分片、分库分表并不是一门创新技术,也不是一个好方案,它只是由于数据体系结构的限制而做的无奈之举,所以后来在听到这些词时,对笔者来说,更大意义在于感觉到朋友的公司业务量在快速增长,而对这个方案本身,其实有非常多问题。

 

二、分表的根本原因

以 MySQL 为例,分库分表从阶段应该拆分为分表、分库,一般来说是先进行分表,分表的原动力在于  MySQL 单表性能问题,相信大家都听说过类似这样的话,据说 MySQL 单表数据量超过 N 千万、或者表 Size 大于 N十G 性能就不行了。这个说法背后的逻辑是数据量超过一定大小,B+Tree 索引的高度就会增加,而每增加一层高度,整个索引扫描就会多一次 IO 。整个逻辑有一定道理,而从笔者的经验来看,其实更关键在于应用本身的使用,如果多数是索引命中率很高的点查或者小范围查,其实这个上限还很高,我们维护的系统里超过10亿级的表很常见。但正是由于业务的不可控,所以大家往往采取比较保守的策略,这就是分表的原因。

 

三、分库 + 分表的根本原因

分库主要由于 MySQL 容量上,MySQL 的写入是很昂贵的操作,它本身有很多优化技术,即使如此,写入也存在放大很多倍的现象。同时 MySQL M-S 的主从库读写分离架构虽然天然地支持读流量扩展,但由于 MySQL 从库复制默认采用单线程的 SQL thread 进行 Binlog 顺序重放,这种单线程的从库写入极大地限制整个集群的写入能力,(除非不在意数据延迟,而数据延迟与否直接影响了读流量的可用性)。MySQL 基于组提交的并行复制从某种程度上缓解了这个问题,但本质上写入上限还是非常容易达到(实际业务也就 小几千 的 TPS ) 。说到这,目前有一些云 RDS 通过计算与存储分离、log is database 的理念来很大程度解决了写入扩大的问题,但在这之前,更为普遍的解决方案就是把一个集群拆分成 N 个集群,即分库分表(sharding)。为了规避热点问题,绝大多数采用的方法就是 hash 切分,也有极少的范围、或者基于 Mapping 的查询切分。

 

四、Sharding + Proxy

既然做了分表,那数据的分发、路由就需要进行处理,自下而上分为三层,分别 DB 层、中间层、应用层。DB 层实现,简单来说就是把路由信息加入到某个 Metedata 节点,同时加上一些诸如读写分离、HA 整合成一个 DB 服务或者产品,但这种方案实现复杂度非常高,有的逐步演变成了一种新的数据库,更为常见的是在中间层实现,而中间层又根据偏向 DB 还是偏向应用分为 DB proxy 和 JDBC proxy。

图:Sharding + Proxy

 

五、DB proxy or JDBC proxy

DB proxy、JDBC proxy 业内有很多种,很多都是开源的,我们简单汇总了常见的一些产品,如下图表。

下面各找一些进行对比,只讨论两种方案的优缺点,为了不必要的麻烦,本文不具体讨论哪个产品的优缺点。

实现方案 业界组件 原厂 功能特性 备注
DB proxy-based 多语言支持 Atlas 360 读写分离、静态分表  
Meituan Atlas 美团 读写分离、单库分表 目前已经在原厂逐步下架。
Cobar 阿里(B2B) Cobar 中间件以 Proxy 的形式位于前台应用和实际数据库之间,对前台的开放的接口是 MySQL 通信协议 开源版本中数据库只支持 MySQL,并且不支持读写分离。
MyCAT 阿里 是一个实现了 MySQL 协议的服务器,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问,而其后端可以用MySQL 原生协议与多个 MySQL 服务器通信 MyCAT 基于阿里开源的 Cobar 产品而研发
Heisenberg 百度 热重启配置、可水平扩容、遵守 MySQL 原生协议、无语言限制。  
Kingshard Kingshard 由 Go 开发高性能 MySQL Proxy 项目,在满足基本的读写分离的功能上,Kingshard 的性能是直连 MySQL 性能的80%以上。  
JDBC - based 支持多 ORM 框架,一般有语言限制。 TDDL 阿里淘宝 动态数据源、读写分离、分库分表 TDDL 分为两个版本, 一个是带中间件的版本, 一个是直接 JAVA library 的版本。
Zebra 美团点评 实现动态数据源、读写分离、分库分表、CAT监控 功能齐全且有监控,接入复杂、限制多。
MTDDL 美团点评 动态数据源、读写分离、分布式唯一主键生成器、分库分表、连接池及SQL监控  
DB - based 解决方案 Vitess 谷歌、Youtube 集群基于ZooKeeper管理,通过RPC方式进行数据处理,总体分为,server,command line,gui监控 3部分 Youtube 大量应用
DRDS 阿里 DRDS(Distributed Relational Database Service)专注于解决单机关系型数据库扩展性问题,具备轻量(无状态)、灵活、稳定、高效等特性,是阿里巴巴集团自主研发的中间件产品。 逐渐下沉为DB服务

图:常见的Proxy

 

DB proxy

DB proxy 高度依赖网络组件,它需要诸如 LVS/F5 等 VIP 来实现流量的负载均衡,如果跨 IDC,还依赖诸如 DNS 进行IDC 分发。同时部分 DBproxy 对 Prepare 这类操作支持不友好,所以它的问题概括来说:

1)链路过长,每层都会增加响应时间

2)网络单点,并且往往是整个公司层面的单点

3)部分产品对Prepare 应用不友好,需要绑定 connection 信息

 

JDBC proxy

JDBC Proxy 最大的问题是违背了 DB 透明的原则,它需要对不同的语言编写 Driver,概括来说:

1)语言限制,总会遭到一批 RD 同学的吐槽 “世界上最好的语言竟然不支持!”

2)接入繁琐

3)DB 不透明

图:DB proxy VS JDBC proxy

 

六、Sharding + Proxy 成本汇总

Sharding + Proxy 本质上只解决了一个问题,那就是单机数据容量问题,但它有哪些成本呢?

前面提了每种 proxy 都有比较大的硬伤,我们再把分库分表拉上,一起整理这个方案的成本。

1、应用限制

1)Sharding 后对应用和 SQL 的侵入都很大,需要 SQL 足够简单,这种简单的应用导致 DB 弱化为存储。

2)SQL 不能跨维度 join、聚合、子查询等。

3)每个分片只能实现 Local index,不能实现诸如唯一键、外键等全局约束。

 

2、Sharding 业务维度选择

有些业务没有天然的业务维度,本身选择一个维度就是个问题。

大部分业务需要多维度的支持,多维度的情况下。

 

哪个业务维度为主?

其它业务维度产生了数据冗余,如果没有全局事务的话,很难保证一致性,全局事务本身实现很难,并且响应时间大幅度下降,业务相互依赖存在重大隐患,于是经常发生“风控把支付给阻塞了”的问题。

多维度实现方式,数据库同步还是异步?同步依赖应用端实现双写,异步存在实效性问题,对业务有限制,会发生“先让订单飞一会的问题”。

多维度数据关系表(mapping)维护。

 

3、Sharding key 选择(非业务维度选择)

1)非业务维度选择,会存在“我要的数据到底在那个集群上”的问题。

2)业务维度列如何选择 Sharding key ?

3)热点如何均摊,数据分布可能有长尾效应。

 

4、Sharding 算法选择

1)Hash 算法可以比较好的分散的热点数据,但对范围查询需要访问多个分片。反之 Range 算法又存在热点问题。所以要求在设计之初就要清楚自己的业务常用读写类型。

2)转换算法成本很高。

 

5、高可用问题

高可用的扩散问题(一个集群不可用,整个业务“不可用”)。

如何应对脑裂的情况?

1)MGR 多主模式数据冲突解决方案不成熟,基本上还没公司接入生产系统。

2)PXC 未解决写入容量,存在木桶原则,降低了写入容量。

3)第三方依赖,MHA(判断主库真死、新路由信息广播都需要一定的时间成本) 最快也需要 15s。

4)虽然有 GTID,仍然需要手工恢复。

 

6、数据一致性(其实这个严格上不属于分库分表的问题,但这个太重要了,不得不说)

MySQL 双一方案( redo、binlog 提交持久化) 严重影响了写入性能。

即使双一方案,主库硬盘挂了,由于异步复制,数据还是会丢。

强一致场景需求,比如金融行业,MySQL 目前只能做到双一+半同步复制,既然是半同步,随时可能延迟为异步复制,还是会丢数据。

MGR ?上面说过,多写模式问题很多,距离接入生产系统还很远。

InnoDB Cluster ?先搞出来再说吧。

 

7、DB Proxy

依赖网络层(LVS)实现负载均衡,跨 IDC 依赖 DNS,DNS + LVS + DBproxy + MySQL 网络链路过长,延迟增加、最重要的是存在全公司层面网络单点。

部分产品对 Prepare 不友好,需要绑定 connection。

 

8、JDBC Proxy

语言限制,需要单独对某语言写 Driver,应用不友好。

并未实现 DB 层的透明使用。

 

9、全局 ID

很简单的应用变成了很复杂的实现。

采用 MySQL 自增 ID,写入扩大,单机容量有限。

利用数据库集群并设置相应的步长,绝对埋坑的方案。

依赖第三方组件,Redis Sequence、Twitter Snowflake ,复杂度增加,还引入了单点。

Guid、Random 算法,说好的连续性呢?还有一定比例冲突。

业务属性字段 + 时间戳 + 随机数,冲突比例很高,依赖 NTP 等时间一致服务。

详见米扑博客:数据库分库分表解决方案汇总

 

10、Double resource for AP

同样的数据需要双倍的人力和产品。

产品的重复,Hadoop、Hive、Hbase、Phoenix

人力的重复。

数据迁移的复杂实现,Canal、databus、puma、dataX ?

实时查询?普遍 T+1 查询。

TP 业务表变更导致 AP 业务统计失败,“老板问为啥报表显示昨天订单下降这么多,因为做个了 DDL。”

 

11、运维友好度 (DDL、扩容等)

运维的复杂性是随着机器数量指数级增长的,Google 在 F1 之前维护了一个 100 多个节点的 MySQL sharding 就痛得不行了,不惜重新写了一个 Spanner 和 F1 搞定这个问题。

传统 RDBMS 上 DDL 锁表的问题,对于数据量较大的业务来说,锁定的时间会很长,如果使用 pt-osc、gh-ost 这样第三方工具来实现非阻塞 DDL,额外的空间开销会比较大,另外仍然需要人工的介入确保数据的一致性,最后切换的过程系统可能会有抖动,pt-osc 还需要两次获取 metalock,虽然这个操作本事很轻量,可糟糕的是如果它被诸如 DDL的锁阻塞,它会阻塞所有的 DML,于是悲剧了。

 

12、与原有业务的兼容性

时间成本,如果业务一开始设计时没有考虑分库分表或者中间件这类的方案,在应对数据量暴增的情况下匆忙重构是很麻烦的事情。

技术成本,如果没有强有力和有经验的架构师,很难在业务早期做出良好的设计,另外对于大多数非互联网行业的开发者来说更是不熟悉。

 

13、Sharding 容量管理

拆分不足,需要再次拆分的问题,工作量巨大。

拆分充足,大部分业务增长往往比预期低很多,经常发生“又被 PM 妹纸骗了,说好的百万级流量呢”的问题,即时业务增长得比较好,往往需要一个很长的周期,机器资源浪费严重。

 

14、运维成本,人力成本

不解释,SRE、DBA 兄弟们懂的。

图:sharding 成本汇总

 

七、分库分表的策略

1、分库策略

分库维度确定后,如何把记录分到各个库里呢?

一般有两种方式:

1)根据数值范围,比如用户 Id为1-9999的记录分到第一个库,10000-20000的分到第二个库,以此类推。

2)根据数值取模,比如用户 Id mod n,余数为0的记录放到第一个库,余数为1的放到第二个库,以此类推。

优劣比较:

评价指标按照范围分库按照Mod分库

库数量前期数目比较小,可以随用户/业务按需增长前期即根据mode因子确定库数量,数目一般比较大

访问性能前期库数量小,全库查询消耗资源少,单库查询性能略差前期库数量大,全库查询消耗资源多,单库查询性能略好

调整库数量比较容易,一般只需为新用户增加库,老库拆分也只影响单个库困难,改变mod因子导致数据在所有库之间迁移

数据热点新旧用户购物频率有差异,有数据热点问题新旧用户均匀到分布到各个库,无热点

实践中,为了处理简单,选择mod分库的比较多。同时二次分库时,为了数据迁移方便,一般是按倍数增加,比如初始4个库,二次分裂为8个,再16个。这样对于某个库的数据,一半数据移到新库,剩余不动,对比每次只增加一个库,所有数据都要大规模变动。

补充下,mod分库一般每个库记录数比较均匀,但也有些数据库,存在超级Id,这些Id的记录远远超过其他Id,比如在广告场景下,某个大广告主的广告数可能占总体很大比例。如果按照广告主Id取模分库,某些库的记录数会特别多,对于这些超级Id,需要提供单独库来存储记录。

2、分库数量

分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过5000万条记录,Oracle单库超过1亿条记录,DB压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。

在满足上述前提下,如果分库数量少,达不到分散存储和减轻DB性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。

最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分4-8个库。

3、路由透明

分库从某种意义上来说,意味着DB schema改变了,必然影响应用,但这种改变和业务无关,所以要尽量保证分库对应用代码透明,分库逻辑尽量在数据访问层处理。当然完全做到这一点很困难,具体哪些应该由DAL负责,哪些由应用负责,这里有一些建议:

对于单库访问,比如查询条件指定用户Id,则该SQL只需访问特定库。此时应该由DAL层自动路由到特定库,当库二次分裂时,也只要修改mod 因子,应用代码不受影响。

对于简单的多库查询,DAL负责汇总各个数据库返回的记录,此时仍对上层应用透明。

4、使用框架还是自主研发

目前市面上的分库分表中间件相对较多,其中:

1)基于代理方式的有MySQL Proxy和Amoeba

2)基于Hibernate框架的是Hibernate Shards

3)基于jdbc的有当当sharding-jdbc

4)基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding

5)通过重写spring的ibatis template类是Cobar Client

这些框架各有各的优势与短板,架构师可以在深入调研之后结合项目的实际情况进行选择,但是总的来说,我个人对于框架的选择是持谨慎态度的。一方面多数框架缺乏成功案例的验证,其成熟性与稳定性值得怀疑。另一方面,一些从成功商业产品开源出框架(如阿里和淘宝的一些开源项目)是否适合你的项目是需要架构师深入调研分析的。当然,最终的选择一定是基于项目特点、团队状况、技术门槛和学习成本等综合因素考量确定的。

 

八、分布式事务的解决方案

分布式事务、两阶段提交、一阶段提交、Best Efforts 1PC模式和事务补偿机制的研究

1. XA

XA是由X/Open组织提出的分布式事务的规范。

XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或JMS队列)。

下图说明了事务管理器、资源管理器,与应用程序之间的关系:

图1. XA规范下的分布式事务各类参与者之间的关系

 

2. JTA(Java Transaction API,Java事务API)

作为Java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的Java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:

1)J2EE容器所提供的JTA实现(JBoss)

2)独立的JTA实现:如JOTM,Atomikos

这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。

如Tomcat,Jetty以及普通的Java应用。

 

3. 两阶段提交

所有关于分布式事务的介绍中都必然会讲到两阶段提交,因为它是实现XA分布式事务的关键

确切地说,两阶段提交主要保证了分布式事务的原子性,即所有结点要么全做要么全不做。

所谓的两个阶段是指:

第一阶段:准备阶段

第二阶段:提交阶段

图2. 两阶段提交示意图(摘自info发布的《java事务设计策略》一文)

1. 准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。(关于每一个参与者在准备阶段具体做了什么目前我还没有参考到确切的资料,但是有一点非常确定:参与者在准备阶段完成了几乎所有正式提交的动作,有的材料上说是进行了“试探性的提交”,只保留了最后一步耗时非常短暂的正式提交操作给第二阶段执行。)

2. 提交阶段

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源

将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是一个耗时极短的微小操作,这种操作在一个分布式系统中失败的概率是非常小的,也就是所谓的“网络通讯危险期”非常的短暂,这是两阶段提交确保分布式事务原子性的关键所在。(唯一理论上,两阶段提交出现问题的情况是:当协调者发出提交指令后宕机,并出现磁盘故障等永久性错误,导致事务不可追踪和恢复

从两阶段提交的工作方式来看,很显然,在提交事务的过程中需要在多个节点之间进行协调,而各节点对锁资源的释放必须等到事务最终提交时,这样,比起一阶段提交,两阶段提交在执行同样的事务时会消耗更多时间。事务执行时间的延长意味着锁资源发生冲突的概率增加,当事务的并发量达到一定数量的时候,就会出现大量事务积压甚至出现死锁,系统性能就会严重下滑。这就是使用XA事务

4. 一阶段提交(Best Efforts 1PC模式)

不像两阶段提交那样复杂,一阶段提交非常直白,就是从应用程序向数据库发出提交请求到数据库完成提交或回滚之后将结果返回给应用程序的过程。一阶段提交不需要“协调者”角色,各结点之间不存在协调操作,因此其事务执行时间比两阶段提交要短,但是提交的“危险期”是每一个事务的实际提交时间,相比于两阶段提交,一阶段提交出现在“不一致”的概率就变大了。但是我们必须注意到:只有当基础设施出现问题的时候(如网络中断,宕机等),一阶段提交才可能会出现“不一致”的情况,相比它的性能优势,很多团队都会选择这一方案。

5. 事务补偿机制

像best efforts 1PC这种模式,前提是应用程序能获取所有的数据源,然后使用同一个事务管理器(这里指是的spring的事务管理器)管理事务。这种模式最典型的应用场景非数据库sharding莫属。但是对于那些基于web service/rpc/jms等构建的高度自治(autonomy)的分布式系统接口,best efforts 1PC模式是无能为力的,此类场景下,还有最后一种方法可以帮助我们实现“最终一致性”,那就是事务补偿机制。关于事务补偿机制是一个大话题,本文只简单提及,以后会作专门的研究和介绍。

事务补偿(幂等值)是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等,事务补偿还要结合业务系统来考虑。

6. 在基于两阶段提交的标准分布式事务和Best Efforts 1PC两者之间如何选择

一般而言,需要交互的子系统数量较少,并且整个系统在未来不会或很少引入新的子系统且负载长期保持稳定,即无伸缩要求的话,考虑到开发复杂度和工作量,可以选择使用分布式事务。对于时间需求不是很紧,对性能要求很高的系统,应考虑使用Best Efforts 1PC或事务补偿机制。对于那些需要进行sharding改造的系统,基本上不应再考虑分布式事务,因为sharding打开了数据库水平伸缩的窗口,使用分布式事务看起来好像是为新打开的窗口又加上了一把枷锁。

补充:关于网络通讯的危险期

由于网络通讯故障随时可能发生,任何发出请求后等待回应的程序都会有失去联系的危险。这种危险发生在发出请求之后,服务器返回应答之前,如果在这个期间网络通讯发生故障,发出请求一方无法收到回应,于是无法判断服务器是否已经成功地处理请求,因为收不到回应可能是请求没有成功地发送到服务器,也可能是服务器处理完成后的回应无法传回请求方。这段时间称为网络通讯的危险期(In-doubt Time)。很显然,网络通讯的危险期是分布式系统除单点可靠性之外需要考虑的另一个可靠性问题。

 

总结

分库分表为了解决一个问题,引入了很多成本,从长久看这种方案会逐步被新的解决方案替代。

目前看来,解决的思路主要分为两个方向:

第一个思路,既然分库的原动力主要是单实例的写入容量限制,那么我们可以最大程度地提升整个写入容量,云计算的发展为这种思路提供了新的可能,以 AWS Aurora 为代表 RDS ,它以  Log is database 为理念,将复杂的随机写入简化为顺序写的 Log,并通过将计算与存储分离,把复杂的数据持久化、一致性、数据合并都扔给一个高可用的共享存储系统来完成,进而打开写入的天花板,将昂贵的写入容量提升一个量级;

第二种思路,承认分片的必要性,将这种分片的策略集成到一套整体的分布式数据库体系中,同时通过 Paxos/Raft  复制协议加上多实例节点来实现数据强一致的高可用,其中代表产品有 Google 的 Spanner & F1、TiDBCockRoachDB 等,是比较理想的 Sharding + Proxy 的替代方案。

 

支持分库分表中间件

站在巨人的肩膀上能省力很多,目前分库分表已经有一些较为成熟的开源解决方案:

sharding-jdbc(当当)

TSharding(蘑菇街)

Leaf (美团)

Atlas(奇虎360)

alibaba.cobar (是阿里巴巴(B2B)部门开发)

MyCAT(基于阿里开源的Cobar产品而研发)

OneProxy (支付宝首席架构师楼方鑫开发)

TDDL Smart Client的方式(淘宝)

Oceanus(58同城)

Vitess(谷歌)

 

本文作者:房晓乐(葱头巴巴),PingCAP 资深解决方案架构师,前美团数据库专家、美团云 CDS 架构师、前搜狗、百度资深 DBA,擅长研究各种数据库架构,NewSQL 布道者。

本文转自: 数据库Sharding+Proxy实践解析 

 

 

参考推荐:

1亿qq在线背后的技术

阿里巴巴的海量数据技术架构

数据库分库分表解决方案汇总

数据库分库分表的解决方案比较

大型网站海量数据的业主拆分与高并发

MySQL 临时表,复制记录插入同一张表

Google、Facebook等技术发展历程

MySQL 中 InnoDB 和 MyISAM 小结

MySQL 事务隔离级别和实现原理

大型网站技术架构的知识总结

大型网站架构技术知识点一览

大型网站技术架构:核心原理与案例分析

MySQL基于mysqldump快速搭建从库

淘宝分享:跳出MySQL的10个大坑