利用一致性哈希水平拆分MySql单表

Sharding(切片) 不是一门新技术,而是一个相对简朴的软件理念,就是当我们的数据库单机无法承受高强度的i/o时,我们就考虑利用 sharding 来把这种读写压力分散到各个主机上去。

所以Sharding 不是一个某个特定数据库软件附属的功能,而是在具体技术细节之上的抽象处理,是Horizontal Partitioning 水平扩展(或横向扩展)的解决方案,其主要目的是为突破单节点数据库服务器的 I/O 能力限制,注意这里是突破单点数据库服务器的“I/O”能力。

在MySql 5.1 中增加了对单表的 PARTITION(分区)支持,可以把一张很大的单表通过 partition 分区成很多物理文件,避免每次操作一个大文件,可以对读写新能有所提升,下面是一个 partition 分区的例子。

一张游戏的日志表,有几千万行的数据,记录了接近一年的游戏物品获取日志,如果不对它进行 partition 分区存储,每次统计和分析日志都会消耗大量的时间。然后我们新建一张分区表,把老的日志数据导入到新的数据,统计分析的时间就会节约很多。
  
CREATE TABLE `xxxxxxxx` (     
`crttm` int ( 11 ) NOT NULL ,     
`srvid` int ( 11 ) NOT NULL ,     
`evtid` int ( 11 ) NOT NULL ,     
`aid` int ( 11 ) NOT NULL ,     
`rid` int ( 11 ) NOT NULL ,     
`itmid` int ( 11 ) NOT NULL ,     
`itmnum` int ( 11 ) NOT NULL ,     
`gdtype` int ( 11 ) NOT NULL ,     
`gdnum` int ( 11 ) NOT NULL ,     
`islmt` int ( 11 ) NOT NULL ,  
KEY `crttm` ( `crttm` ),  
  KEY `itemid` ( `itmid` ),  
  KEY `srvid` ( `srvid` ),  
  KEY `gdtype` ( `gdtype` )  
) ENGINE = myisam DEFAULT CHARSET = utf8  
PARTITION BY RANGE ( crttm )   
(  
PARTITION p201303 VALUES LESS THAN ( unix_timestamp ( '2014-04-01' )),  
PARTITION p201304 VALUES LESS THAN ( unix_timestamp ( '2014-05-01' )),  
PARTITION p201305 VALUES LESS THAN ( unix_timestamp ( '2014-06-01' )),  
PARTITION p201306 VALUES LESS THAN ( unix_timestamp ( '2014-07-01' )),  
PARTITION p201307 VALUES LESS THAN ( unix_timestamp ( '2014-08-01' )),  
PARTITION p201308 VALUES LESS THAN ( unix_timestamp ( '2014-09-01' )),  
PARTITION p201309 VALUES LESS THAN ( unix_timestamp ( '2014-10-01' )),  
PARTITION p201310 VALUES LESS THAN ( unix_timestamp ( '2014-11-01' )),  
PARTITION p201311 VALUES LESS THAN ( unix_timestamp ( '2014-12-01' )),  
PARTITION p201312 VALUES LESS THAN ( unix_timestamp ( '2015-01-01' )),  
PARTITION p201401 VALUES LESS THAN ( unix_timestamp ( '2015-02-01' ))  
);  

对于这种业务场景,使用 mysql 的 partition 就已经足够了,但是对于 i/o 非常频繁的大表,单机垂直升级也已经支撑不了,存储已经不是影响其性能的主要原因,这时候就要用到sharding了。

我们一般会将一张大表的唯一键作为 hash 的 key,比如我们想要水平拆分的是一张拥有3千万行数据的用户表,我们可以利用唯一的字段用户id作为拆分的依据,这样就可以依据如下的方式,将用户表水平拆分成3张,下面是伪代码,将老的用户数据导入到新的3个被水平拆分的数据库中。
  
if userId % 3 == 0 :
#insert data in user_table ( user_table_0 databaseip: 127.0.0.1)
elif userId % 3 == 1:
#insert data in user_table ( user_table_1 databaseip: 127.0.0.2)
else:
#insert data in user_table ( user_table_2 databaseip: 127.0.0.3)

我们还会对每一个被拆分的数据库,做一个双主 master 的副本集备份,至于backup,我们则可以使用 percona的cluster来解决。它是比 mysql m/s 或者 m/m 更靠谱的方案。

所以最后拆分的拓扑图大致如下:
利用一致性哈希水平拆分MySql单表 - snoopyxdy - snoopyxdy的博客
 
