大型网站存储瓶颈(广义水平拆分)

来源:互联网 发布:阿里云ip地址 编辑:程序博客网 时间:2024/04/29 03:49

原文:http://blog.jobbole.com/84073/

一、简介

我把从技术角度来进行的数据库水平拆分,称之为数据库的狭义水平拆分;把从业务角度进行的数据库水平拆分称为数据库的广义水平拆分。如下所示的都是数据库的广义水平拆分:



本文主要讲数据库的狭义水平拆分。关于数据库的狭义水平拆分请参考《大型网站之存储瓶颈(狭义水平拆分)

淘宝是个大平台,它的交易表里一定是要记下所有商户的交易数据,但是针对单个商家他们只会关心自己的网店的销售数据,这就有一个问题了,如果某一个商家要查询自己的交易信息,淘宝就要从成千上万的交易信息里检索出该商家的交易信息,那么如果我们把所有交易信息放在一个交易表里,肯定有商家会有这样的疑问,我的网店每天交易额不大,为什么我查询交易数据的速度和那些大商家一样慢了?那么我们到底该如何是解决这样的场景了?


 碰到这样的情况,当网站的交易规模变大后就算我们把交易表做了读写分离估计也是没法解决实际的问题,就算我们做的彻底点把交易表垂直拆分出来估计还是解决不了问题,因为一个业务数据库拥有很多张表,但是真正压力大的表毕竟是少数,这个符合28原则,而数据库大部分的关键问题又都是在那些数据压力大的表里,就算我们把这些表单独做读写分离甚至做垂直拆分,其实只是把数据库最大的问题迁移出原来数据库,而不是在解决该表的实际问题。

二、从时间维度考虑

如果我们要解决交易表的问题我们首先要对交易表做业务级的拆分,那么我们要为交易表增加一个业务维度:实时交易和历史交易,一般而言实时交易以当天及当天24小时为界,历史交易则是除去当天交易外的所有历史交易数据。实时交易数据和历史交易数据有着很大不同,实时交易数据读与写是比较均衡的,很多时候估计写的频率会远高于读的频率,但是历史交易表这点上和实时交易就完全不同了,历史交易表的读操作频率会远大于写操作频率,如果我们将交易表做了实时交易和历史交易的拆分后,那么读写分离方案适合的场景是历史交易查询而非实时交易查询,不过历史交易表的数据是从实时交易表里同步过来的,根据这两张表的业务特性,我们可以按如下方案设计,具体如下:


们可以把实时交易表设计成两张表,把它们分别叫做a表和b表,a表和b表按天交替进行使用,例如今天我们用a表记录实时交易,明天我们就用b表记录实时交易,当然我们事先可以用个配置表记录今天到底使用那张表进行实时交易记录,为什么要如此麻烦的设计实时交易表了?这么做的目的是为了实时交易数据同步到历史数据时候提供便利,一般我们会在凌晨0点切换实时交易表,过期的实时交易表数据会同步到历史交易表里,这个时候需要数据迁移的实时交易表是全表数据迁移,效率是非常低下,假如实时交易表的数据量很大的时候,这种导入同步操作会变得十分耗时,所以我们设计两张实时交易表进行切换来把数据同步的风险降到最低。由此可见,历史交易表每天基本都只做一次写操作,除非同步出了问题,才会重复进行写操作,但是写的次数肯定是很低的,所以历史交易表的读写比例失衡是非常严重的。不过实时交易表的切换也是有技术和业务风险的,为了保证实时交易表的高效性,我们一般在数据同步操作成功后会清空实时交易表的数据,但是我们很难保证这个同步会不会有问题,因此同步时候我们最好做下备份,此外,两个表切换的时候肯定会碰到这样的场景,就是有人在凌晨0点前做了交易,但是这个交易是在零点后做完,假如实时交易表会记录交易状态的演变过程,那么在切换时候就有可能两个实时表的数据没有做好接力,因此我们同步到历史交易表的数据一定要保持一个原则就是已经完成交易的数据,没有完成的交易数据两张实时交易还要完成一个业务上的接力,这就是业界常说的数据库日切的问题。

  历史交易表本身就是为读使用的,所以我们从业务角度将交易表拆分成实时交易表和历史交易表本身就是在为交易表做读写分离,居然了设计了历史交易表我们就做的干脆点,把历史交易表做垂直拆分,将它从原数据库里拆分出来独立建表,随着历史交易的增大,上文里所说的某个商户想快速检索出自己的数据的难题并没有得到根本的改善,为了解决这个难题我们就要分析下难题的根源在那里。这个根源很简单就是我们把所有商户的数据不加区别的放进了一张表里,不管是交易量大的商户还是交易量小的商户,想要查询出自己的数据都要进行全表检索,而关系数据库单表记录达到一定数据量后全表检索就会变的异常低效,例如DB2当数据量超过了1亿多,mysql单表超过了100万条后那么全表查询这些表的记录都会存在很大的效率问题,那么我们就得对历史交易表进一步拆分,因为问题根源是单表数据量太大了,那我们就可以对单表的数据进行拆分,把单表分成多表,这个场景就和前面说的水平拆分里把原表变成逻辑表,原表的数据分散到各个独立的逻辑表里的方式一致,不过这里我们没有一开始做水平拆分,那是会把问题变麻烦,我们只要在一个数据库下对单表进行拆分即可,这样也能满足我们的要求,并且避免了水平拆分下的跨库写作的难题。接下来我们又有一个问题了那就是我们按什么维度拆分这张单表呢?

  我们按照前文讲到的水平拆分里主键设计方案执行吗?当然不行哦,因为那些方案明显提升不了商户检索数据的效率问题,所以我们要首先分析下商户检索数据的方式,商户一般会按这几个维度检索数据,这些维度分别是:商户号、交易时间、交易类型,当然还有其他的维度,我这里就以这三个维度为例阐述下面的内容,商户查询数据效率低下的根本原因是全表检索,其实商户查询至少有一个维度那就是商户号来进行查询,如果我们把该商户的数据存入到一张单独的表里,自然查询的效率会有很大的提升,但是在实际系统开发里我们很少通过商户号进行拆分表,这是为什么呢?因为一个电商平台的商户是个动态的指标,会经常发生变化,其次,商户号的粒度很细,如果使用商户号拆分表的必然会有这样的后果那就是我们可能要频繁的建表,随着商户的增加表的数量也会增加,造成数据的碎片化,同时不同的商户交易量是不一样的,按商户建表会造成数据存储的严重不平衡。如果使用交易类型来拆分表,虽然维度的粒度比商户号小,但是会造成数据的分散化,也就是说我们查询一个商户的全部交易数据会存在很大问题。由此可见拆表时候如何有效的控制维度的粒度以及数据的聚集度是拆分的关键所在,因为使用交易时间这个维度就会让拆分更加合理,不过时间的维度的设计也是很有学问的,下面我们看看腾讯分析的维度,如下所示:



