大型分布式业务平台数据库优化方法(上)
Posted 苏研大云人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大型分布式业务平台数据库优化方法(上)相关的知识,希望对你有一定的参考价值。
点击关注哦
一个小小的mysql数据库B-Tree索引可能会带来意想不到的性能优化提升……
第
1
则
数据库优化的必要性
目前在云计算、大数据、电商、金融支付等应用领域,几乎所有的业务平台都免不了与数据库打交道。由于服务本身是无状态的,一个涉及全链路的业务操作往往需要访问多个数据库实例来完成。在SOA与微服务架构设计大行其道的今天,虽然解决了业务平台的组件化和服务化的问题,然而业务平台上线运行后积累的业务数据呈指数型增长,将成为系统性能的瓶颈。因此,有必要考虑业务平台数据库的性能优化问题。对于热点数据表往往多添加一个B-Tree/B+Tree索引后对平台吞吐量、平均响应时间的性能提升,往往比通过加机器、提高网络带宽能力等硬件资源水平扩展方式更为有效和节约建设成本。
由于业务快速迭代或发布上线的压力,一般在建设业务平台初期,对后端服务的数据存储设计往往会采用“单库单表”模式。大家会觉得对于业务上线初期,“单库单表”模式往往足够,更多的精力可能会放在业务流程的设计和业务代码编程上。
不知道哪天,当运维同事收到监控告警,显示数据库中存量业务数据在慢慢增多,同时在进行巡检时发现业务平台对业务访问的平均响应时间明显超过往常。研发同事通过系统日志分析发现,系统中执行每一条sql语句都耗时比较长,上线之初空空如也的数据表中不知不觉已经接近500W的数据。研发同事遇到的问题也就是比较常见的“数据库存储优化”问题,当单表数据过大时候将对业务平台的增删改查操作造成巨大的影响。下面我们将通过以下几点从原理出发一步步分析如何进行数据库优化。
第
2
则
DB单表优化方法
目前在云计算、大数据、电商、金融支付等应用领域,几乎所有的业务平台都免不了与数据库打交道。由于服务本身是无状态的,一个涉及全链路的业务操作往往需要访问多个数据库实例来完成。在SOA与微服务架构设计大行其道的今天,虽然解决了业务平台的组件化和服务化的问题,然而业务平台上线运行后积累的业务数据呈指数型增长,将成为系统性能的瓶颈。因此,有必要考虑业务平台数据库的性能优化问题。对于热点数据表往往多添加一个B-Tree/B+Tree索引后对平台吞吐量、平均响应时间的性能提升,往往比通过加机器、提高网络带宽能力等硬件资源水平扩展方式更为有效和节约建设成本。
由于业务快速迭代或发布上线的压力,一般在建设业务平台初期,对后端服务的数据存储设计往往会采用“单库单表”模式。大家会觉得对于业务上线初期,“单库单表”模式往往足够,更多的精力可能会放在业务流程的设计和业务代码编程上。
不知道哪天,当运维同事收到监控告警,显示数据库中存量业务数据在慢慢增多,同时在进行巡检时发现业务平台对业务访问的平均响应时间明显超过往常。研发同事通过系统日志分析发现,系统中执行每一条sql语句都耗时比较长,上线之初空空如也的数据表中不知不觉已经接近500W的数据。研发同事遇到的问题也就是比较常见的“数据库存储优化”问题,当单表数据过大时候将对业务平台的增删改查操作造成巨大的影响。下面我们将通过以下几点从原理出发一步步分析如何进行数据库优化。
对于业务平台来说,除非单表存储的数据会持续增长,否则一般不需要考虑拆分库表。因此,拆分库和表虽然可以提高单库和单表的容量以及系统的吞吐量,但是会对系统的逻辑、部署、运维等带来不小的影响并提高复杂度。一般,MySQL数据库的单表通过主键、索引以及分区表等方案可以存储千万级别以下的数据。如果加上本地缓存或者redis这样子的分布式缓存做缓冲,还可以进一步将表的存储容量控制在千万级别以上。下面以MySQL数据库为例,主要介绍数据库单表的优化方法。
1.数据字段类型
一般在创建库和数据表时,就应该考虑每张表的数据类型大小。为每个表的数据字段选择合适的类型会减少数据表每一行的存储大小。设想下,每行节约十几个字节,那么100W行的数据量节约的存储量是相当可观的,且存储量越小的表执行查询的速度也就越快。对于数据表的字段类型常用设计原则如下:
a、尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED,VARCHAR的长度只分配真正需要的空间;
b、尽量少使用DOUBLE和DECIMAL类型;
c、时间类型上,尽量使用TIMESTAMP而非DATETIME,其存储空间只需要 DATETIME 类型的一半;
d、单表不要有太多字段,建议在20以内;
e、尽量设置NOT NULL,避免使用NULL字段,NULL字段很难查询优化且占用额外索引空间;
f、使用枚举或整数代替字符串类型;
2.添加索引以及索引原理
可能读到这个标题的时候,很多童鞋可能会觉得,添加索引嘛,这个很简单,一个MySQL语句或者客户端操作下,应该是So Easy的事儿。其实不然,数据库的索引作用和如何添加索引相信对于大家来说应该是比较熟悉的。下文将主要从MySQL的两种主要数据库引擎(MyISAM和InnoDB)以及索引的数据结构B-Tree(B+Tree)出发,介绍如何添加正确的索引来提高业务平台的增删改查操作的性能效率。
(1)索引的本质
这里借用下MySQL官方对索引的定义:“索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。”
大家都知道数据库查询是DB的最基本的功能之一。任何人都希望查询数据能够尽可能地快,因此数据库的设计者也从查询性能的角度尽可能地去优化查询算法和选择合适的数据结构。在MySQL中,主要采用B-/B+Tree作为索引的结构(至于为什么选择这两种数据结构,限制篇幅就不展开叙述了)。
(2)MySQL两种存储引擎的索引实现
目前,大家用MySQL数据库建数据表时主要用MyISAM和InnoDB两种主流的存储引擎。对于这两种数据库存储引擎索引结构的差异可能不太会去深究,下面先介绍下这两种数据库存储引擎的差异,其中也分析了索引结构的差异。
MyISAM |
InnoDB |
|
物理文件构成区别 |
MyISAM表是独立于操作系统的,每当我们建立一个MyISAM引擎的表时,就会在本地磁盘上建立三个文件,分别是 “.frm”表元数据定义 “.MYD”数据存储 “.MYI”存储索引 |
Innodb的所有数据保存在一个单独的表空间里面,而这个表空间可以由很多个文件组成,一个表可以跨多个文件存在。基于磁盘的资源是InnoDB表空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小,一般为 2GB |
事务处理 |
MyISAM并不支持事务这样的高级数据库特性,但MyISAM类型的表强调的是执行性能。 |
InnoDB提供对事务的支持、外键约束。 |
锁 |
MyISAM存储引擎只支持表锁,锁的粒度较粗。 |
提供行锁,不过需要注意的是,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%” |
索引的结构 |
InnoDB引擎用的也是B+Tree作为索引结构。在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。 |
|
DB的CRUD操作 |
MyISAM比较适合执行大量查询的操作,在筛选大量数据时候非常迅速是其的特点。 |
执行大量的insert或update,出于性能方面的考虑,应该使用InnoDB表。 |
虽然InnoDB也是使用B+Tree作为索引结构,但是具体实现方式与MyISAM截然不同。InnoDB的索引结构如下图所示:
了解该两种MySQL主流存储引擎的索引实现方式对正确使用和索引优化还是非常有帮助的。比如,了解InnoDB的索引实现后,就容易明白为什么不应该使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再比如,用非单调的字段作为主键在InnoDB存储引擎中并不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
(3)通过索引优化sql性能方法
a、应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致数据库存储引擎放弃使用索引而进行全表扫描;
b、索引不是越多越好,根据业务代码的查询有针对性的创建,考虑在WHERE、ORDER/GROUP BY命令上涉及的列建立索引,同时通过MySQL的执行计划命令EXPLAIN(关于执行计划的EXPLAIN下文会讲到)来查看自己的SQL语句是否落到索引上还是执行全表扫描;字段值分布很稀少的字段不适合建索引,例如"性别"这种只有两个值的枚举字段;
c、字符串值的字段只需建前缀索引(最左前缀匹配);同时该类字段最好不要用做主键;
d、尽量不用外键,由业务代码约束保证;
e、使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引;
f、合理地使用覆盖索引,避免不必要地回表操作;
3.EXPLAIN的具体用法
上一节在讲通过添加索引优化sql语句时候已经带到过数据库EXPLAIN执行计划,下面将主要叙述下这个EXPLAIN关键词的用户,并通过一个现实开发中遇到的问题示例进行讲解。MySQL的EXPLAIN关键字用于SQL语句的查询执行计划(QEP)。这条命令的输出结果能够让我们了解MySQL 优化器是如何执行SQL 语句的。然而,这条命令的输出结果并没有提供任何SQL优化的建议,但它提供的重要信息可以帮助我们做出SQL调优决策。
(1)EXPLAIN的基本用法
MySQL 的EXPLAIN 语法可以运行在SELECT 语句或者特定表上。如果作用在表上,那么此命令等同于DESC 表命令。UPDATE和DELETE 的语句需要进行性能改进时,当这些SQL语句不是直接在表的主键或者索引上执行时,为了确保最优化的索引使用率,需要把它们改写成SELECT 语句(以便对它们执行EXPLAIN 命令)。
一般我们用EXPLAIN关键字查看SELECT查询SQL语句的QEP时,每个执行的QEP中均会出现如下几个主要结果字段。
a、key: 指出优化器选择使用的索引。一般来说SQL 查询中的每个表都仅使用一个索引。也存在索引合并的少数例外情况,如给定表上用到了两个或者更多索引;
b、rows:提供了QEP试图分析所有存在于累计结果集中的行数目的MySQL 优化器估计值。QEP 很容易描述这个很困难的统计量;
c、possible_keys:指出SQL优化器为查询选定的索引;
d、key_len:定义用于SQL 语句的连接条件的键的长度;
e、table: EXPLAIN 命令输出结果中的一个单独行的唯一标识符。这个值可能是表名、表的别名或者一个为查询产生临时表的标识符;
f、select_type:提供了各种表示table 列引用的使用方式的类型。最常见的值包括SIMPLE(不包含子查询和其他复杂语法的简单查询)、PRIMARY(为复杂查询创建的首要表)、DERIVED(当查询的表不是物理表时,那么就用该关键字标识) 、UNION(执行union的sql语句的SQL元素)、DEPENDENT SUBQUERY(为使用子查询而定义的);
g、Extra:提供了有关不同种类的MySQL 优化器路径的一系列额外信息;
h、type:代表QEP 中指定的表使用的连接方式,const(当前表只有一行匹配时出现该关键字)、eq_ref(表示有一行是为每个之前确定的表而读取的)、ref(表示所有具有匹配的索引值的行都被用到)、range(所有符合一个给定范围值的索引行都被用到)、ALL(表示需要一次全表扫描其他类型的值);
i、ref:可以被用来标识那些用来进行索引比较的列或者常量;
(2)EXPLAIN执行计划性能调优的示例
以某业务平台的日志表历史记录查询的SQL语句为例,在没有对数据表添加索引的情况下,对业务逻辑代码中通过MyBatis—ORM框架执行的SQL语句进行EXPLAIN的QEP查询。具体的语句如下:
EXPLAIN SELECT * FROM `os_biz_operator_log` WHERE user_id = 'CIDC-U-a5b982db32364abbb6ff28c893d19255' AND operator_time BETWEEN "2017-01-01" AND "2017-02-28"
可以得到结果如下:
由于执行计划的结果TYPE为ALL,存在全表扫描问题,同时可以看出该条SQL语句的每次执行查询后返回行数为256656。通过查看日志发现,每次平均执行的时间为258毫秒。
因此,可以通过对USER_ID和OPERATOR_TIME两个字段为添加索引:
ALTER TABLE `os_biz_operator_log` ADD INDEX user_id_time_index(USER_ID,OPERATOR_TIME)
添加索引后通过查看执行日志,可以看出执行时间为50毫秒,速率提升超过6倍。
4.分区表解决单表数据过大
当数据库单表存的数据量过大时候,可以考虑采用分区表的方案来解决。MySQL的分区表是由多个相关的物理子表组成,这些表也是可以由句柄对象表示,所以对于用户也可以直接访问各个分区,存储引擎管理分区的各个物理表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引。
MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询时,优化器会根据分区定义过滤那些没有需要数据的分区,这样查询就无须扫描所有分区—只需查找包含数据的分区即可。
在分区表上操作按照下面的操作逻辑进行:
a、select查询:当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据;
b、insert操作:当写入一条记录时,分区层打开并锁住所有的底层表,然后确定哪个分区接受这条记录,再将记录写入对应的底层表;
c、delete操作:当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作;
d、update操作:当更新一条数据时,分区层先打开并锁住所有的底层表,mysql先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,然后对底层表进行写入操作,并对原数据所在的底层表进行删除操作;
虽然每个SQL操作都会打开并锁住所有的底层表,但这并不是说分区表在处理过程中是锁住全表的,如果存储引擎能够自己实现行级锁,如:innodb,则会在分区层释放对应的表锁,这个加锁和解锁过程与普通innodb上的查询类似。
(1)MySQL分区表的优点:
a、单表可以存储更多的数据;
b、分区表的数据更容易维护,可以通过清除整块分区以批量删除大量数据,也可以增加新的分区来支持新插入的数据;
c、部分查询能够从查询条件确定只落在少数分区上,查询执行速度比较快;
d、分区表的数据还可以分布在不同的物理设备上,从而高效地利用多个硬件设备;
e、可以使用分区表来避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争;
f、可以备份和恢复单个分区;
(2)分区表的限制和缺点
a、一个表最多只能分1024个区;
b、如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引都必须包含;
c、分区表中无法使用外键约束;
下面是根据时间字段来建立分区表的一个示例:
CREATE TABLE IF NOT EXISTS `cloudmaster`.`session_history_month` (
`ID` INT NOT NULL AUTO_INCREMENT COMMENT '主键id自增',
`USER_NAME` VARCHAR(50) NOT NULL COMMENT '用户登录名,这里是用户名唯一',
`LOGIN_DATE` DATETIME NULL COMMENT '登录时间',
`LOGOUT_DATE` DATETIME NULL COMMENT '登出时间',
`EXPIRE_DATE` DATETIME NULL COMMENT '登录过期时间',
`CREATE_DATE` DATETIME NULL COMMENT '创建时间',
`MODIFY_DATE` DATETIME NULL COMMENT '修改时间',
`VERSION` INT NULL DEFAULT 1 COMMENT '版本号',
PRIMARY KEY (`ID`,`CREATE_DATE`))
ENGINE = INNODB
PARTITION BY RANGE (TO_DAYS(CREATE_DATE)) (
PARTITION y2016_m12 VALUES LESS THAN (TO_DAYS('2017-10-01')),
PARTITION y2017_m01 VALUES LESS THAN (TO_DAYS('2017-11-01')),
PARTITION y2017_m02 VALUES LESS THAN (TO_DAYS('2017-12-01')),
PARTITION y2017_m03 VALUES LESS THAN (TO_DAYS('2018-01-01')),
PARTITION m_d VALUES LESS THAN MAXVALUE);
(3)分区表适合的场景
a、最适合的场景数据的时间序列性比较强,则可以按时间来分区,查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容易批量删除;
b、如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存;
对于数据库的单表在千万级以内的数据量,通过以上的DB单表优化方法一般都可以应应对,但是当数据量超过千万级别时,就需要通过分库分表、读写分离数据和缓存并用等方案进行技术架构优化改造来解决。限于篇幅原因,将在下篇文章继续为大家进行阐述。
END
以上是关于大型分布式业务平台数据库优化方法(上)的主要内容,如果未能解决你的问题,请参考以下文章
史上最强大型分布式架构详解:高并发+数据库+缓存+分布式+微服务+秒杀