你分库分表的姿势对么?——详谈水平分库分表
Posted vivo互联网技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你分库分表的姿势对么?——详谈水平分库分表相关的知识,希望对你有一定的参考价值。
作者:vivo平台产品开发团队-Han Lei
一、背景
提起分库分表,对于大部分服务器开发来说,其实并不是一个新鲜的名词。随着业务的发展,我们表中的数据量会变的越来越大,字段也可能随着业务复杂度的升高而逐渐增多,我们为了解决单表的查询性能问题,一般会进行分表操作。
同时我们业务的用户活跃度也会越来越高,并发量级不断加大,那么可能会达到单个数据库的处理能力上限。此时我们为了解决数据库的处理性能瓶颈,一般会进行分库操作。不管是分库操作还是分表操作,我们一般都有两种方式应对,一种是垂直拆分,一种是水平拆分。
关于两种拆分方式的区别和特点,互联网上参考资料众多,很多人都写过相关内容,这里就不再进行详细赘述,有兴趣的读者可以自行检索。
此文主要详细聊一聊,我们最实用最常见的水平分库分表方式中的一些特殊细节,希望能帮助大家避免走弯路,找到最合适自身业务的分库分表设计。
【注1】本文中的案例均基于mysql数据库,下文中的分库分表统指水平分库分表。
【注2】后文中提到到M库N表,均指共M个数据库,每个数据库共N个分表,即总表个数其实为M*N。
二、什么是一个好的分库分表方案?
2.1 方案可持续性
前期业务数据量级不大,流量较低的时候,我们无需分库分表,也不建议分库分表。但是一旦我们要对业务进行分库分表设计时,就一定要考虑到分库分表方案的可持续性。
那何为可持续性?其实就是:业务数据量级和业务流量未来进一步升高达到新的量级的时候,我们的分库分表方案可以持续使用。
一个通俗的案例,假定当前我们分库分表的方案为10库100表,那么未来某个时间点,若10个库仍然无法应对用户的流量压力,或者10个库的磁盘使用即将达到物理上限时,我们的方案能够进行平滑扩容。
在后文中我们将介绍下目前业界常用的翻倍扩容法和一致性Hash扩容法。
2.2 数据偏斜问题
一个良好的分库分表方案,它的数据应该是需要比较均匀的分散在各个库表中的。如果我们进行一个拍脑袋式的分库分表设计,很容易会遇到以下类似问题:
a、某个数据库实例中,部分表的数据很多,而其他表中的数据却寥寥无几,业务上的表现经常是延迟忽高忽低,飘忽不定。
b、数据库集群中,部分集群的磁盘使用增长特别块,而部分集群的磁盘增长却很缓慢。每个库的增长步调不一致,这种情况会给后续的扩容带来步调不一致,无法统一操作的问题。
这边我们定义分库分表最大数据偏斜率为 :(数据量最大样本 - 数据量最小样本)/ 数据量最小样本。一般来说,如果我们的最大数据偏斜率在5%以内是可以接受的。
三、常见的分库分表方案
3.1 Range分库分表
顾名思义,该方案根据数据范围划分数据的存放位置。
举个最简单例子,我们可以把订单表按照年份为单位,每年的数据存放在单独的库(或者表)中。如下图所示:
* 通过年份分表
*
* orderId
* */
String
year = Integer.parseInt(orderId.substring( + year;
ShardCfg
hash = userId.hashCode();
dbIdx = Math. tblIdx = Math.
ShardCfg(dbIdx, tblIdx);
ShardCfg
hash = userId.hashCode();
sumSlot = DB_CNT * TBL_CNT;
slot = Math. dbIdx = slot % DB_CNT ;
tblIdx = slot / DB_CNT ;
ShardCfg(dbIdx, tblIdx);
该方案确实很巧妙的解决了数据偏斜的问题,只要ShardCfg hash = userId.hashCode();
sumSlot = DB_CNT * TBL_CNT;
slot = Math. dbIdx = slot / TBL_CNT ;
tblIdx = slot % TBL_CNT ;
ShardCfg(dbIdx, tblIdx);
大家可以注意到,和错误案例二中的区别就是通过分配序号重新计算库序号和表序号的逻辑发生了变化。它的分配情况如下:
那为何使用这种方案就能够有很好的扩展持久性呢?我们进行一个简短的证明:
通过上面结论我们知道,通过翻倍扩容后,我们的表序号一定维持不变,库序号可能还是在原来库,也可能平移到了新库中(原库序号加上原分库数),完全符合我们需要的扩容持久性方案。
【方案缺点】
1、翻倍扩容法前期操作性高,但是后续如果分库数已经是大几十的时候,每次扩容都非常耗费资源。
2、连续的分片键ShardCfg tblIdx = Math.abs(userId.hashCode() % TBL_CNT);
Integer dbIdx = loadFromCache(userId);
(== dbIdx)
dbIdx = loadFromRouteTable(userId);
(!= dbIdx)
saveRouteCache(userId, dbIdx);
(== dbIdx)
dbIdx = selectRandomDbIdx();
saveToRouteTable(userId, dbIdx);
saveRouteCache(userId, dbIdx);
ShardCfg(dbIdx, tblIdx);
该方案还是通过常规的ShardCfg dbIdx = Math. tblIdx = Math. ShardCfg(dbIdx, tblIdx);
ShardCfg
dbIdx = Math. tblIdx = Math. ShardCfg(dbIdx, tblIdx);
TreeMap<Long, Integer> nodeTreeMap = TreeMap<>();
List<HashCfg> cfgList = fetchCfgFromDb();
(HashCfg cfg : cfgList)
nodeTreeMap.put(cfg.endKey, cfg.nodeIdx);
ShardCfg
hash = userId.hashCode();
dbIdx = nodeTreeMap.tailMap(( tblIdx = Math. ShardCfg(dbIdx, tblIdx);
vivo 云服务业务数据库数据压缩实践
富文本编辑器之游戏角色升级ing
灵活运用分布式锁解决数据重复插入问题
分库分表的正确姿势,你GET到了么?
每个优秀的程序员和架构师都应该掌握分库分表,这是我的观点。
移动互联网时代,海量的用户每天产生海量的数量,比如:
用户表
订单表
交易流水表
以支付宝用户为例,8亿;微信用户更是10亿。订单表更夸张,比如美团外卖,每天都是几千万的订单。淘宝的历史订单总量应该百亿,甚至千亿级别,这些海量数据远不是一张表能Hold住的。事实上MySQL单表可以存储10亿级数据,只是这时候性能比较差,业界公认MySQL单表容量在1KW以下是最佳状态,因为这时它的BTREE索引树高在3~5之间。
既然一张表无法搞定,那么就想办法将数据放到多个地方,目前比较普遍的方案有3个:
分区;
分库分表;
NoSQL/NewSQL;
说明:只分库,或者只分表,或者分库分表融合方案都统一认为是分库分表方案,因为分库,或者分表只是一种特殊的分库分表而已。NoSQL比较具有代表性的是MongoDB,es。NewSQL比较具有代表性的是TiDB。
Why Not NoSQL/NewSQL?
首先,为什么不选择第三种方案NoSQL/NewSQL,我认为主要是RDBMS有以下几个优点:
- RDBMS生态完善;
- RDBMS绝对稳定;
- RDBMS的事务特性;
NoSQL/NewSQL作为新生儿,在我们把可靠性当做首要考察对象时,它是无法与RDBMS相提并论的。RDBMS发展几十年,只要有软件的地方,它都是核心存储的首选。
目前绝大部分公司的核心数据都是:以RDBMS存储为主,NoSQL/NewSQL存储为辅!互联网公司又以MySQL为主,国企&银行等不差钱的企业以Oracle/DB2为主!NoSQL/NewSQL宣传的无论多牛逼,就现在各大公司对它的定位,都是RDBMS的补充,而不是取而代之!
Why Not 分区?
我们再看分区表方案。了解这个方案之前,先了解它的原理:
分区表是由多个相关的底层表实现,这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区,存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引,从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。
事实上,这个方案也不错,它对用户屏蔽了sharding的细节,即使查询条件没有sharding column,它也能正常工作(只是这时候性能一般)。不过它的缺点很明显:很多的资源都受到单机的限制,例如连接数,网络吞吐等!从而导致它的并发能力非常一般,远远达不到互联网高并发的要求!
至于网上提到的一些其他缺点比如:无法使用外键,不支持全文索引。我认为这都不算缺点,21世纪的项目如果还是使用外键和全文索引,我都懒得吐槽了!
所以,如果使用分区表,你的业务应该具备如下两个特点:
数据不是海量(分区数有限,存储能力就有限);
并发能力要求不高;
Why 分库分表?
最后要介绍的就是目前互联网行业处理海量数据的通用方法:分库分表。
虽然大家都是采用分库分表方案来处理海量核心数据,但是还没有一个一统江湖的中间件,笔者这里列举一些有一定知名度的分库分表中间件:
阿里的TDDL,DRDS和cobar,
京东金融的sharding-jdbc;
民间组织的MyCAT;
360的Atlas;
美团的zebra;
sharding-jdbc的3.x版本即sharding-sphere已经支持了proxy模式。
其他比如网易,58,京东等公司都有自研的中间件。总之各自为战,也可以说是百花齐放。
但是这么多的分库分表中间件全部可以归结为两大类型:
CLIENT模式;
PROXY模式;
CLIENT模式代表有阿里的TDDL,京东金融的sharding-jdbc。架构如下:
PROXY模式代表有阿里的cobar,民间组织的MyCAT。架构如下:
但是,无论是CLIENT模式,还是PROXY模式。几个核心的步骤是一样的:SQL解析,重写,路由,执行,结果归并。
笔者比较倾向于CLIENT模式,架构简单,性能损耗较小,运维成本低。
接下来,以几个常见的大表为案例,说明分库分表如何落地!
实战案例
分库分表第一步也是最重要的一步,即sharding column的选取,sharding column选择的好坏将直接决定整个分库分表方案最终是否成功。而sharding column的选取跟业务强相关,笔者认为选择sharding column的方法最主要分析你的API流量,优先考虑流量大的API,将流量比较大的API对应的SQL提取出来,将这些SQL共同的条件作为sharding column。例如一般的OLTP系统都是对用户提供服务,这些API对应的SQL都有条件用户ID,那么,用户ID就是非常好的sharding column。
这里列举分库分表的几种主要处理思路:
只选取一个sharding column进行分库分表 ;
多个sharding column多个分库分表;
sharding column分库分表 + ElasticSearch;
再以几张实际表为例,说明如何分库分表。
订单表
订单表几个核心字段一般如下:
order_id | user_id | merchant_code | order_amount | order_time |
---|
以阿里订单系统为例(参考《企业IT架构转型之道:阿里巴巴中台战略思想与架构实现》),它选择了三个column作为三个独立的sharding column,即:order_id,user_id,merchant_code。user_id和merchant_code就是买家ID和卖家ID,因为阿里的订单系统中买家和卖家的查询流量都比较大。而根据order_id进行分库分表,应该是根据order_id的查询也比较多。
这里还有一点需要提及,多个sharding-column的分库分表是全量冗余还是只冗余关系索引表。
冗余全量的情况如下--每个sharding列对应的表的数据都是全量的,这样做的优点是不需要二次查询,性能更好,缺点是比较浪费存储空间:
sharding column为order_id:
order_id | user_id | merchant_code | order_amount | order_time |
---|
sharding column为user_id:
user_id | order_id | merchant_code | order_amount | order_time |
---|
sharding column为merchant_code:
merchant_code | order_id | user_id | order_amount | order_time |
---|
冗余索引表的情况如下--只有一个sharding column的分库分表的数据是全量的,其他分库分表只是与这个sharding column的关系表,这样做的优点是节省空间,缺点是除了第一个sharding column的查询,其他sharding column的查询都需要二次查询:
sharding column为order_id:
order_id | user_id | merchant_code | order_amount | order_time |
---|
sharding column为user_id:
user_id | order_id |
---|
sharding column为merchant_code:
merchant_code | order_id |
---|
总结:选择冗余全量表还是索引关系表,这是一种架构上的trade off,两者的优缺点明显,阿里的订单表是冗余全量表。
用户表
用户表几个核心字段一般如下:
user_id | mobile_no | username |
---|
一般用户登录场景即可以通过mobile_no,也可以通过email,还可以通过username进行登录。但是一些用户相关的API,又都包含user_id,那么可能需要根据这4个column都进行分库分表,即4个列都是sharding-column。
账户表
账户表几个核心字段一般如下:
account_no | available_amount | frozen_amount | status |
---|
与账户表相关的API,一般条件都有account_no,所以以account_no作为sharding-column即可。
复杂查询
上面提到的都是条件中有sharding column的SQL执行。但是,总有一些查询条件是不包含sharding column的,同时,我们也不可能为了这些请求量并不高的查询,无限制的冗余分库分表。那么这些条件中没有sharding column的SQL怎么处理?以sharding-jdbc为例,有多少个分库分表,就要并发路由到多少个分库分表中执行,然后对结果进行合并。具体如何合并,可以看笔者sharding-jdbc系列文章,有分析源码讲解合并原理。
这种条件查询相对于有sharding column的条件查询性能很明显会下降很多。如果有几十个,甚至上百个分库分表,只要某个表的执行由于某些因素变慢,就会导致整个SQL的执行响应变慢,这非常符合木桶理论。
更有甚者,那些运营系统中的模糊条件查询,或者上十个条件框,任何一个条件可有可无。这种情况下,即使单表都不好创建索引,更不要说分库分表的情况下。那么怎么办呢?这个时候大名鼎鼎的ElasticSearch,即es就派上用场了。将分库分表所有数据全量冗余到es中,将那些复杂的查询交给es处理。
所以,以订单表为例,整个架构如下:
以上是关于你分库分表的姿势对么?——详谈水平分库分表的主要内容,如果未能解决你的问题,请参考以下文章