随着我们的业务增长,数据涨到5千万了,慢慢的发现3个sharding不能满足我们的需求了,因为服务器紧张,所以这时候BOSS打算再加2个sharding,以后会慢慢加到10个sharding。

所以我们得在之前的3台sharding服务器上分别执行导入数据代码,将数据根据新的hash规则导入到每台sharding服务器上。几乎5千万行数据每行都移动了一遍,如果服务器够牛逼,Mysql每秒的插入性能能高达 2000/s,即使这样整个操作,都要让服务暂停8个小时左右。这时候DBA的脸色已经不好看了,他应该是已经通宵在导数据了。

那有没有一种更好的办法,让添加或者删除 sharding 节点对整个分片系统的数据迁移量降低呢?

我们可以利用一致性哈希算法,把用户id散列到各个 sharding 节点,这样就可以保证添加和删除节点数据迁移影响较小。关于什么是一致性哈性算法,参考我的另一篇博客:

这里介绍一个Node.js模块,hashring,github主页地址如下,上面有demo和api文档:
这是一个使用的demo代码,我翻译了注释,供大家参考:

// 加载模块,返回HashRing的构造函数
var HashRing = require('hashring');
//实例化HashRing,这个例子中,我们把各个服务器均匀的添加了,没有设置权重
// 设置了最大的缓冲区 10000
var ring = new HashRing([
    '127.0.0.1',
    '127.0.0.2',
    '127.0.0.3', 
    '127.0.0.4'
  ], 'md5', {
    'max cache size': 10000
  });
//我们获取这个字符串的服务器ip
var server = ring.get('foo bar banana'); // returns 127.0.0.x
console.log(server)
// 如果你想把数据冗余的存储在多个服务器上
ring.range('foo bar banana', 2).forEach(function forEach(server) {
  console.log(server); // do stuff with your server
});
// 对环上移除或新增加一台服务器
ring.add('127.0.0.7').remove('127.0.0.1');
var server = ring . get ( 'foo bar banana' ); // returns 127.0.0.x
console.log(server)

接下来我们就要验证这种方式的可行性。
第一,假如我们有3万条数据,根据一致性哈希算法存储好了之后,这个算法是否能够较平均的将3万条数据分散到3台sharding服务器上。
第二,当数据量增加到5万,然后我们增加2台sharding服务器后,这个算法移动的数据量和最终每台服务器上的数据分布是如何的。

connHashStep1.js将3万用户数据通过一致性哈希算法存储在3台服务器上

var HashRing = require ( 'hashring' );
var ring = new HashRing ([
    '127.0.0.1' ,
    '127.0.0.2' ,
    '127.0.0.3' ,  
  ], 'md5' , {
    'max cache size' : 10000
  });
var record = {
'127.0.0.1' : 0 ,
    '127.0.0.2' : 0 ,
    '127.0.0.3' : 0
};
var userMap = {}
for ( var i = 1 ; i <= 30000 ; i ++){
var userIdStr = i . toString ();
var server = ring . get ( userIdStr );
userMap [ userIdStr ] = server ;
record [ server ]++;
}
console . log ( record );

第一次利用一致性hash之后,每台服务器存储的用户数据。

{ '127.0.0.1': 9162, '127.0.0.2': 9824, '127.0.0.3': 11014 }

connHashStep2.js将5万用户数据通过一致性哈希算法存储在3台服务器上,然后用户数据5万不改变,新增加2台sharding,查看新的5台sharding的用户数据存储情况以及计算移动的数据条数。
   
var HashRing = require ( 'hashring' );
var ring = new HashRing ([
    '127.0.0.1' ,
    '127.0.0.2' ,
    '127.0.0.3' ,  
  ], 'md5' , {
    'max cache size' : 10000
  });
var record = {
'127.0.0.1' : 0 ,
    '127.0.0.2' : 0 ,
    '127.0.0.3' : 0
};
var userMap = {}
  
for ( var i = 1 ; i <= 50000 ; i ++){
var userIdStr = i . toString ();
var server = ring . get ( userIdStr );
userMap [ userIdStr ] = server ;
record [ server ]++;
}
console . log ( record );
//新增加2个sharding节点
var record2 = {
'127.0.0.1':0,
    '127.0.0.2':0,
    '127.0.0.3':0,
'127.0.0.4':0,
'127.0.0.5':0,
};
ring.add('127.0.0.4').add('127.0.0.5')
var moveStep = 0 ;
for ( var i = 1 ; i <= 50000 ; i ++){
var userIdStr = i . toString ();
var server = ring . get ( userIdStr );
//当用户的存储server改变,则计算移动
if(userMap[userIdStr] && userMap[userIdStr] != server){
userMap[userIdStr] = server;
moveStep++;
}
record2[server]++;
}
console.log(record2);
console.log('move step:'+moveStep);