腾讯分析的维度是今天这个其实相当于实时交易查询,除此之外都是对历史数据查询,它们分为昨天、最近7天和最近30天,我们如果要对历史交易表进行拆分也是可以参照腾讯分析的维度进行,不过不管我们选择什么维度拆分数据,那么都是牺牲该维度成全了其他维度,例如我们按腾讯分析的维度拆分数据,那么我们想灵活使用时间查询数据将会受到限制。
三、从用户数据体量考虑
我们把历史交易数据通过交易时间维度进行了拆分,虽然得到了效率提升,但是历史交易数据表是个累积表,随着时间推移,首先是月表,接下来是周表都会因为数据累积产生查询效率低下的问题,这个时候我们又该如何解决了?这个时候我们需要再引进一个维度,那么这个时候我们可以选择商户号这个维度,但是商户号作为拆分维度是有一定问题的,因为会造成数据分布不均衡,那么我们就得将维度的粒度由小变粗,其实一个电商平台上往往少数商户是完成了大部分电商平台的交易,因此我们可以根据一定指标把重要商户拆分出来,单独建表,这样就可以平衡了数据的分布问题。一个电商平台下会接入很多不同的商户,但是不同的商户每天产生的交易量是不同,也就是说商户的维度会让我们使交易数据变得严重的不均衡,可能电商平台下不到5%的商户完成了全天交易量的80%,而其他95%的商户仅仅完成20%的交易量,但是作为业务系统的数据表,进行读操作首先被限制和约束的条件就是商户号,如果要为我们设计的实时交易表进行狭义的水平拆分,做拆分前我们要明确这个拆分是由交易量大的少量商户所致,而不是全部的商户所致的。如果按照均匀分布主键的设计方案,不加商户区分的分布数据,那么就会发生产生少量交易数据的商户的查询行为也要承受交易量大的商户数据的影响,而能产生大量交易数据的商户也没有因为自己的贡献度而得到应有的高级服务,碰到这个问题其实非常好解决,就是在做狭义水平拆分前,我们先做一次广义的水平拆分,把交易量大的商户交易和交易量小的商户交易拆分出来,交易量小的商户用一张表记录,这样交易量小的商户也会很happy的查询出需要的数据,心里也是美滋滋的。接下来我们就要对交易量大的商户的交易表开始做狭义的水平拆分了,为这些重点商户做专门的定制化服务。

四、分页查询与排序

做实时查询的标准做法就是分页查询了,在讲述如何解决分页查询前,我们看看我们在淘宝里搜索【衣服】这个条件的分页情况,如下图所示:



