转自:http://www.ywnds.com/?p=7239算法
关系型数据库自己比较容易成为系统性能瓶颈,单机存储容量、链接数、处理能力等都颇有限,数据库自己的“有状态性”致使了它并不像Web和应用服务器那么容易扩展。在互联网行业海量数据和高并发访问的考验下,聪明的技术人员提出了分库分表技术(有些地方也称为Sharding、分片)。同时,流行的分布式系统中间件(例如MongoDB、ElasticSearch等)均自身友好支持Sharding,其原理和思想都是大同小异的。sql
目前针对海量数据的优化,其分库分表是MySQL永远的话题,通常状况下认为MySQL是个简单的数据库,在数据量大到必定程度以后处理查询的效率下降,若是须要继续保持高性能运转的话,必须分库或者分表了。关于数据量达到多少大是个极限这个事儿,本文先不讨论,研究源码的同窗已经证明MySQL或者Innodb内部的锁粒度太大的问题大大限制了MySQL提供QPS的能力或者处理大规模数据的能力。在这点上,通常的使用者只好坐等官方不断推出的优化版本了。shell
在通常运维的角度来看,咱们什么状况下须要考虑分库分表?数据库
首先说明,这里所说的分库分表是指把数据库数据的物理拆分到多个实例或者多台机器上去,而不是相似分区表的原地切分。安全
原则零:能不分就不分服务器
是的,MySQL 是关系数据库,数据库表之间的关系从必定的角度上映射了业务逻辑。任何分库分表的行为都会在某种程度上提高业务逻辑的复杂度,数据库除了承载数据的存储和访问外,协助业务更好的实现需求和逻辑也是其重要工做之一。分库分表会带来数据的合并,查询或者更新条件的分离,事务的分离等等多种后果,业务实现的复杂程度每每会翻倍或者指数级上升。因此,在分库分表以前,不要为分而分,去作其余力所能及的事情吧,例如升级硬件,升级,升级网络,升级数据库版本,读写分离,负载均衡等等。全部分库分表的前提是,这些你已经尽力了。网络
原则一:数据量太大,正常的运维影响正常业务访问架构
这里说的运维,例如:并发
1)对数据库的备份。若是单表或者单个实例太大,在作备份的时候须要大量的磁盘IO或者网络IO资源。例如1T的数据,网络传输占用50MB的时候,须要20000秒才能传输完毕,在此整个过程当中的维护风险都是高于平时的。咱们在Qunar的作法是给全部的数据库机器添加第二块网卡,用来作备份,或者SST,Group Communication等等各类内部的数据传输。1T的数据的备份,也会占用大量的磁盘IO,若是是SSD还好,固然这里忽略某些厂商的产品在集中IO的时候会出一些BUG的问题。若是是普通的物理磁盘,则在不限流的状况下去执行xtrabackup,该实例基本不可用。负载均衡
2)对数据表的修改。若是某个表过大,对此表作DDL的时候,MySQL会锁住全表,这个时间可能很长,在这段时间业务不能访问此表,影响甚大。解决的办法有相似腾讯游戏DBA本身改造的能够在线秒改表,不过他们目前也只是能添加字段而已,对别的DDL仍是无效;或者使用pt-online-schema-change,固然在使用过程当中,它须要创建触发器和影子表,同时也须要很长很长的时间,在此操做过程当中的全部时间,均可以看作是风险时间。把数据表切分,总量减少,有助于改善这种风险。
3)整个表热点,数据访问和更新频繁,常常有锁等待,你又没有能力去修改源码,下降锁的粒度,那么只会把其中的数据物理拆开,用空间换时间,变相下降访问压力。
原则二:表设计不合理,须要对某些字段垂直拆分
这里举一个例子,若是你有一个用户表,在最初设计的时候多是这样:
1
2
3
4
5
|
id bigint #用户的ID
name varchar #用户的名字
last_login_time datetime #最近登陆时间
personal_info text #私人信息
xxxxx #其余信息字段
|
通常的users表会有不少字段,我就不列举了。如上所示,在一个简单的应用中,这种设计是很常见的。可是:
设想状况一:你的业务中彩了,用户数从100w飙升到10个亿。你为了统计活跃用户,在每一个人登陆的时候都会记录一下他的最近登陆时间。而且的用户活跃得很,不断的去更新这个login_time,搞的你的这个表不断的被update,压力很是大。那么,在这个时候,只要考虑对它进行拆分,站在业务的角度,最好的办法是先把last_login_time拆分出去,咱们叫它 user_time。这样作,业务的代码只有在用到这个字段的时候修改一下就好了。若是你不这么作,直接把users表水平切分了,那么,全部访问users表的地方,都要修改。或许你会说,我有proxy,可以动态merge数据。到目前为止我还从没看到谁家的proxy不影响性能的。
设想状况二:personal_info这个字段原本没啥用,你就是让用户注册的时候填一些我的爱好而已,基本不查询。一开始的时候有它没它无所谓。可是到后来发现两个问题,一,这个字段占用了大量的空间,由于是text嘛,有不少人喜欢长篇大论地介绍本身。更糟糕的是二,不知道哪天哪一个产品经理心血来潮,说容许我的信息公开吧,以方便让你们更好的相互了解。那么在全部人猎奇窥私心理的影响下,对此字段的访问大幅度增长。数据库压力瞬间抗不住了,这个时候,只好考虑对这个表的垂直拆分了。
原则三:某些数据表出现了无穷增加
例子很好举,各类的评论,消息,日志记录。这个增加不是跟人口成比例的,而是不可控的,例如微博的feed的广播,我发一条消息,会扩散给不少不少人。虽然主体可能只存一份,但不排除一些索引或者路由有这种存储需求。这个时候,增长存储,提高机器配置已经苍白无力了,水平切分是最佳实践。拆分的标准不少,按用户的,按时间的,按用途的,不在一一举例。
原则四:安全性和可用性的考虑
这个很容易理解,鸡蛋不要放在一个篮子里,我不但愿个人数据库出问题,但我但愿在出问题的时候不要影响到100%的用户,这个影响的比例越少越好,那么,水平切分能够解决这个问题,把用户,库存,订单等等原本同统一的资源切分掉,每一个小的数据库实例承担一小部分业务,这样总体的可用性就会提高。这对Qunar这样的业务仍是比较合适的,人与人之间,某些库存与库存之间,关联不太大,能够作一些这样的切分。
原则五:业务耦合性考虑
这个跟上面有点相似,主要是站在业务的层面上,咱们的火车票业务和烤羊腿业务是彻底无关的业务,虽然每一个业务的数据量可能不太大,放在一个MySQL实例中彻底没问题,可是极可能烤羊腿业务的DBA 或者开发人员水平不好,动不动给你出一些幺蛾子,直接把数据库搞挂。这个时候,火车票业务的人员虽然技术很优秀,工做也很努力,照样被老板打屁股。解决的办法很简单:惹不起,躲得起。
垂直拆分常见有垂直分库和垂直分表两种。垂直分表在平常开发和设计中比较常见,通俗的说法叫作“大表拆小表”,拆分是基于关系型数据库中的“列”(字段)进行的。一般状况,某个表中的字段比较多,能够新创建一张“扩展表”,将不常用或者长度较大的字段拆分出去放到“扩展表”中,以下图所示:
在字段不少的状况下,拆分开确实更便于开发和维护(笔者曾见过某个遗留系统中,一个大表中包含100多列的)。某种意义上也能避免“跨页”的问题(MySQL、MSSQL底层都是经过“数据页”来存储的,“跨页”问题可能会形成额外的性能开销,这里不展开,感兴趣的朋友能够自行查阅相关资料进行研究)。
拆分字段的操做建议在数据库设计阶段就作好。若是是在发展过程当中拆分,则须要改写之前的查询语句,会额外带来必定的成本和风险,建议谨慎。
垂直分库是根据数据库里面的数据表的相关性进行拆分,好比:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分能够把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按经常使用字段和很是用字段进行拆分,每一个表里面的数据记录数通常状况下是相同的,只是字段不同,使用主键关联。
另外,在“微服务”盛行的今天已经很是普及了,按照业务模块来划分出不一样的数据库,也是一种垂直拆分。而不是像早期同样将全部的数据表都放到同一个数据库中。以下图:
垂直拆分优势:
垂直拆分缺点:
垂直拆分小结:
系统层面的“服务化”拆分操做,可以解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制相似,咱们也能对不一样业务类型的数据进行“分级”管理、维护、监控、扩展等。
众所周知,数据库每每最容易成为应用系统的瓶颈,而数据库自己属于“有状态”的,相对于Web和应用服务器来说,是比较难实现“横向扩展”的。数据库的链接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库必定程度上可以突破IO、链接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。
而后,不少人并无从根本上搞清楚为何要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的作法。致使拆分后遇到不少问题(例如:跨库join,分布式事务等)。
水平拆分是经过某种策略将数据分片来存储,分为库内分表和分库分表两部分,每片数据会分散到不一样的MySQL表或库,达到分布式的效果,可以支持很是大的数据量。
库内分表,仅仅是单纯的解决了单一表数据过大的问题,因为没有把表的数据分布到不一样的机器上,所以对于减轻 MySQL 服务器的压力来讲,并无太大的做用,你们仍是竞争同一个物理机上的 IO、CPU、网络,这个就要经过分库分表来解决。
最多见的方式就是经过主键或者时间等字段进行Hash和取模后拆分。以下图所示:
当下分表有静态分表和动态分表两种:
静态分表:事先估算出表能达到的量,而后根据每个表须要存多少数据直接算出须要建立表的数量。如:1亿数据每个表100W条数据那就要建100张表,而后经过必定的hash算法计算每一条数据存放在那张表。其实就有点像是使用partition table同样。静态分表有一个毙命就是当分的那么多表还不知足时,须要再扩展难度和成本就会很高。
动态分表:一样也是对大数据量的表进行拆分,他能够避免静态分表带来的后遗症。固然也须要在设计上多一些东西(这每每是咱们能接受的)。
某种意义上来说,有些系统中使用的“冷热数据分离”(将一些使用较少的历史数据迁移到其余的数据库中。而在业务功能上,一般默认只提供热点数据的查询),也是相似的实践。在高并发和海量数据的场景下,分库分表可以有效缓解单机和单库的性能瓶颈和压力,突破IO、链接数、硬件资源的瓶颈。固然,投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战(例如:跨分片的复杂查询,跨分片事务等)。
水平拆分优势:
水平拆分缺点:
垂直分库带来的问题和解决思路:
在拆分以前,系统中不少列表和详情页所需的数据是能够经过sql join来完成的。而拆分后,数据库多是分布式在不一样实例和不一样的主机上,join将变得很是麻烦。并且基于架构规范,性能,安全性等方面考虑,通常是禁止跨库join的。那该怎么办呢?首先要考虑下垂直分库的设计问题,若是能够调整,那就优先调整。若是没法调整的状况,下面笔者将结合以往的实际经验,总结几种常见的解决思路,并分析其适用场景。
跨库Join的几种解决思路
全局表
所谓全局表,就是有可能系统中全部模块均可能会依赖到的一些表。比较相似咱们理解的“数据字典”。为了不跨库join查询,咱们能够将这类表在其余每一个数据库中均保存一份。同时,这类数据一般也不多发生修改(甚至几乎不会),因此也不用太担忧“一致性”问题。
字段冗余
这是一种典型的反范式设计,在互联网行业中比较常见,一般是为了性能来避免join查询。
举个电商业务中很简单的场景:“订单表”中保存“卖家Id”的同时,将卖家的“Name”字段也冗余,这样查询订单详情的时候就不须要再去查询“卖家用户表”。
字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场景也比较有限,比较适合依赖字段较少的状况。最复杂的仍是数据一致性问题,这点很难保证,能够借助数据库中的触发器或者在业务代码层面去保证。固然,也须要结合实际业务场景来看一致性的要求。就像上面例子,若是卖家修改了Name以后,是否须要在订单信息中同步更新呢?
数据同步
定时A库中的tab_a表和B库中tbl_b有关联,能够定时将指定的表作同步。固然,同步原本会对数据库带来必定的影响,须要性能影响和数据时效性中取得一个平衡。这样来避免复杂的跨库查询。笔者曾经在项目中是经过ETL工具来实施的。
系统层组装
在系统层面,经过调用不一样模块的组件或者服务,获取到数据并进行字段拼装。提及来很容易,但实践起来可真没有这么简单,尤为是数据库设计上存在问题但又没法轻易调整的时候。具体状况一般会比较复杂。
按业务拆分数据库以后,不可避免的就是“分布式事务”的问题。想要了解分布式事务,就须要了解“XA接口”和“两阶段提交”。值得提到的是,MySQL5.5x和5.6x中的xa支持是存在问题的,会致使主从数据不一致。直到5.7x版本中才获得修复。Java应用程序能够采用Atomikos框架来实现XA事务(J2EE中JTA)。感兴趣的读者能够自行参考《分布式事务一致性解决方案》,连接地址:http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
在不少中小项目中,咱们每每直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不一样的分片上,不能再借助数据库自增加特性直接生成,不然会形成不一样分片上的数据表主键会重复。简单介绍下使用和了解过的几种ID生成算法。
1. Twitter的Snowflake(又名“雪花算法”)
2. UUID/GUID(通常应用程序和数据库均支持)
3. MongoDB ObjectID(相似UUID的方式)
4. Ticket Server(数据库生存方式,Flickr采用的就是这种方式)
其中,Twitter的Snowflake算法是近几年在分布式系统项目中使用最多的,未发现重复或并发的问题。该算法生成的是64位惟一Id(由41位的timestamp+10位自定义的机器码+13位累加计数器组成)。这里不作过多介绍,感兴趣的读者可自行查阅相关资料。
在开始分片以前,咱们首先要肯定分片字段(也可称为“片键”)。不少常见的例子和场景中是采用ID或者时间字段进行拆分。这也并不绝对的,个人建议是结合实际业务,经过对系统中执行的sql语句进行统计分析,选择出须要分片的那个表中最频繁被使用,或者最重要的字段来做为分片字段。
常见的分片策略有随机分片和连续分片这两种,以下图所示:
当须要使用分片字段进行范围查找时,连续分片能够快速定位分片进行高效查询,大多数状况下能够有效避免跨分片查询的问题。后期若是想对整个分片集群扩容时,只须要添加节点便可,无需对其余分片的数据进行迁移。可是,连续分片也有可能存在数据热点的问题,就像图中按时间字段分片的例子,有些节点可能会被频繁查询压力较大,热数据节点就成为了整个集群的瓶颈。而有些节点可能存的是历史数据,不多须要被查询到。
随机分片其实并非随机的,也遵循必定规则。一般,咱们会采用Hash取模的方式进行分片拆分,因此有些时候也被称为离散分片。随机分片的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。可是,后期分片集群扩容起来须要迁移旧的数据。使用一致性Hash算法可以很大程度的避免这个问题,因此不少中间件的分片集群都会采用一致性Hash算法。离散分片也很容易面临跨分片查询的复杂问题。
不多有项目会在初期就开始考虑分片设计的,通常都是在业务高速发展面临性能和存储的瓶颈时才会提早准备。所以,不可避免的就须要考虑历史数据迁移的问题。通常作法就是经过程序先读出历史数据,而后按照指定的分片规则再将数据写入到各个分片节点中。
此外,咱们须要根据当前的数据量和QPS等进行容量规划,综合成本因素,推算出大概须要多少分片(通常建议单个分片上的单表数据量不要超过1000W)。
若是是采用随机分片,则须要考虑后期的扩容问题,相对会比较麻烦。若是是采用的范围分片,只须要添加节点就能够自动扩容。
通常来说,分页时须要按照指定字段进行排序。当排序字段就是分片字段的时候,咱们经过分片规则能够比较容易定位到指定的分片,而当排序字段非分片字段的时候,状况就会变得比较复杂了。为了最终结果的准确性,咱们须要在不一样的分片节点中将数据进行排序并返回,并将不一样分片返回的结果集进行汇总和再次排序,最后再返回给用户。以下图所示:
上面图中所描述的只是最简单的一种状况(取第一页数据),看起来对性能的影响并不大。可是,若是想取出第10页数据,状况又将变得复杂不少,以下图所示:
有些读者可能并不太理解,为何不能像获取第一页数据那样简单处理(排序取出前10条再合并、排序)。其实并不难理解,由于各分片节点中的数据多是随机的,为了排序的准确性,必须把全部分片节点的前N页数据都排序好后作合并,最后再进行总体的排序。很显然,这样的操做是比较消耗资源的,用户越日后翻页,系统性能将会越差。
在使用Max、Min、Sum、Count之类的函数进行统计和计算的时候,须要先在每一个分片数据源上执行相应的函数处理,而后再将各个结果集进行二次处理,最终再将处理结果返回。以下图所示:
<摘之>
周彦伟:http://mp.weixin.qq.com/s/BI2P45pnzUceCSQWU961zA
丁浪:http://www.infoq.com/cn/articles/key-steps-and-likely-problems-of-split-table