#数据切分关系型数据库本身比较容易成为系统瓶颈,单机的存储容量、连接数、处理能力都是有限的。当单表数据量达到1000W或100G时,由于查询维度较多,即使加从库,优化索引,在做很多操作时,性能还是会严重下降。这时候就要考虑拆分了。拆分的目的是减轻数据库的负担,缩短查询时间。数据库分布的核心内容无外乎是数据分片,以及分片后数据的定位与整合。数据切分是将数据分散存储在多个数据库中,从而减少单个数据库的数据量,通过扩展主机数量来缓解单个数据库的性能问题,从而达到目的提高数据库的运行性能。根据数据切分的类型,数据切分可分为垂直(vertical)切分和水平(horizo??ntal)切分两种方式1、垂直(vertical)切分垂直切分通常分为两种:垂直数据库类和垂直表类.垂直分片是将关联度低的不同表按照业务耦合度存放在不同的数据库中。其做法类似于将一个大系统拆分成多个小系统,按业务分类独立划分。类似于“微服务治理”的方式,每个微服务使用一个单独的数据库。如图:垂直分表是基于数据库中的“列”。如果表中字段较多,可以创建扩展表,将不常用或字段长度较大的字段拆分到扩展表中。在字段多的情况下(比如大表有100多个字段),通过“大表拆小表”更容易开发和维护,也能避免跨页问题。MySQL底层是通过数据页来存储的。占用过多空间的记录会导致跨页,造成额外的性能开销。另外,数据库以行为单位加载数据到内存中,这样表中的字段长度越短,访问频率越高,内存可以加载更多的数据,命中率更高,磁盘IO减少,从而提高数据库性能。垂直切分的优点:解决业务系统层面的耦合。清晰的业务类似于微服务的治理。还可以对不同业务的数据进行分级管理、维护、监控和扩展。在垂直切分等高并发场景下,一定程度上增加了IO、数据库连接、单机硬件资源的瓶颈。缺点:部分表无法join,只能通过接口聚合解决,增加了开发的复杂度。Horizo??ntalsegmentation)2.Horizo??ntal(水平)切分当一个应用很难进行细粒度的垂直切分,或者切分后数据量巨大,在单库读写、存储等方面存在性能瓶颈。这时候就需要水平切分了。切片。水平切分分为数据库分表和分库分表。根据表中数据的内在逻辑关系,将同一张表根据不同的条件分布到多个数据库或多个表中。每个表只包含部分数据,这样单个表的数据量就变小了,达到了分布式的效果。如图:分库分表只是解决了单表数据量过大的问题,并没有将表分布到不同机器的库中。所以对于减轻MySQL数据库的压力帮助不大。竞争同一台物理机的CPU、内存、网络IO,最好通过分库分表来解决。横向切分的优点:不存在单库数据量过大、高并发的性能瓶颈,提高系统稳定性和负载能力应用端改造小,无需拆分业务模块。缺点:跨分片事务一致性难保证跨库join关联查询性能差。多次数据扩容困难,维护量大。横向切分后,同一张表会出现在多个数据库/表中,每个数据库/表的内容都不一样。几种典型的数据分片规则是:1.按照时间范围或ID范围按照值范围切分。例如:将不同月份甚至不同日期的数据按照日期分散到不同的库中;userId为1~9999的记录分配给第一个图书馆,userId为10000~20000的记录分配给第二个图书馆,依此类推。从某种意义上说,一些系统采用的“冷热数据分离”,将一些使用较少的历史数据迁移到其他库中,只提供业务功能的热数据查询,也是一种类似的做法。这样做的好处是:单表大小可控,横向扩展自然方便。如果以后要扩展整个shard集群,只需要增加节点,不需要从其他shard迁移数据。在使用分片字段进行范围搜索时,连续分片可以快速定位分片进行快速查询,有效避免跨分片查询的问题。缺点:热数据成为性能瓶颈。连续分片可能会有数据热点,比如按时间字段分片。有些分片存储的是最近一段时间的数据,可能会被频繁读写,而有些分片存储的是很少被查询到的历史数据。2、根据值的取模,一般采用hash取模的切分方法。比如Customer表根据cusno字段分4个库,第一个库取余0,第二个取余1。图书馆等。这样,同一个用户的数据就会分散在同一个库中。如果查询条件有cusno字段,可以明确定位到对应的库中进行查询。优点:数据分片比较均匀,不易出现热点和并发访问瓶颈。缺点:后期分片集群扩容时,需要迁移旧数据(使用一致性哈希算法可以更好的避免这个问题),容易面临跨分片查询的复杂问题。比如上面的例子,如果经常使用的查询条件不包含cusno,则无法定位到数据库,所以需要同时向四个数据库发起查询,然后合并内存中的数据,取最小的集合并将其返回给应用程序。图书馆反而变成了累赘。#分库分表带来的问题分库分表可以有效链接单机单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,但也会带来一些问题。这些技术挑战和相应的解决方案如下所述。1.事务一致性问题分布式事务当更新的内容同时分布在不同的数据库中时,必然会导致跨库事务问题。跨分片交易也是分布式交易,没有简单的解决方案。一般可以使用“XA协议”和“两阶段提交”进行处理。分布式事务可以最大化数据库操作的原子性。但提交交易时需要多个节点协同,延误了提交交易的时间点,延长了交易的执行时间。这导致事务访问共享资源时发生冲突或死锁的可能性更高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面横向扩展的桎梏。最终一致性对于那些对性能要求高但对一致性要求不高的系统,往往不需要系统的实时一致性。只要在允许的时间段内达到最终一致性,就可以使用事务补偿。不同于交易执行出错后立即回滚的方式,交易补偿是一种事后检查和修复的措施。一些常见的实现方式包括:数据的对账校验、基于日志的比对、定期与标准数据源比对同步等。交易补偿也要结合业务系统来考虑。2、跨节点关联查询join在问题划分之前,系统中很多列表和详情页需要的数据都可以通过sqljoin来完成。拆分后,数据可能分布在不同的节点上。这时候join带来的问题就比较麻烦了。考虑到性能,尽量避免使用连接查询。解决这个问题的一些方法:1)全局表全局表也可以看作是“数据字典表”,是系统中所有模块都可能依赖的一些表。为了避免跨库join查询,这样的表可以放在每个数据库中各保存一份。这些数据通常很少被修改,因此无需担心一致性问题。2)字段冗余是典型的反范式设计,以空间换时间,避免join查询以求性能。例如:在order表中保存userId时,也冗余保存了一份userName,这样查询订单明细时就不用去查询“买家用户表”了。但是这种方式适用场景有限,更适合依赖字段较少的情况。而且冗余字段的数据一致性也很难保证。就像上面订单表的例子,买家修改userName后,是否需要在历史订单中同步更新?这个也要结合实际业务场景考虑。3)数据在系统层面组装,查询分两次进行。在第一次查询的结果集中找到关联数据id,然后发起第二次请求根据id获取关联数据。最后,对获得的数据进行现场组装。4)在ER分片关系型数据库中,如果能够先确定表之间的关联关系,将具有关联关系的表记录存储在同一个分片中,则可以更好的避免cross-shardjoin问题。在1:1或者1:n的情况下,通常是根据主表的ID主键来划分。如下图所示:这样DataNode1上的order订单表和orderdetail订单明细表可以通过orderId部分关联查询,DataNode2上也是如此。3.跨节点分页、排序、功能问题跨节点、多数据库查询时,会出现限制分页、排序排序等问题。分页需要根据指定的字段进行排序。当排序字段为分片字段时,更容易通过分片规则定位到指定的分片;当排序字段不是分片字段时,它变得更复杂。需要先对不同分片节点中的数据进行排序返回,然后对不同分片返回的结果集进行汇总和重新排序,最后返回给用户。如图:上图只取了第一页的数据,对性能影响不大。但是如果获取的page数量很大,情况就复杂很多,因为每个shard节点的数据可能是随机的。为了排序的准确性,需要对所有节点的前N页数据进行排序进行合并,最后再进行整体排序,比较消耗CPU和内存资源,所以页数越大,系统性能越差将。在使用Max、Min、Sum、Count等函数进行计算时,也需要先在每个分片上执行相应的函数,然后对每个分片的结果集进行汇总和重新计算,最后返回结果。如图:4.全局主键规避问题在分库分表环境下,由于表中的数据同时存在于不同的数据库中,主键值的自增长usuallyused将无用,并且不能保证某个分区数据库ID的自生成数据是全局唯一的。因此需要单独设计全局主键,避免跨数据库主键重复。有一些常见的主键生成策略:1)UUIDUUID标准形式包含32个十六进制数,分为5段,36个字符,形式为8-4-4-4-12,例如:550e8400-e29b-41d4-a716-446655440000UUID是最简单的主键解决方案。本地生成,性能高,不费时网络。但缺点也很明显。因为UUID很长,会占用很大的存储空间;另外,建立索引作为主键,根据索引进行查询也会有性能问题。在InnoDB下,UUID的乱序会导致数据位置的频繁变化,导致分页。2)结合数据库维护主键ID表在数据库中创建序列表:CREATETABLE`sequence`(`id`bigint(20)unsignedNOTNULLauto_increment,`stub`char(1)NOTNULLdefault'',PRIMARYKEY(`id`),UNIQUEKEY`stub`(`stub`))ENGINE=MyISAM;存根字段设置为唯一索引,同一个存根值在序列表中只有一条记录,可以同时为多张表生成全局ID。序列表内容如下:+--------------------+------+|id|stub|+-------------------+------+|72157623227190423|a|+--------------------+------+使用MyISAM存储引擎代替InnoDB以获得更高的性能。MyISAM使用表级锁,对表的读写都是序列化的,所以并发时不用担心读取同一个ID值两次。当需要全局唯一的64位ID时,执行:REPLACEINTOsequence(stub)VALUES('a');SELECTLAST_INSERT_ID();这两个语句是Connection级别的,selectlast_insert_id()必须和replaceinto连接到同一个数据库才能得到刚刚插入的新ID。使用replaceinto而不是insertinto的好处是避免了表行过多,不需要定期清理。这种方案比较简单,但缺点也很明显:存在单点问题,对DB的依赖性强。当DB异常时,整个系统不可用。配置主从可以提高可用性,但是当主库挂掉,主从切换时,特殊情况下数据一致性很难保证。另外,性能瓶颈仅限于单个MySQL的读写性能。flickr团队使用的一种主键生成策略和上面的序列表方案类似,但是更好的解决了单点和性能瓶颈的问题。本方案的总体思路是:建立2台以上的服务器进行全局ID的生成,每台服务器上只部署一个数据库,每个数据库都有一个序列表记录当前的全局ID。表中ID增长的步长为数据库个数,起始值依次错开,这样ID的生成可以hash到每个数据库。如下图:ID由两台数据库服务器生成,设置不同的auto_increment值。第一个序列的起始值为1,每一步递增2,另一个序列的起始值为2,每一步递增2。这样一来,第一台机器生成的ID都是奇数(1,3,5,7...),第二台机器生成的ID都是偶数(2,4,6,8...)。该方案将生成ID的压力平均分配到两台机器上。同时提供系统容错能力。如果第一台机器出现错误,可以自动切换到第二台机器获取ID。但是,也有几个缺点:向系统中添加机器使得水平扩展更加复杂;每获取一个ID,都需要对DB进行读写,对DB的压力还是很大的,只能通过堆机来提升性能。可以在flickr方案的基础上继续优化,使用batch的方式减少数据库的写入压力,每次获取一个范围的身份证号,用完再去数据库获取,可以大大降低数据库的压力。如下图所示:仍然使用两个DB来保证可用性,数据库中只存储当前最大的ID。ID生成服务一次批量取6个ID,先将max_id改为5,应用访问ID生成服务时,不需要访问数据库,从0到5依次从编号开始分配ID段缓存。发送完这些ID后,将max_id改为11,下次分发6~11的ID。这样一来,数据库的压力降低到原来的1/6。3)Snowflake分布式自增ID算法Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位Long数。组成部分为:1、接下来的41位是毫秒级别的时间,41位的长度可以表示69年的时间,5位datacenterId,5位workerId。10位长度支持最多部署1024个节点。最后12位是毫秒内的计数。12位计数序列号支持每个节点每毫秒产生4096个ID序列。随时间趋势递增;不依赖第三方系统,稳定性和效率高。理论上QPS约为409.6w/s(1000*2^12),整个分布式系统不会发生ID冲突;自身业务可以灵活分配比特。缺点是对机器时钟有很强的依赖性,如果时钟拨回,可能会导致产生重复的ID。综上,结合数据库和snowflake独有的ID方案,可以参考业界比较成熟的方案:Leaf——美团点评分布式ID生成系统,兼顾了高可用、容灾、分布式等问题计时。5、数据迁移和扩容当业务快速发展,面临性能和存储瓶颈时,就会考虑分片设计。这时候就不可避免地要考虑历史数据迁移的问题。一般的做法是先读取历史数据,然后按照指定的分片规则将数据写入各个分片节点。另外需要根据当前数据量、QPS、业务发展速度进行容量规划,计算大概需要的分片数量(一般建议单表数据量单个分片不超过1000W)。如果采用数值范围分片,只需要增加节点扩容,不需要迁移分片数据。如果采用数值取模分片,考虑后期扩容问题相对麻烦。#什么时候考虑拆分我们来谈谈什么时候考虑数据拆分。1、尽量不要拆分。并非所有表都需要拆分。主要看数据的增长速度。切分会在一定程度上增加业务的复杂性。数据库除了承载数据的存储和查询之外,帮助业务更好的实现需求也是其重要的工作之一。除非万不得已,否则不要使用分库分表的大招,避免“过度设计”和“过早优化”。分库分表之前,不要为了分而分,先量力而行,比如:升级硬件,升级网络,读写分离,索引优化等。当数据量达到单表瓶颈,考虑分库分表。2、数据量过大,正常运维影响业务接入。这里所说的运维是指:1)数据库备份,如果单表太大,备份需要大量的磁盘IO和网络IO。比如1T的数据,当网络传输占用50MB时,需要20000秒才能完成传输。整个过程风险比较大。2)当对大表进行DDL修改时,MySQL会锁住整张表。很长一段时间,业务不能在这段时间访问这张表,影响很大。如果使用pt-online-schema-change,在使用过程中会创建触发器和影子表,耗时较长。在此操作期间,它被计为风险时间。拆分数据表并减少总量可以帮助降低这种风险。3)大表会被频繁访问和更新,更容易出现锁等待。将数据切分,以空间换时间,变相降低访问压力。3、随着业务的发展,部分领域需要垂直拆分。例如,如果项目一开始设计的用户表是这样的:idbigint#user'sIDnamevarchar#usernamelast_login_timedatetime#最近登录时间personal_infotext#Privateinformation.....#Otherinformationfields项目,这种设计满足简单的业务需求,便于快速迭代开发。当业务快速发展时,用户数量从10万增加到10亿,用户非常活跃。每次登录都会更新last_login_name字段,导致user表不断更新,压力很大。而其他字段:id、name、personal_info不变或很少更新。这时候从业务上来说,应该把last_login_time拆分出来,新建一个user_time表。personal_info属性更新查询频率较低,文本域占用空间大。这时候就需要对user_ext表进行垂直拆分。4、数据量的快速增长随着业务的快速发展,单表的数据量会不断增长。当性能接近瓶颈时,就要考虑水平切分,做分库分表。这时候就必须选择合适的切分规则,提前预估数据容量。5.安全性和可用性鸡蛋不应放在一个篮子里。在业务层面,垂直切分将不相关业务的数据库隔离开来,因为每个业务的数据量和访问量都不一样,不可能因为一个业务挂掉数据库而牵连到其他业务。采用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个数据库只承载部分业务数据,从而提高整体的可用性。#案例分析1.用户中心业务场景用户中心是一个很常见的业务,主要提供用户注册、登录、查询/修改等功能,其核心表为:User(uid,login_name,passwd,sex,age,nickname)uid是用户ID,主键login_name,passwd,sex,age,nickname,用户属性任何架构设计出来的业务都是流氓。分库分表前,需要梳理业务场景需求:1、用户端:前台Access,访问量大,需要保证高可用和高一致性。需求主要分为两类:用户登录:通过login_name/phone/email查询用户信息,1%的请求属于此类用户信息查询:登录后,通过uid查询用户信息,99%的请求属于此类2、运营端:后台接入,支持运营需求,按年龄、性别、登录时间、注册时间等进行分页查询。属于访问量小、可用性和一致性要求不高的内部系统。2.水平切分法当数据量越来越大时,需要对数据库进行水平切分。上述的分割方法包括“按取值范围”和“按取模值”。“按取值范围”:以主键uid作为划分依据,将数据按uid范围横向划分到多个数据库中。例如:user-db1存储uid范围为0~1000w的数据,user-db2存储uid范围为1000w~2000wuid的数据。优点是:容易扩容,如果容量不够,只需要增加一个新的db。缺点是:请求量不均匀,一般新注册用户的活跃度会比较高,所以newuser-db2的负载会比user-db1高,造成服务器利用率不均衡。“按值取模”:也是以主键uid作为划分依据,将数据按照uid取模的值横向划分到多个数据库中。例如:user-db1存储uid为模1的数据,user-db2存储uid为模0的uid数据。优点是:数据量和请求量分布均匀。不足之处是:扩容麻烦。当容量不够时,添加一个新的db需要rehash。需要考虑数据的平滑迁移。3、非uid查询方式进行横向切分后,可以很好的满足uid查询的需求,可以直接路由到具体的数据库。但是,如果通过非uid查询,比如login_name,就不知道要访问哪个库了。这时候就需要遍历所有的库,性能会大大降低。对于用户端,可以采用“建立非uid属性与uid的映射关系”的方案;对于运营端,可以采用“前后台分离”的方案。建立非uid属性到uid的映射关系1)映射关系例如:login_name不能直接定位到数据库中,可以建立login_name→uid的映射关系,存储到索引表或缓存中。访问login_name时,先通过映射表查询login_name对应的uid,再通过uid定位到具体的库。映射表只有两列,可以承载很多数据。当数据量太大时,也可以水平拆分映射表。这种kv格式的索引结构可以很好的利用缓存优化查询性能,而且映射关系不会频繁变化,缓存命中率高。2)Genome方法分库基因:如果使用uid将数据库分为8个数据库,使用uid%8方法进行路由,则uid的后3位将决定这行User数据落在哪个数据库进入。那么这3位就可以看作是分库基因。上述映射关系方式需要映射表额外存储,非uid字段查询时,需要多访问一次数据库或缓存。如果要消除冗余存储和查询,可以使用f函数将login_name的基因作为uid的分库基因。生成uid时,参考上述分布式唯一ID生成方案,加上最后3位值=f(login_name)。查询login_name时,只需要计算f(login_name)%8的值就可以定位到具体的库。但是,这需要提前进行容量规划,预估未来几年的数据量需要分成多少个数据库,并预留一定的分库基因。前景和背景分离。对于用户端,主要需求是单行查询。需要建立login_name/phone/email到uid的映射关系,可以解决这些字段的查询问题。在操作端,批量分页、各种条件的查询比较多。这种查询需要大量的计算,返回大量的数据,对数据库的性能消耗很大。此时,如果同一批服务或数据库共享给用户端,后台少量请求可能会占用大量数据库资源,导致用户端访问性能下降或超时。这类业务最好采用“前后台分离”的方案,在运营侧将后台业务抽取独立的service和db,解决与前台业务系统的耦合。由于运营端对可用性和一致性的要求不高,所以不需要访问实时库,而是通过binlog异步同步数据来访问运营库。在数据量大的情况下,也可以使用ES搜索引擎或者Hive来满足后台复杂的查询方式。#支持分库分表中间件站在巨人的肩膀上可以省很多力气。目前有一些成熟的分库分表开源方案:sharding-jdbc(当当网)TSharding(蘑菇街)Atlas(奇虎360)Cobar(阿里巴巴)MyCAT(基于Cobar)Oceanus(58.com)Vitess(谷歌)