我们看到一共才100页,淘宝上衣服的商品最多了,居然搜索出来的总页数只有100页,这是不是在挑战我们的常识啊,淘宝的这个做法也给我们在实现水平拆分后如何做分页查询一种启迪。要说明这个启迪前我们首先要看看传统的分页是如何做的,传统分页的做法是首先使用select count(1) form table这样的语句查询出需要查询数据的总数,然后再根据每页显示的记录条数,查询出需要显示的记录,然后页面根据记录总数,每页的条数,和查询的结果来完成分页查询。回到我们的交易表实例里,有一个重要商户在做实时交易查询,可是这个时候该商户已经产生了1千万笔交易了,假如每页显示10条,记录那么我们就要分成100万页,这要是真显示在页面上,绝对能让我们这些开发人员像哥伦布发现新大陆那样惊奇,反正我见过的最多分页也就是200多页,还是在百度搜索发现的。其实当数据库一张表的数据量非常大的时候,select的count查询效率就非常低下,这个查询有时也会近似个全表检索,所以count查询还没结束我们就会失去等待结果的耐心了,更不要是说等把数据查询出来了,所以这个时候我们可以学习下淘宝的做法,当商户第一次查询我们准许他查询有限的数据。


我自己所做的一个项目的做法就是这样的,当某个商户的交易量实在是很大时候我们其实不会计算数据的总笔数,而是一次性查询出1000条数据,这1000条数据查询出来后存入到缓存里,页面则只分100页,当用户一定要查询100页后的数据,我们再去追加查询,不过实践下来,商户基本很少会查询100页后的数据,常常看了5,6页就会停止查询了。不过商户也时常会有查询全部数据的需求,但是商户有这种需求的目的也不是想在分页查询里看的,一般都是为了比对数据使用的,这个时候我们一般是提供一个发起下载查询全部交易的功能页面,商户根据自己的条件先发起这样的需求,然后我们系统会在后台单独起个线程查询出全部数据,生成一个固定格式的文件,最后通过一些有效手段通知商户数据生成好了,让商户下载文件即可。

  对于进行了狭义水平拆分的表做分页查询我们通常都不会是全表查询,而是抽取全局的数据的一部分结果呈现给用户,这个做法其实和很多市场调查的方式类似,市场调查我们通常是找一些样本采集相关数据,通过分析这些样本数据推导出全局的一个发展趋势,那么这些样本选择的合理性就和最终的结论有很大关系,回到狭义水平拆分的表做分页查询,我们为了及时满足用户需求,我们只是取出了全部数据中的一部分,但是这一部分数据是否满足用户的需求,这个问题是很有学问的,如果是交易表,我们往往是按时间先后顺序查询部分数据,所以这里其实使用到了一个时间的维度,其他业务的表可能这个维度会不一样,但肯定是有个维度约束我们到底返回那些部分的数据。这个维度可以用一个专有的名词指代那就是排序,具体点就是要那个字段进行升序还是降序查询,看到这里肯定有人会有异议,那就是这种抽样式的查询,肯定会导致查询的命中率的问题,即查出来的数据不一定全部都是我们要的,其实要想让数据排序正确,最好就是做全量排序,但是一到全量排序那就是全表查询,做海量数据的全表排序查询对于分页这种场景是无法完成的。回到淘宝的例子,我们相信淘宝肯定没有返回全部数据,而是抽取了部分数据分页,也就是淘宝查询时候加入了维度,每个淘宝的店家都希望自己的商户放在搜索的前列,那么淘宝就可以让商家掏钱,付了钱以后淘宝改变下商家在这个维度里的权重,那么商家的商品就可以排名靠前了。

  狭义水平拆分的本身对排序也有很大的影响,水平拆分后我们一个分页查询可能要从不同数据库不同的物理表里去取数据,单表下我们可以先通过数据库的排序算法得到一定的数据,但是局部的排序到了全局可能就不正确了,这个又该怎么办了?其实由上面内容我们可以知道要满足对海量数据的所有查询限制是非常难的,时常是根本就无法满足,我们只能做到尽量多满足些查询限制,也就是海量查询只能做到尽量接近查询限制的条件,而很难完全满足,这个时候我前面提到的主键分布方案就能起到作用了,我们在设计狭义水平拆分表主键分布时候是尽量保持数据分布均衡,那么如果我们查询要从多张不同物理表里取的时候,例如我们要查1000条数据,而狭义水平拆分出了两个物理数据库,那么我们就可以每个数据库查询500条,然后在服务层归并成1000条数据,在服务层排序,这种场景下如果我们的主键设计时候还包含点业务意义,那么这个排序的精确度就会得到很大提升。假如用户对排序不敏感,那就更好做了,分页时候如果每页规定显示10条,我们可以把10条数据平均分配给两个数据库,也就是显示10条A库的数据,再显示5条B库的数据。
1 0
原创粉丝点击