架构设计01——数据库拆分实现数据库能力线性扩展
我们公司前几年的核心系统,平均80人左右开发至今已经8年多了,至今还在维护,在全国20多个省部署了上千个点的运行,运行六七年后数据量上来了,结果平均每天都要出现宕机情况,90%以上都是数据库的原因,客户对我们的满意度急剧下降,可见数据库性能前期如果不设计好,后来带来的问题真的是灾难性的,虽然近些年各种存储技术层出不穷,但关系型数据库还是各种业务系统的核心,这篇文章详细讲讲我们在数据库性能方面如何实现线性扩展。
一、读写分离
最典型的场景就是单一数据库存储全部数据,数据量少、并发量少的时候没问题,数据量和并发量上来了就出现了问题,由于写(增删改)操作会对数据库上锁,数据量大导致写入较慢,或写入操作较多时候,导致读操作阻塞时间较长,从而引起性能下降。最常见、最有效、也是最容易实施的方案就是读写分离,讲读操作和写操作分离。
读写分离有2个关键点,一个是数据复制延迟,一个是应用访问:
1、数据复制延迟,主数据库写入数据后通过复制实现和从数据库同步,期间一定会有延迟,比较快的也就秒级同步,1s延迟都算快的,数据量大甚至可能达到分钟级别,所以对实时性要求特别特别高的应用就要好好考虑了,小心出现刚注册完,登录时提示没注册的现象。
应对复制延迟也有很多方法,比如对实时性要求高的操作在主库上进行,实时要求不高的放到从库上,也就是部分读写分离;再比如从库读失败了再从主库读一次等。
2、应用访问的便捷性,原来一个数据源的时候,应用直接写sql执行就行了,现在数据库集群了,不可能让应用自行分辨去查询哪个数据库,这时候要加一层数据访问层了,一般有两种方式,简单点的是代码中直接写,比如用hibernate访问数据库,简单封装一下hibernate就行,或者用一些组件(如淘宝的TDDL)等,如下图:
复杂一点的,就是使用数据库中间件,数据库中间件是一套独立的系统,业务系统无需自行管理读取哪个库,正常发送sql执行就行了,中间件将sql路由到要执行的数据库上,不过通常中间件比较复杂,能不能符合自己的需求还要自行测试,如下图:
读写分离后,拓展了数据库对读的处理能力,整体上也大大提高了数据库的读写能力,而且由于库分离后,可以针对库做不同的优化,比如在写库上减少索引,在读库上增加索引等。读写分离比较适合读多写少的操作,随着访问量增大,读的库可以水平扩展,大大提高了读写操作的压力,但却没有分散存储的压力,当数据量达到千万或亿条时候,单台数据库存储就会成为瓶颈,写操作就会极慢,索引维护也会时间很长,备份恢复也会很耗时间等。
二、分库
数据量大了,最常见,最直接的办法就是拆分库表,将一个大库拆成多个小库,一个大表拆成多个小表,这样性能瓶颈就降下来了,拆分也是有很多原则和方法的,最常见的就是按业务拆分库,这个粒度通常也是比较大的,就是按一定规则(通常是按业务)将表分类,放入不同的库中:
最大的好处就是分散了存储和访问的压力,拆分后业务清晰,专库专用维护简单,按业务扩展也容易,缺点也有不少,其中有3个重要的要考虑
1、跨库join,这个基本是所有分库分表都会涉及到的,联表查询就要修改成逐个查询了;
2、事务问题,数据库拆分后,分布式事务是最头痛的问题,我见过很多团队因为这个问题难以解决,他们的拆分原则就是把有事务的放到一个库中。
3、维护复杂,成本较高,数据库多了,肯定比但数据库维护复杂,建议数据量上来了,或者随着业务发展再拆库,避免上来就拆,运维复杂。
三、垂直分表
垂直拆分后,解决了业务间瓶颈的问题,但是单库表数据量大带来的瓶颈还没有解决,那单业务或单表遇到数据量大的瓶颈如何解决?这就涉及到拆分表了,一般有两种拆分方式,垂直拆分和水平拆分:
垂直拆分,当一个表特别宽,字段特别多的时候,每次读写全部对磁盘IO较大,性能容易出现瓶颈,可以将表的字段按访问频率分分类,比如我们之前做的业务里面,当事人表有300多个字段,常用字段大约10个,就可以拆分成当事人主表,当事人扩展表。还有类似有很多论坛的设计也是,用户表通常也被拆成两个表,一个user,一个user_ext,90%查询user就够了,这样也大大提高了性能。
拆表原则:
1、长度较短,访问频率较高的属性尽量放在主表
2、字段较长,访问频率较低的属性放在子表
3、经常一起访问的属性,放在一个表里,避免join和跨表查询
4、如果实在属性过多,主表和扩展表都可以有多个,不限于一个
这里提到了跨表查询,当我们可以一个sql查询一个user的全部信息时,我们可以一个sql联表查询,也可以两个简单sql,每个sql查询一个表,在应用中合并数据,我们怎么选?
执行一个sql的方法是将压力扔给了数据库,让数据库进行运算,执行两个简单的sql,数据库压力较小,而且由于查询简单,很多缓存等都可以直接使用,速度快,但应用计算压力大些。如何取舍呢,就要看未来的业务扩展了,如果有拆库拆表的可能,有数据量极大的可能,那尽量执行简单sql,减少数据库压力,因为相比数据库的压力,应用的瓶颈是比较好解决的,而数据库的瓶颈解决会很复杂。
四、水平分表
业务对数据的操作主要集中在某些字段上,比较适合垂直分表,当业务对数据的操作在整个表层面较均匀分布,那就适合水平分表了。相比垂直拆分的复杂度,水平拆分复杂度就上了一个级别,水平拆分是按照某种规则把结构相同的数据划分到不同表或数据库里,这些库表都是完全同构的,比如我们的user表有1亿条数据,并发读写都是问题,经过测算放到64个数据库中比较合适,每个数据库150w条,我们根据一个规则,id取模64进行运算,平均分布到这64个库中,如下图:
这就是一个典型的水平拆分场景,水平拆分后,未来如果数据量变化较大,可以通过动态扩充数据库来支持性能的扩展,看似非常好,但也带来一个非常大的问题,就是应用访问的时候要考虑查询哪个数据库,要做一次运算,这显然给应用带来了极大的麻烦。通常的解决办法就是增加一层数据库中间件(和前面读写分离时提到的中间件一样),主要做SQL路由,优化,数据聚合等工作,如下图:
现在的开源数据库中间件有一些,不过或多或少都有些不足,好一点的执行sql都是并发执行,也就是如图中的3、4步骤并发执行,大大提高sql执行效率,不过这些中间件很多比较复杂,很多公司由于需求简单,也经常自行开发,这些不在此次讨论范畴。
我们继续说水平拆分,拆分依据要选择最适合的,能够与业务能够吻合才是最好的,常见的有根据范围、枚举、时间、取模、哈希、指定等很多方式。水平拆分也有很多原则:
1、拆分要尽可能平均,不均匀会产生访问热点问题,我之前遇到过根据省份划分的,结果有些省数据量极大,就产生了不平均问题,性能瓶颈没解决。
2、尽量减少事务边界,事务边界的意思是指尽量符合业务操作,如果拆分后,每个业务操作都要查询全部的表,大量的跨库join等操作,反而会导致性能下降,没起到拆分的效果。
这两点有时候是冲突的,很多时候我们要取舍,举个例子,最常见的用户、订单两个表,订单数据量大,我们要水平拆分,遵守拆分平均的原则,我们设计成id自增,哈希取模进行平均拆分,分布到1024张表中,如下图:
这时候问题来了,用户大部分的操作是根据用户id查询购买的订单信息,想想你在网上买东西,看的最多的就是“我的订单”吧,所以业务上有大量的sql都是全表扫描,如下图:
第③④步骤要查询全部的表,性能消耗非常严重,如果数据多的时候,第⑤步的时候聚合时间较长,对cpu、内存消耗较大。那有什么好办法吗?根据第三个原则,我们查询较多的情况是根据用户查询订单,那应该按照用户id进行取模分库,而不是根据订单id。但是仔细想想,如果根据用户id进行拆分,可能有些用户买得东西多,有些用户买的少,结果就是订单就不会均匀分布在这些表中,依然没解决问题,那这两个原则,我们如何平衡呢?通常来说我们以数据平均原则为主,优先考虑平均,因为解决查询多,全表扫描等问题比较容易,有很多方案,而解决数据不均匀问题相对困难。
五、异构索引表
以平均原则为主后,如何解决跨表join、全表扫描等的场景呢?比较典型的方案就是异构索引表。就是在按订单id分表存储的时候,再存储一份以用户id为主的表,但只存储到id层面,也就是做到索引。
简单来说,也就是两套水平拆分都有了,想查询哪个就查询哪个,不过这种对磁盘资源消耗较大,所以以订单分区为主,人员分区只存储常用的字段,如id等,查询的时候需要查询两次,如下图(注意图中的顺序,先执行步骤②,根据用户id查询订单id,再执行步骤⑤,根据订单id查询内容):
异构索引表的方式已经能解决90%以上的问题了,如果还不满足,就要考虑其他专门用来查询的方案了,如实时性要求不高可以用Hadoop,实时性要求高可以用内存数据库,HBase等,这些不在咱们讨论范围,不深入讲解。
据了解淘宝目前就是采用异构索引表的方式,不过他们不单单做了索引,而是完全两套表,一套以订单水平拆分,一套以用户水平拆分,这样应用查询起来一个sql即可,非常简单方便,不过代价就是数据同步的要求较高。异构索引表的同步也是一个问题,最好的方式有个数据同步服务,自动根据订单表信息同步索引表。
六、总结
做到数据库的水平拆分,基本上就能够实现数据库性能的线性扩展了,未来数据量再大,通过增加节点就能够达到。总结一下,我们从读写分离、分库、分表讨论到了简单解决分表后的一些典型跨库查询方案,如下图:
其实我们讨论的还是很粗的,只是从大体的架构方案层面进行了讨论,如果真去实现的话有大量的细节需要考虑,不是几篇文章能够说清楚的,这里只是提供了通用思路,让大家对数据库性能设计,分库分表,线性扩展有一个整体了解,具体什么场景适合什么分法,如何权衡利弊,可能就要依赖对业务的积累和长期磨练的经验了。
补充一下,我们如果做设计,不要上来就分库分表,分库分表其实是最复杂的方案,往往是随着数据量的提高而演进过来的,简单说一下遇到性能问题我的大概思路:
1、优先做硬件优化,例如从机械硬盘改成固态硬盘等,根据实际情况判断
2、数据库层面调优操作,例如调整缓存,增加索引,数据库的很多的参数可以调整
3、缓存和其他技术,如redis,mongdb等,减少数据库压力
4、程序与数据库表优化,重构,sql优化等
5、这些都不能优化性能的情况下,单表数据量千万以上,再考虑分库分表吧,也别上来分太多,逐渐扩大
6、一定要根据业务考虑技术,根据场景,大部分的场景不需要太高实时性,不需要那么强的一致性,我们都有很多可优化的地方,还有很多可用的新技术,拆库拆表如果搞不好通常伤敌一千,自损八百。
最后,恭喜你看完了一篇4000多字的文章,希望能引起你思考,祝你有收获。