一、分库分表背景
- 为什么分库
1.1数据库最容易产生性能瓶颈的服务组件。数据库连接数资源捉襟见肘和数据库因为表多、数据多造成的性能问题。这里以淘宝为例,在淘宝平台向共享服务体系改造的过程中,通过各个服务中心拥有各自独立数据库的方式,即采用数据库垂直分区的方式对业务数据进行分区。
1.2单一服务中心的数据访问压力也必然会达到单机数据库的承载上限,所以在进行服务化改造的同一时间段内,需要对数据库能力做扩展的工作。
1.3.单台数据库 这里以mysql为例,mysql数据库,当访问连接数过多时,就会出现‘too many connections’的错误,一般来说是访问量太大或者数据库设置的最大连接数太小的原因。Mysql默认的最大连接数为100.这个连接连接数可以修改,而mysql服务允许的最大连接数为16384.虽然可以通过连接池可以一定程序上优化连接,但是当数据量过大时,单台数据库显示已经不能支撑应用服务器的访问。所以就需要做分库,具体分库方法下面会有详细介绍。
1.4 在实际的应用中,绝大部分情况都是读远大于写。Mysql提供了读写分离的机制,所有的写操作都必须对应到Master,读操作可以在 Master和Slave机器上进行,Slave与Master的结构完全一样,一个Master可以有多个Slave,甚至Slave下还可以挂 Slave,通过此方式可以有效的提高DB集群的 QPS.
所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
此外,可以看出Master是集群的瓶颈,当写操作过多,会严重影响到Master的稳定性,如果Master挂掉,整个集群都将不能正常工作。
所以
1. 当读压力很大的时候,可以考虑添加Slave机器的分式解决,但是当Slave机器达到一定的数量就得考虑分库了。
2. 当写压力很大的时候,就必须得进行分库操作。
2.为什么分表
有一组数据可以参考:
基本指标:
库物理文件大小<100G
表<100
字段<200
单表记录数<500W
经测试在单表1000万条记录一下,写入读取性能是比较好的. 这样在留点buffer,那么单表全是数字类型的保持在800万条记录以下, 有字符型的单表保持在500万以下.如果按 100库100表来规划,如用户业务:
500万*100*100 = 50000000万 = 5000亿记录.供参考,具体按按业务做规划.
3.什么是分区
分表是将一张表分成N多个小表,分区是把一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上。mysql的分表是真正的分表,一张表分成很多表后,每一个小表都是完整的一张表,都对应三个文件,一个.MYD数据文件,.MYI索引文件,.frm表结构文件。
二、现状
2.1淘宝的解决方案
首先是实施的是数据库的读写分离。读写分离的基本原理是让主数据库处理事务性增、改、删(Insert、update、delete)操作,而从数据库专门负责处理查询(select)操作,在数据库的后台会把事务性操作导致的主数据库中的数据变更同步到集群中的从数据库。
采用读写分离的方式,拓展了数据库对数据读的处理能力,整体上也大大提升了数据库的读写能力。但是这样的架构在主数据库的数据写入能力依然没法扩展,一旦数据库写压力比较大时,则对整个平台带来非常大的影响。而且数据库单表的数据量是有限制的,当单表数据量达到一定数量后数据库性能会出现显著下降的情况,所以简单的读写分离不能满足淘宝的发展的要求。
为了更好的在数据库层面支持好淘宝的业务,在2008年,阿里巴巴内部开始了分布式数据库的研发工作,相关的平台和工具陆续登上了淘宝技术发展史的历史舞台。
2006年阿里巴巴以开源的形式研发了Cobar这一关系型数据库的分布式处理系统。当时Cobar平均每天处理近50亿次的SQL操作,但是随着阿里巴巴业务场景越来越复杂,有以下缺点:
- 不支持跨库情况下的连接、分页、排序、子查询
- SET语句执行会被忽略,处理事务和字符集设置除外。
- 分库情况下,insert语句必须包含拆分字段列名
- 分库情况下,update语句不能更新拆分字段的值
- 不支持SAVEPOINT操作
- 使用JBDC时,不支持rewriteBatchStatements = true参数设置
等
2008年内部 在Cobar的基础上重新研发了分布式数据层框架TDDL(外号头都大了)
TDDL提供了以下优点:
- 数据库主备和动态切换
- 带权重的读写分离
- 单线程读重试
- 集中式数据源信息管理和动态变更
- 支持MySQL和Oracle数据库
- 基于JDBC规范,很容易扩展支持实现JDBC规范的数据源
- 无Server、client-jar形式存在,应用直连数据库
- 读写次数,并发度流程控制,动态变更
- 可分析的日志打印,日志流控,动态变更。
阿里巴巴分库分表的几个原则:
- 数据尽可能的平均拆分
- 尽可能的减少事务边界 所谓的事务边界 指单个SQL语句在后端数据库上同时执行的数量。因为事务边界越大,系统的锁冲突概率就越高,系统越难以扩展,整体性能越低。
- 异构索引表尽量降低全表扫描频率 这个是针对假如数据是按订单id进行分拆,当用户想查看自己的订单时,针对这种场景,常采用异构索引表来解决,即采用异步机制将原表内的每一次创建或更新,都换另一个维度保存一份完整的数据表或索引表。本质上这是互联网公司很多时候采用的一个解决思路:拿空间换时间 。
实现异构索引的方式,也就是阿里巴巴内部目前采用的方式 ,通过一填名为精卫填海的产品实现了数据的异构复制。
- 将多条件频繁查询引入搜索引擎平台 比如Iucene、Solr、ElasticSearch等搜索引擎。
总结
从系统的风险的角度考虑,以82法则,在实际中,我们仅针对那些在80%情况下访问的那20%的场景进行如数据异构索引这样的处理,达到这类场景的性能最优化,而以其他80%偶尔出现跨库join、全表扫描的场景,采用最为简单直接的方式往往是最有效的方式。
2.2美团的解决方案
通常有两种方案。第一种方案是直接对现有的商品库进行垂直拆分,可以缓解目前写峰值QPS过大、DB主从延迟的问题。第二种方案是对现有的商品库大表进行分库分表,从根本上解决现有问题。方案一实施起来周期较短,但只能解决一时之痛,由此可见,分库分表是必然的。
在确定分库分表的方案之后,调研了外卖订单、结算以及主站等业务的分库分表实现方案,也调研了业界很多分库分表中间件。在综合考虑性能、稳定性及实现成本的前提下,最终决定自主研发客户端分库分表中间件MTDDL来支撑外卖商品分库分表项目,这也就是MTDDL的由来。
在MTDDL的设计研发过程中,我们充分考虑了MTDDL的通用性、可扩展性、功能的全面性和接入的便利性。到目前为止一共开发了四期,实现了MySQL动态数据源、读写分离、分布式唯一主键生成器、分库分表、连接池及SQL监控、动态化配置等一系列功能,支持分库分表算法、分布式唯一主键生成算法的高可扩展性,而且支持全注解的方式接入,业务方不需要引入任何配置文件。
分布式唯一主键生成器
众所周知,分库分表首先要解决的就是分布式唯一主键的问题,业界也有很多相关方案(大众点评用的是时间戳+用户标识码+随机数):
综上,方案3的缺点可以通过一些手段避免,但其他方案的缺点不好处理,所以选择第3种方案。目前该方案已由美团点评技术工程部实现——分布式ID生成系统Leaf,MTDDL集成了此功能。
美团分库分表流程
分库分表取模算法:
分库分表目前默认使用的是取模算法,分表算法为 (#shard_key % (group_shard_num * table_shard_num)),分库算法为 (#shard_key % (group_shard_num * table_shard_num)) / table_shard_num,其中group_shard_num为分库个数,table_shard_num为每个库的分表个数。
例如把一张大表分成100张小表然后散到2个库,则0-49落在第一个库、50-99落在第二个库。核心实现如下:
大众点评订单表早已超过200G,由于查询维度较多,即使加了两个从库,优化索引,仍然存在很多查询不理想的情况。
先对订单库进行垂直切分,将原有的订单库分为基础订单库、订单流程库等。
垂直切分缓解了原来单集群的压力,但是在抢购时仍然捉襟见肘。原有的订单模型已经无法满足业务需求,于是我们设计了一套新的统一订单模型,为同时满足C端用户、B端商户、客服、运营等的需求,我们分别通过用户ID和商户ID进行切分,并通过PUMA(我们内部开发的MySQL binlog实时解析服务)同步到一个运营库。
切分策略
- 查询切分
将ID和库的Mapping关系记录在一个单独的库中。
优点:ID和库的Mapping算法可以随意更改
缺点:引入额外的单点
- 范围切分
比如按照时间区间或ID区间来切分
优点:单表大小可控,天然水平扩展
缺点:无法解决集中写入瓶颈的问题
- hash切分
一般采用Mod来切分,下面着重讲一下Mod的策略
数据水平切分后我们希望是一劳永逸或者易于水平扩展的,所以推荐mod2^n这种一致性Hash
以统一订单库为例,我们分库分表的方案是32*32的,即通过UserId后四位mod32到32个库中,同时再将UserId后四位Div 32 Mod 32将每个库分为32个表,共计分为1024个表。线上部署情况为8个集群(主从),每个集群4个库。为什么说这种方式易于水平扩展呢?
分析两种场景。
场景一:数据库性能达到瓶颈
方法一
按照现有规则不变,可以直接扩展到32个数据库集群。
方法二
如果32个集群也无法满足需求,那么将分库分表规则调整为(32*2^n)*(32/2^n)可以达到最多1024个集群。
场景二:单表容量达瓶颈(或者1024已经无法满足)
方法:
假如单表都已突破200G,200*1024=200T(按照现有的订单模型算了算,大概1000万亿订单),32*(32*2^n),这时分库规则不变,单库里的表再进行裂变,当然目前的订单规则下(userId后四位mod)还是有极限的。因为只有四位,所以最多拆8192个表。
唯一ID方案
这个方案也有很多种,主流的有那么几种:
- 利用数据库自增ID
优点:最简单。缺点:单点风险,单机性能瓶颈。
2. 利用数据库集群并设置相应的步长(Flickr方案)
优点:高可用、ID较简洁。 缺点:需要单独的数据库集群。
3. Twitter Snowflake
优点:高性能高可用、易拓展。 缺点:需要独立的集群以及ZK。
4. 一大波GUID、Random算法
优点:简单。 缺点:生成ID较长,有重复几率。
美团的方案:为了减少运营成本并减少额外的风险,排除了所有需要独立集群的方案,采用带有业务属性的议案: 时间戳+用户标识码+随机数
有以下几个好处:
1.方便、成本低。
2.基本无重复的可能。
3.自带分库规则,这里的用户标识码即为用户ID的后四位,在查询的场景下,只需要订单号就可以匹配到相应的库表而无需用户ID,只取四位是希望订单号尽可能的短一些,并且评估下来四位已经足够。
4.可排序,因为时间戳在最前面。
其他问题
事务支持:我们是将整个订单领域聚合体切分,维度一致,所以对聚合体的事务是支持的。
复杂查询:垂直切分后,就跟join说拜拜了;水平切分后,查询的条件一定要在切分的维度内,比如查询具体某个用户下的各位订单等;禁止不带切分的维度的查询,即使中间件可以支持这种查询,可以在内存中组装,但是这种需求往往不应该在在线库查询,或者可以通过其他方法转换到切分的维度来实现。
数据迁移
数据库拆分一般是业务发展到一定规模之后的优化和重构,为了支持业务快速上线,很难一开始就分库分表,垂直拆分还好办,改改数据源就搞定了,一旦开始水平拆分,数据清洗是一个大问题。为此,经历了以下几个阶段。
第一阶段:
数据库双写(事务成功以老模型为准),查询走老模型
每日job数据对账(通过DW),并将差异补平
通过job导历史数据
第二阶段
历史数据导入完毕并且数据对账无误
依然是数据库双写,但是事务成功与否以新模型为准,在线查询切换新模型
每日job数据对账,将差异补平
第三阶段
老模型不同同步写入,仅当订单有终态时才会异步补上。
此阶段只有离线数据依然依赖老的模型,并且下游的依赖非常多,待DW改造完就可以完全废除老模型了。
总结
1.并非所有表都需要水平拆分,要看增长的类型和速度,水平拆分是大招,拆分后会增加开发的复杂度,不到万不得已不使用。
2.在大规模并发的业务上,尽量做到在线查询和离线查询隔离,交易查询和运营/客服查询隔离。
3.拆分维度的选择很重要,要尽可能在解决拆分前问题的基础上,便于开发。
4.数据库没你想象的那么坚强,需要保护,尽量使用简单的、良好索引的查询,这样数据库整体可控,也易于长期容量规划以及水平扩展。
三、分库方案
3.1垂直切分
按业务切分,将表按照功能模块、关系密切程度划分出来,部署到不同的库上。例如用户相关、教务相关、cc业务相关、报表相关等。
3.2水平切分
当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,例如userID散列,进行划分,然后存储到多个结构相同的表,和不同的库上。例如,我们的userDB中的用户数据表中,每一个表的数据量都很大,就可以把userDB切分为结构相同的多个userDB:part0DB、part1DB等,再将userDB上的用户数据表userTable,切分为很多userTable:userTable0、userTable1等,然后将这些表按照一定的规则存储到多个userDB上。
3.3切分的选择
应该使用哪一种方式来实施数据库分库分表,这要看数据库中数据量的瓶颈所在,并综合项目的业务类型进行考虑。
如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清晰、低耦合,那么规则简单明了、容易实施的垂直切分必是首选。而如果数据库中的表并不多,但单表的数据量很大、或数据热度很高,这种情况之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻辑上属于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,考虑数据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管理负担。
在现实项目中,往往是这两种情况兼而有之,这就需要做出权衡,甚至既需要垂直切分,又需要水平切分。
四、分表方案
4.1利用数据库的merge存储引擎来实现分表
优势:不用改代码。
缺点:有一些限制,必须是MyISAM存储引擎,分表的id不能是自增的,每个分表的表结构必须相同,Mysql必须具有存储分一数据文件和索引文件的目录的读写权限,必须启用mysql的符号链接支持功能。
4.2垂直切分
垂直拆分是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放入到用户库,把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联。
比如原始用户表是:
垂直拆分后的表是:
垂直拆分的优点是:
1. 可以使得数据变小,一个数据块(block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block就少)
2. 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
3. 数据维护简单
缺点是:
1.主键出现冗余,需要管理冗余例
2.会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
3.依然存在单表数据量过大的问题(需要水平拆分)
4.事务处理复杂
4.3 水平切分
4.3.1按照订单号做Hash分散订单数据
把订单号看作是一个字符串,做hash,分散到多个服务器。具体分到哪个库、哪个表存储数据,订单号的数字来记录。以微信红包为例:订单分库分表,是对订单号进行hash计算。然后订单的末尾3个数包含了库名称、表名称。如:可参考美团的方法。
那么如何查询某用户的所有订单呢?由于根据订单号来分散数据的,他们的订单分散在了多个库、多个表中。
建异构索引表
同样如何应付多维度的检索,例如所有订单的分页、订单状态检索、根据促销维度检索等
做数据冗余、存储多份。或者用ES来处理
4.3.2按照用户id来切分
按照用户来切分数据有两种思路:
一种方案是某个范围的uid订单到哪些库。例如0到2000w uid对应订单数据到a库,2000w到4000w对应订单到b库。
缺点:某个范围内的用户,下单量比较多,那么造成这个库的压力特别大,数据分布极为不均衡。
优点:查询指定用户的所有订单,避免了跨库跨表查询。因为用户的id是规定不变的,那么计算出的值永远是固定的某库某表。那么a用户的所有订单都是在某库某表里面。
第二种方案是使用uid取模运算,第二种方案业界用的比较多。
具体实现如下:
按照用户id作为key来切分订单数据,具体如下:
1、 库名称定位:用户id末尾4位 Mod 32。
Mod表示除以一个数后,取余下的数。比如除以32后,余下8,余数就是8。
代码符号是用%表示:15%4=3。
2、表名称定位:(用户id末尾4位 Dev 32) Mod 32。
Dev表示除以一个数,取结果的整数。比如得到结果是25.6,取整就是25。
代码用/来表示:$get_int = floor(15/4)。15除以4,是一个小数3.75,向下取整就是3。一定是向下取整,向上取整就变成了4了。
按照上面的规则:总共可以表示多少张表呢?32个库*每个库32个表=1024张表。如果想表的数量小,就把32改小一些。
上面是用计算机术语来表示, 下面用通俗的话描述。
1、库名称计算
用户id的后4位数,取32的模(取模就是除以这个数后,余多少)。余下的数,是0-31之间。
这样可以表示从0-31之间,总共32个数字。用这个32个数字代表着32个库名称:order_db_0、order_db_2…………………….order_db_31
2、表名称计算
最后要存储定到哪个表名里面去呢?
用户id的最后4位数,除以32,取整数。将整数除以32,得到余数,能够表示从0-31之间32个数字,表示表名称。
表名称类似这样:order_tb_1、order_tb_2……………………..order_tb_31。一个库里面,总共32个表名称。
比如用户id:19408064,用最后4位数字8064除以32,得到是251.9,取它的整数是251。
接着将251除以32,取余数,余数为27。
为了保持性能,每张表的数据量要控制。单表可以维持在一千万-5千万行的数据。1024*一千万。这个已经是很大的数据量了。
4.3.4按订单的时间来分表
一个月一张,一年一张表,用户的所有订单,会分散在不同的库、不同的表中。这个可以根据实际的应用场景来使用。适合于经常只查询某一时间段的数据。因为这个种分表方法会有几个缺点,如果查询某个用户所有订单数据,就会出现跨库、跨表,效率会低。
4.4 阿里云的RDS分库分表方案。DRDS(Distributed Relational Database Service,分布式关系型数据库服务 )是阿里巴巴自主研发,致力于解决单机数据库瓶颈而推出的分布式数据库中间件产品。DRDS高度兼容Mysql协议和语法,支持水平拆分、平滑扩容、弹性扩展、透明读写分离和分布式事务等特性,具备分布式数据库全生命周期的运维管控能力。
DRDS 支持库级拆分、表级拆分和分库分表拆分。拆分键暂时只支持单个字段。
分库键:DRDS 根据分库键的值将数据水平拆分到后端的每一个 RDS 分库里。键值相同的数据,一定会位于同一个 RDS 数据库里。
分表键:每一张逻辑表都可以定义自己的分表键,键值相同的数据,一定会位于同一个 RDS 数据表里。
网上数据:贝聊大概有几万所幼儿园,上亿数据量,效果比较 :
1、 性能大幅度提升
响应速度提升5至10倍,以当时获取班级动态列表接口为例:
分库分表实施前:响应时间大致为200ms至1500ms级之间。
分库分表实施后:响应时间提升至大致为30ms至300ms之间。
2、 可方便快速进行横向扩容
如果需要扩容,直接按照阿里云DRDS指引,增加新的分库即可。
3、 架构的优化
独立动态业务,通过Dubbo提供服务,对系统进行解耦,让业务具备快速横向扩容能力。
4、后遗症
独立后,暂时没有做分布式事务,在业务中需要避免跨库事务。
- 分库分表存在的问题及解决方案
- 事务问题
在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
- 跨库跨表join问题
在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。
解决方法:建立异构索引表。以此为例,建立用户id与订单号的索引关系表。
例如:假设是以订单号分散订单数据。需求是查询某个用户的所有订单。如果知道某个用户的所有订单。那么就可以根据订单号定位到表名称。假设是以用户id分散订单数据的,那么我们只要知道这个订单号是谁的(得到了用户id),就知道去哪个库、哪个表查询数据了。
索引表里包括两项:订单号、用户编号。当数据量越来越大时,索引表也不能用单个表存储了,也需要分库分表了,因为关系一一对应,不会变化,所以也可以存在redis里。这样速度就比较快了。
五、总结
根据公司的adu来看,包括与美团淘宝的比较而言,可以通过业务来分库,并不是一定要做以id取模来分库,否则要更改的东西比较 大。另外分表的话可以水平和垂直混用,包括适当的场景用ES.而具体实施的时候参美团的三个步骤执行,先老库旧库时间使用,然后监控情况,待新库稳定之后再切到新库。另外以于一些开源的工具可以参考使用。如果按业务进行拆分的话,像报表统计相关的可以分离,教研相关业务独立拆分,用户相关、权限管理可独立拆分成一个模块,小程序相关独立拆分,cc业务独立拆分,订单业务独立拆分、app相关独立拆分、市场推广独立拆分。原则是高聚合,低耦合。具体根据公司的实际应用场景再有所调整。
文章摘自知乎:闫冬