首先说明,这里所说的分库分表是指把数据库中数据物理地拆分到多个实例或多台机器上去,而不是MySQL原生的Partitioning。
这里稍微提一下Partitioning,这是MySQL官方版本支持的,在本地针对表的分区进行操作,它可以将一张表的数据分别存储为多个文件。
如果在写SQL的时候,遵从了分区规则,就能把原本需要遍历全表的工作转变为只需要遍历表里某一个或某些分区的工作。
这样降低了查询对服务器的压力,提升了查询效率。如果分区表使用得当,也可能大规模地提升MySQL 的服务能力。
但是这种分区方式,一方面,在使用的时候必须遵从分区规则写SQL语句,如果不符合分区规则,性能反而会非常低下;
另一方面,Partitioning的结果受到MySQL实例,或者说MySQL单实例的数据文件无法分布式存储的限制,不管怎么分区,所有的数据还是都在一个服务器上,没办法通过水平扩展物理服务的方法把压力分摊出去。所以,限于篇幅,在讲分库分表时就不谈Partitioning这种分区方式了。
在MySQL运维实践中,拆分一般分为垂直拆分和水平拆分。
在考虑数据库拆分的时候,一般情况下,应该先考虑垂直拆分。垂直可以理解为分出来的库表结构是互相独立各不相同的。
如果有多个业务,每个业务直接关联性不大,那么就可以把每个业务拆分为单独的实例、库或表。
如果在一个实例上,有多个数据库,那么从分摊压力的角度考虑,可以把每个数据库拆分到单独的实例上。
如果在一个库里面有多张表,那么可以把每张表拆分到不同的实例上。
如果你有一张表,但这个表里的字段很多,每个字段都有不同的含义,例如user表里面有姓名、生日、地址等个人信息,那么当改表太大的时候,就可以把每个字段独立出来拆分为一张新表。例如user_birth表,可以只有两个字段:user_id、user_birthday。
水平拆分是针对一张表来说的。在经过垂直拆分之后,如果数据量仍然过大,例如注册用户已经超过了10亿,那只好通过某种算法进行水平拆分。拆分后的结果是多张具有相同表结构的表,每张表里面存储一部分数据。拆分的算法依据很多,例如通过id取模100、1024的方式,以及通过区分不同日期的方式等,如图24.1所示。
blog表通过取模100,分成了100份。log表通过每个月一个表进行按时间的水平拆分。feed表通过除以约定的值进行了水平拆分。
进行拆分的目的是,通过这样的做法降低MySQL的负载,把原本不支持分布式存储的MySQL实例转换为基于MySQL的分布式集群。在进行水平拆分之后,如果数据库压力还是非常大,那么可以把水平拆分的结果再进行垂直拆分。
例如上面的blog表。先通过水平拆分把一张blog表拆分为100份;然后,可以根据业务的需求,再通过垂直拆分,把一部分水平拆分后的blog_N表迁移出去。按照目前的设计,在极端情况下,可以把blog表分布在100台物理机器上,每台物理机器上承担blog表访问压力的百分之一。
这样的操作在MySQL Partitioning中是无法完成的。
此外,还有一种拆分方式是水平拆分之后的垂直拆分。
进行拆分的目的是,通过这样的做法降低MySQL的负载,把原本不支持分布式存储的MySQL实例转换为基于MySQL的分布式集群。在进行水平拆分之后,如果数据库压力还是非常大,那么可以把水平拆分的结果再进行垂直拆分。
例如上面的blog表。先通过水平拆分把一张blog表拆分为100份;然后,可以根据业务的需求,再通过垂直拆分,把一部分水平拆分后的blog_N表迁移出去。按照目前的设计,在极端情况下,可以把blog表分布在100台物理机器上,每台物理机器上承担blog表访问压力的百分之一。
这样的操作在MySQL Partitioning中是无法完成的。
从一般运维的角度来看,什么情况下需要考虑分库分表呢?
MySQL是关系型数据库,数据库表之间的关系从一定的角度上映射了业务逻辑。
任何分库分表的行为都会在某种程度上提升业务逻辑的复杂度,数据库除了承载数据的存储和访问外,协助业务更好地实现需求和逻辑也是其重要工作之一。
分库分表会带来数据的合并、查询或更新条件的分离,以及事务的分离等多种后果,业务实现的复杂程度往往会翻倍或指数级上升。所以,在分库分表之前,不要为分而分,而应该尽量去做其他力所能及的事情,例如升级硬盘、升级内存、升级CPU、升级网络、升级数据库版本、读写分离及负载均衡等。所有分库分表的前提是,这些已经尽力做好了。
这里说的运维,包括如下三种情况。
如果单表或单个实例太大,在做备份的时候就需要大量的磁盘IO或网络IO资源。例如1T的数据,网络传输占用50MB的时候,需要20000多秒才能传输完毕,整个过程中的维护风险都是高于平时的。
去哪儿网的做法是,给所有的数据库机器添加第二块网卡,用来做备份,或者SST、Group Communication等各种内部的数据传输。lT的数据备份,也会占用大量的磁盘IO,如果是SSD还好。
当然,这里忽略某些厂商的产品在集中IO的时候会出一些BUG的问题。如果是普通的物理磁盘,则在不限流的情况下去执行XtraBackup,但对于该实例来说基本不可用。
如果某个表过大,对此表做DDL的时候,MySQL会锁住全表,这个时间可能会很长,在这段时间内业务不能访问此表,影响甚大。解决的办法有类似某些大厂DBA自己改造的可以在线秒改表的方法,不过他们目前也只是能添加字段而已,对其他DDL还是无效;
或者使用pt-online-schema-change,当然在使用过程中,它需要建立触发器和影子表,同时也需要很长很长的时间,在此操作过程中的所有时间,都可以看作风险时间。
把数据表切分,使总量减小,有助于改善这种风险。
例如曾经有个表user_last_login_time,从名字可以看出来是记录用户最后一次登录时间信息的表。由于业务开展时的数据量比较小,并且考虑到每个注册用户只有一条记录,就没有分表。
但是随着业务爆发性增长,这张表的数据量很快达到了10亿。这时候,DAU在不到1亿的情况下,就有每天几个亿的update操作(同一个用户可能每天登陆好多次),虽然大部分都是简单的update操作,但由于更新太过频繁,也导致此表压力很大,经常出问题。
这个时候,又没有能力去修改源码,降低锁的粒度,那么只会把其中的数据物理拆开,用空间换时间,变相降低访问压力。这个案例中的热点表拆分,用到了水平拆分。
这里举一个例子,如果有一个名为users用户表,在最初设计的时候可能是如下这样的。
一般的users表会有很多字段,这里不再列举。如上表所示,在一个简单的应用中,这种设计是很常见的。但是,设想一下以下两种情况。
为了统计活跃用户,在每个人登录的时候都会记录一下他最近登录的时间。并且有的用户活跃得很,不断地去更新这个login_time,使这个表不断地被update,压力非常大。
那么,在这个时候,只要考虑对它进行拆分,站在业务的角度,最好的办法是先把last_login_time拆分出去,并将拆分出来的字段命名为user_last_login_time。
这样,业务的代码只有在用到这个字段的时候修改一下就好了。如果不这么做,直接把users表水平切分,那么所有访问users 表的地方都要修改。或许你会说:"我有 proxy,能够动态merge数据。"到目前为止,我还从没看到过proxy不影响性能的情况。
一开始的时候,有它没它无所谓。但是后来发现了两个问题,
那么,在所有人猎奇窥私心理的影响下,对此字段的访问量就会大幅度增加。这时,数据库压力瞬间抗不住了,只好考虑对这个表进行垂直拆分。
也就是,把这个字段加上用户的id单独独立出去。关于TEXT的拆分,这里多说一句,如果一个查询表里存在TEXT或BLOB字段,而且这个查询需要创建内部临时表的话,那它不能使用内存临时表,必须使用磁盘临时表,不然会对性能造成巨大影响,也会占用物理磁盘大量IO,最终导致性能剧烈下降。
一般情况下,这是必须要拆分的情形。
例子很好举,各种的评论、消息、日志记录。这个增长不是跟人口成比例的,而是不可控的,例如社交网站业务的feed广播,发一条消息会扩散给很多很多人。虽然主体可能只存一份,但不排除一些索引或路由有这种存储需求。这个时候,通过增加存储提升机器配置已经显得苍白无力,水平切分是最佳实践。拆分的标准很多,按用户的、按时间的、按用途的,不再一一举例。
这也算是拆分带来的意外惊喜吧。不要把所有鸡蛋放在一个篮子里,我们不希望数据库出问题,但出不出问题不是我们能决定的,或者可以这么说,出问题几乎是一定的。我能决定什么呢?我希望在出问题的时候不要影响到100%的用户,影响的比例越小越好,那么水平切分可以解决这个问题。
把用户、库存、订单等本来统一的资源切分掉,每个小的数据库实例承担一小部分业务,这样整体的可用性就会提升。这对Qunar这样的业务还是比较合适的,人与人之间、某些库存与库存之间关联不太大的时候,可以做一些这样的切分。
这一点跟原则四有些类似,主要是站在业务的层面上来看。火车票业务和烤羊腿业务是完全无关的业务,虽然每个业务的数据量可能不太大,放在一个MySQL实例中完全没问题,但是烤羊腿业务的DBA或开发人员水平很可能很差,动不动就给你出一些幺蛾子,直接把数据库搞挂。
这时,虽然火车票业务的人员技术很优秀,工作也很努力,但照样被老板打屁股。解决的办法很简单:惹不起,躲得起!
《三国演义》第一回:"话说天下大势,分久必合,合久必分。"其实在实践中,有时候可能原本要分,后来又发现分了还得合,分分合合,完全是现实的需求,随需而变才是王道,而DBA的价值也能在此体现。或分或合的情况太多,不能穷举。