5万用户数据,存储在3台服务器上的数目:

{ '127.0.0.1': 15238, '127.0.0.2': 16448, '127.0.0.3': 18314 }

当我们sharding增加到5台,存储在5台服务器上的数目:

{ '127.0.0.1' : 8869 ,
  '127.0.0.2' : 9972 ,
  '127.0.0.3' : 10326 ,
  '127.0.0.4' : 10064 ,
  '127.0.0.5' : 10769 }

最终我们移动的用户数量:

move step:20833

其实你会发现

20833 = 10064 + 10769 

也就是说,我们只是将1-3节点的部分数据移动到了4,5节点,并没有多余的移动一行数据。根据上面的示例,如果是5千万数据,利用一致性哈希的算法,添加2个节点,仅需2-3小时就可以完成。

那么什么时候我们需要利用一致性哈希水平拆分数据库单表呢?
1、当我们拥有一个数据量非常大的单表,比如上亿条数据。
2、不仅数据量巨大,这个单表的访问读写也非常频繁,单机已经无法抗住 I/O 操作。
3、此表无事务性操作,如果涉及分布式事务是相当复杂的事情,在拆分此类表需要异常小心。
4、查询条件单一,对此表的查询更新条件常用的仅有1-2个字段,比如用户表中的用户id或用户名。
最后,这样的拆分也是会带来负面性的,当水平拆分了一个大表,不得不去修改应用程序或者开发db代理层中间件,这样会加大开发周期、难度和系统复杂性。

P.S 打算在公司试行这种方案,求大牛指点一二,看看有无错误和遗漏。
posted @   silentmuh  阅读(106)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
Live2D
欢迎阅读『利用一致性哈希水平拆分MySql单表』
  1. 1 Walk Thru Fire Vicetone
  2. 2 爱你 王心凌
  3. 3 Inspire Capo Productions - Serenity
  4. 4 Welcome Home Radical Face
  5. 5 粉红色的回忆 李玲玉
爱你 - 王心凌
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 陈思宇/谈晓珍/潘瑛

作曲 : Lee Yong Min/Hwang Se Joon

Rap词:MC HAN韩勇

RAP:

Ya boy MC HAN

我弹的钢琴都是为了你弹

弹了那么久还是觉得浪漫

我弹的时候能听到你在唱

感觉上你在这

跟我一起说话

一天到晚 我不停地想

You’re all that I think of

You’re all that I want

跟你一起总是让我特别开心

不论发生什么事我永远爱你

如果你突然打了个喷嚏 那一定就是我在想你

如果半夜被手机吵醒 啊那是因为我关心

常常想你说的话是不是别有用心

明明很想相信 却又忍不住怀疑

在你的心里 我是否就是唯一 爱就是有我常烦着你

Ho Baby 情话多说一点 想我就多看一眼

表现多一点点 让我能 真的看见

Oh Bye 少说一点 想陪你不止一天

多一点 让我 心甘情愿 爱你

喜欢在你的臂弯里胡闹 你的世界是一座城堡

在大头贴画满心号 贴在手机上对你微笑

常常想我说的话你是否听得进去

明明很想生气 却又止不住笑意

Oh Oh 在我的心里 你真的就是唯一 爱就是有我常赖着你

Ho Baby 情话多说一点 想我就多看一眼

表现多一点点 让我能 真的看见

Oh Bye 少说一点 想陪你不止一天

多一点 让我 心甘情愿 爱你

就这样 一天多一点 慢慢地累积感觉

两人的世界 就能够贴近一点

Ho Baby 情话多说一点 想我就多看一眼

表现多一点点 让我能 真的看见

Oh Bye 少说一点 想陪你不止一天

多一点 让我 心甘情愿 爱你

Ho Baby 情话多说一点 想我就多看一眼

表现多一点点 让我能 真的看见

Oh Bye 少说一点 想陪你不止一天

多一点 让我 心甘情愿 爱你

多一点 才会慢慢发现 因为你 让我心甘情愿

(OT:Nae Yae Gil Eo Bwa)

点击右上角即可分享
微信分享提示