思考
如何设计或优化千万级别的大表?此外无其他信息,个人觉得这个话题有点范,就只好简单说下该如何做,对于一个存储设计,必须考虑业务特点,收集的信息如下:
1.数据的容量:1-3年内会大概多少条数据,每条数据大概多少字节;
2.数据项:是否有大字段,那些字段的值是否经常被更新;
3.数据查询SQL条件:哪些数据项的列名称经常出现在WHERE、GROUP BY、ORDER BY子句中等;
4.数据更新类SQL条件:有多少列经常出现UPDATE或DELETE 的WHERE子句中;
5.SQL量的统计比,如:SELECT:UPDATE+DELETE:INSERT=多少?
6.预计大表及相关联的SQL,每天总的执行量在何数量级?
7.表中的数据:更新为主的业务 还是 查询为主的业务 ?
8.打算采用什么数据库物理服务器,以及数据库服务器架构?
9.并发如何?
10.存储引擎选择InnoDB还是MyISAM?
大致明白以上10个问题,至于如何设计此类的大表,应该什么都清楚了!
至于优化若是指创建好的表,不能变动表结构的话,那建议InnoDB引擎,多利用点内存,减轻磁盘IO负载,因为IO往往是数据库服务器的瓶颈
另外对优化索引结构去解决性能问题的话,建议优先考虑修改类SQL语句,使他们更快些,不得已只靠索引组织结构的方式,当然此话前提是, 索引已经创建的非常好,若是读为主,可以考虑打开query_cache,以及调整一些参数值:sort_buffer_size,read_buffer_size,read_rnd_buffer_size,join_buffer_size
以及调整一些参数值:sort_buffer_size,read_buffer_size,read_rnd_buffer_size,join_buffer_size
案例一
我现在的公司有三张表,是5亿的数据,每天张表每天的增量是100w
每张表大概在10个columns左右
下面是我做的测试和对比
1.首先看engine,在大数据量情况下,在没有做分区的情况下
mysiam比innodb在只读的情况下,效率要高13%左右
2.在做了partition之后,你可以去读一下mysql的官方文档,其实对于partition,专门是对myisam做的优化,对于innodb,所有的数据是存在ibdata里面的,所以即使你可以看到schema变了,其实没有本质的变化
在分区出于同一个physical disk下面的情况下,提升大概只有1%
在分区在不同的physical disk下,我分到了三个不同的disks下,提升大概在3%,其实所谓的吞吐量,由很多因素决定的,比如你的explain parition时候可以看到,record在那一个分区,如果每个分区都有,其实本质上没有解决读的问题,这样只会提升写的效率。
另外一个问题在于,分区,你怎么分,如果一张表,有三个column都是经常被用于做查询条件的,其实是一件很悲惨的事情,因为你没有办法对所有的sql做针对性的分区,如果你只是如mysql官方文档上说的,只对时间做一个分区,而且你也只用时间查询的话,恭喜你
3.表主要用来读还是写,其实这个问题是不充分的,应该这样问,你在写入的时候,同时并发的查询多么?我的问题还比较简单,因为mongodb的shredding支持不能,在crush之后,还是回到mysql,所以在通常情况下,9am-9pm,写入的情况很多,这个时候我会做一个view,view是基于最近被插入或者经常被查询的,通过做view来分离读取,就是说写是在table上的,读在进行逻辑判断前是在view上操作的
4.做一些archive table,比如先对这些大表做很多已有的统计分析,然后通过已有的分析+增量来解决
5.如果你用mysiam,还有一个问题你要注意,如果你的.configure的时候,加了一个max index length参数的时候,当你的record数大于制定长度的时候,这个index会被disable
案例二
任何偏离业务场景的优化都是耍流氓,如果是订单表,主要通过订单id来查询订单信息,则可以对这样的表 进行垂直分库,每个库表容量500万条,按订单号维度 给拆分到多个库,而在查询的时候,使用订单号查询,通过某个业务规则,直接定位到要查询的目标库。或者通过用户ID 、日期维度 进行分库,但是千万要注意,查询时携带 分库的条件。 如果是CRM系统 ,不直接使用订单号直接查询,而是一个范围查询,返回一个列表集合,而你还继续执着于分库分表就能解决你的性能问题,这样你要对各个库的查询结果集进行union,数据库的性能非但不能提高反而会适得其反!
解决方案
- 首先,任何优化,都需要你了解你的业务,了解你的数据。
- QPS要到多少?- 带宽及存储够的情况下,单机几千QPS妥妥的。
- 读写比例如何?- 读多写少和写多读少,优化方法是有很大差别的。设置于只读场景,果断压缩。
- 数据是否快速增长?- 基本就是QPS的要求。
- 数据及服务的SLA要到多少?- 数据需不需要强一致?HA做到什么程度?
- 诸如此类。
不同的场景有不同的侧重,解决方案是不同的。而对于一些典型的场景可能会有成熟的解决方案。
题主已注明“千万级”,因此以下假设题主为最常见的场景:大量数据,QPS要求高,读多写少,数据快速增长,SLA要求高。
- 其次,说优化的方法。
主要从三个维度说:Why, How, When。
0. sql vs nosql
有些跑题,但也是很重要的一方面。
Why:nosql天生分布,而且大多针对某种类型的数据、某种使用场景做过优化。
比如大批量的监控数据,用mysql存费时费力,可以选择mongo,甚至时间序列数据库,存取会有量级提升。
How:找对应解决方案。
When:有足够诱惑 - 针对使用场景,有成熟解决方案,效率获得大量提升。
1. 优化shema、sql语句+索引
Why:再好的MySQL架构也扛不住一个频繁的垃圾查询。不合理的schema设计也会导致数据存取慢。索引的作用不必多说,但如innodb下,错的索引带来的可能不只是查询变慢而已。
How:设计阶段就需要预计QPS及数据规模,参考业务场景对数据的要求,合理设计表结构(参考mysql在线DDL问题),甚至违反设计范式做到适当冗余。生产环境分析慢日志,优化语句。索引的设计需要知道索引是怎么用的,比如innodb的加锁机制。
When:这个不仅仅是第一个要考虑的,而应该是需要持续去优化的。特别是要参考业务。但实际环境中如果是这个的问题,那一般比较幸运了,因为一般已经优化过很多了。实际中遇到的一般是更深的问题。
2. 缓存
缓存没有那么简单。
缓存对于应用不是完全透明的,除非你用Django这种成熟框架,而且缓存粒度很大,但实际。。。像python,最少也得加几个装饰器。
如何保证缓存里面的数据是始终正确的?写数据前失效缓存还是写数据后?
缓存挂了或者过冷,流量压到后端mysql了怎么办?
缓存也不是万能的。写多读少,命中率会很低。
How:memcache用做缓存,redis用于需要持久化的场景。(redis能不能完全取代memcache?呵呵。。)
还可以使用mysql自带的query cache,对应用基本完全透明。但会受限于本机。而且只缓存查询结果,mc和redis可以缓存一些加工后的数据。
而且数据量大、QPS大的情况下,也需要考虑分片及HA的问题。如果有一个数据过热,把一个节点压垮了怎么办?
When:基本上大多数读多写少的场景都能用,写多的情况下可能需要考虑考虑。
3. 复制及读写分离(做主从复制或主主复制,读写分离,可以在应用层做,效率高,也可以用三方工具,第三方工具推荐360的atlas,其它的要么效率不高,要么没人维护)
Why:这个其实是大多数场景下都必须的。因为复制可以实现备份、高可用、负载均衡。就算嫌麻烦不做负载均衡,那备份下总是要的吧?既然已经备份了,何不加个LVS+HAProxy做下HA?顺便稍微修改下应用,读写分离也就成了。
How:节点少的情况下,主备。前面加Keepalived+HAProxy等组件,失效自动切换。读写分离可能需要修改下应用。
节点多的情况下,一是考虑多级备份,减轻主的压力。其次可以引入第三方组件,接管主节点的备份工作。
主主不是很推荐。一是需要考虑数据冲突的情况,比如错开id,同时操作数据后冲突解决。其次如果强一致会导致延迟增加,如果有节点挂了,需要等到超时才返回。
When:主备几乎大多数场景。甚至不论数据大小。高可用对应用透明,为啥不用?主主麻烦,建议先用切分。
4. 切分
包括垂直切分和水平切分,实现方式上又包括分库、分表。
虽然有些难度,但还是推荐常用的。
Why:垂直切分保证业务的独立性,防止不同业务争抢资源,毕竟业务是有优先级的。
水平切分主要用于突破单机瓶颈。除了主主外,只有切分能真正做到将负载分配下去。
切分后也可对不同片数据进行不同优化。如按时间切分,超过一定时间数据不允许修改,就可以引入压缩了,数据传输及读取减少很多。
How:根据业务垂直切分。业务内部分库、分表。一般都需要修改应用。除分表外,其余实现不是很复杂。有第三方组件可用,但通用高效又灵活的方式,还是自己写client。
When:垂直切分一般都要做,只不过业务粒度大小而已。
分库有是经常用的,就算当前压力小,也尽量分出几个逻辑库出来。等规模上去了,很方便就迁移扩展。
水平拆分有一定难度,但如果将来一定会到这个规模,又可能用到,建议越早做越好。因为对应用的改动较大,而且迁移成本高。
综上,数据库设计要面向现代化,面向世界,面向未来。。。
在一般运维的角度来看,我们什么情况下需要考虑分库分表?
首先说明,这里所说的分库分表是指把数据库数据的物理拆分到多个实例或者多台机器上去,而不是类似分区表的原地切分。
原则零:能不分就不分。
是的,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)整个表热点,数据访问和更新频繁,经常有锁等待,你又没有能力去修改源码,降低锁的粒度,那么只会把其中的数据物理拆开,用空间换时间,变相降低访问压力。
原则二:表设计不合理,需要对某些字段垂直拆分
这里举一个例子,如果你有一个用户表,在最初设计的时候可能是这样:
table:users
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 或者开发人员水平很差,动不动给你出一些幺蛾子,直接把数据库搞挂。这个时候,火车票业务的人员虽然技术很优秀,工作也很努力,照样被老板打屁股。解决的办法很简单:惹不起,躲得起。
20条规则摘要如下:
规则1:一般情况可以选择MyISAM存储引擎,如果需要事务支持必须使用InnoDB存储引擎。
规则2:命名规则。
规则3:数据库字段类型定义
- 经常需要计算和排序等消耗CPU的字段,应该尽量选择更为迅速的字段,如用TIMESTAMP(4个字节,最小值1970-01-01 00:00:00)代替Datetime(8个字节,最小值1001-01-01 00:00:00),通过整型替代浮点型和字符型
- 变长字段使用varchar,不要使用char
- 对于二进制多媒体数据,流水队列数据(如日志),超大文本数据不要放在数据库字段中
规则4:业务逻辑执行过程必须读到的表中必须要有初始的值。避免业务读出为负或无穷大的值导致程序失败
规则5:并不需要一定遵守范式理论,适度的冗余,让Query尽量减少Join
规则6:访问频率较低的大字段拆分出数据表。有些大字段占用空间多,访问频率较其他字段明显要少很多,这种情况进行拆分,频繁的查询中就不需要读取大字段,造成IO资源的浪费。
规则7: 水平分表,这个我还是建议 三思,搞不好非但不能提升性能反而多了很多的join和磁盘IO,开发起来也麻烦,有很多的业务就是要求一次查询大部分的字段 看你业务场景了。大表可以考虑水平拆分。大表影响查询效率,根据业务特性有很多拆分方式,像根据时间递增的数据,可以根据时间来分。以id划分的数据,可根据id%数据库个数的方式来拆分。
规则8:业务需要的相关索引是根据实际的设计所构造sql语句的where条件来确定的,业务不需要的不要建索引,不允许在联合索引(或主键)中存在多于的字段。特别是该字段根本不会在条件语句中出现。
规则9:唯一确定一条记录的一个字段或多个字段要建立主键或者唯一索引,不能唯一确定一条记录,为了提高查询效率建普通索引。
规则10:业务使用的表,有些记录数很少,甚至只有一条记录,为了约束的需要,也要建立索引或者设置主键。
规则11:对于取值不能重复,经常作为查询条件的字段,应该建唯一索引(主键默认唯一索引),并且将查询条件中该字段的条件置于第一个位置。没有必要再建立与该字段有关的联合索引。
规则12:对于经常查询的字段,其值不唯一,也应该考虑建立普通索引,查询语句中该字段条件置于第一个位置,对联合索引处理的方法同样。
规则13:业务通过不唯一索引访问数据时,需要考虑通过该索引值返回的记录稠密度,原则上可能的稠密度最大不能高于0.2,如果稠密度太大,则不合适建立索引了。
规则14:需要联合索引(或联合主键)的数据库要注意索引的顺序。SQL语句中的匹配条件也要跟索引的顺序保持一致。
注意:索引的顺势不正确也可能导致严重的后果。
规则15:表中的多个字段查询作为查询条件,不含有其他索引,并且字段联合值不重复,可以在这多个字段上建唯一的联合索引,假设索引字段为 (a1,a2,...an),则查询条件(a1 op val1,a2 op val2,...am op valm)m<=n,可以用到索引,查询条件中字段的位置与索引中的字段位置是一致的。
规则16:联合索引的建立原则(以下均假设在数据库表的字段a,b,c上建立联合索引(a,b,c))。
规则17:重要业务访问数据表时。但不能通过索引访问数据时,应该确保顺序访问的记录数目是有限的,原则上不得多于10。
规则18:合理构造Query语句,慢SQL监控,检查是否有大量的的子查询和关联查询 嵌套查询等,尽量避免使用这些查询,使用连接(JOIN)来代替子查询(Sub-Queries),使用联合(UNION)来代替手动创建的临时表。
规则19:应用系统的优化。
规则20:可以结合redis,memcache等缓存服务,把这些复杂的sql进行拆分,充分利用二级缓存,减少数据库IO操作。对数据库连接池,mybatis,hiberante二级缓存充分利用上。尽量使用顺序IO代替随机IO。合理使用索引,尽量避免全表扫描。