网易MySQL中间件的负载均衡策略及性能优化

团队介绍
网易乐得DBA组,负责网易乐得电商、网易邮箱、网易技术部数据库日常运维,负责数据库私有云平台的开发和维护,负责数据库及数据库中间件Cetus的开发和测试等等。

一、背景

随着业务的爆发式增长,电商系统中的读写压力越来越高,单节点MySQL实例压力越来越大,单纯升级服务器硬件已经无法满足生产环境的需要。解决读请求压力,需要支持从库扩展;解决写请求压力,对数据分片增加多个节点,降低单节点MySQL实例的压力成了更优的选择。

传统的分片是通过DAO层进行的,但是DAO层对数据分片存在诸多问题。从业务角度看,配置修改需要重启服务,代价巨大;需要对分片结果集进行处理,业务逻辑愈加复杂;功能相对简单。从数据库运维角度看,配置管理的统一化难度较大;DB的升级、迁移等操作复杂。

网易电商同样面临着这些问题,为了彻底解决数据库瓶颈,网易乐得团队在实际生产中研发了自己的中间件Cetus。其具有正统基因,基于官方MySQL-Proxy的版本进行全面修复和再创新,已于不久前开源,在各个产品线上得到广泛应用,性能和稳定性均表现良好。

Cetus兼容MySQL协议,前端应用不用修改即可通过Cetus访问数据库,方便DBA运维同学和开发同学使用,实现了数据库层面的横向扩展。

目前Cetus有读写分离和Sharding两个版本,可通过编译参数选择适合的版本。它支持对用户透明的多项功能,例如分布式事务、连接池、结果集压缩、安全管理、状态监控、Tcp Stream传输等等。

二、负载均衡策略及性能优化

本文所讨论的负载均衡,指的是读流量的负载均衡,即读流量如何分配到后端同一MySQL集群内的各个DB。

Cetus的负载均衡策略,主要分为两部分:

  • 主从库之间读流量的负载策略;
  • 从库之间读流量的负载策略。

具体实现时候,流量的分配单位与Atlas等中间件也略有不同,进行了性能优化。下面章节将依次详细介绍。

1、主从库之间读流量的负载策略

默认情况下,非事务中、未通过注释强制路由主库或未使用锁的读流量会优先路由到从库,各个从库之间负载均衡。只有当从库都不可用时,读流量才会路由到主库。

有些业务场景下,主库可以分担部分读流量,这时就涉及到读流量在主库和从库上配置负载策略了。

Cetus中,可以通过配置参数read-master-percentage来指定默认的读流量路由到主库的百分比,该参数的取值范围是[0, 100]。

该值默认为0,即所有读流量会优先路由从库,所有从库均不可用时,才会路由主库;如果该参数设置为100时,则所有读流量都会路由到主库;如果该值设置为(0, 100)时,则会按照设置的比例进行路由。需要注意的是,该值表示的是主库和所有从库的比例。

2、从库之间的读流量负载策略

路由到从库的流量会在各个从库之间进行负载均衡。目前Cetus各个从库之间的读流量负载策略仅支持轮询(RR)方式。

在流量分配方面,Cetus也进行了优化。一些MySQL数据库中间件(例如Atlas)是基于SQL的维度做负载均衡的,不会考虑SQL是同一个连接还是不同连接发送来的,中间件依次将接收到的SQL按照策略发往后端的数据库。

未分类

在实际使用中发现,长连接的场景下,该策略会造成大量的连接切换,从而导致session级变量的频繁调整,影响SQL执行效率。因此,Cetus对其进行了优化,并非完全按照SQL的维度做负载均衡。

Cetus考虑了同一个连接连续发送SQL请求的情况,不会立即将当前SQL使用完的Cetus与MySQL的连接放回连接池复用,而是持有短暂(256毫秒)时间,以期后续仍有SQL执行,从而避免了session级变量的调整,大大增加了SQL执行的效率。

未分类

长连接场景下,对优化前后的Cetus进行了简单测试。通过测试发现,通过优化后的Cetus针对长连场景下的读流量的吞吐量有了明显提升。下图是在docker环境下的简单测试对比:

未分类

为了防止IO过高,简单改造了sysbench发送的SQL,限制了返回的结果集大小。禁用事务和prepare的情况下,采用100个线程每次测试60s,连续测试5次,结果如下:

未分类

由于本机Docker性能较差,且sysbench模拟测试的语句较为简单,不涉及session变量的切换,因此对比效果不甚明显,本次测试性能仅提升30%左右。长连接业务场景下,性能优化可能会更加明显。

3、读流量的路由策略总结

在存在至少1个可用从库的情况下,影响查询语句的路由策略的因素主要有:

a. 事务中的查询;
b. select…for update 或 select … lock in share mode;
c. Cetus设置参数master-preferred=true所有流量默认全部路由主库;
d. Cetus设置参数read-master-percentage控制主从读流量负载;
e. 使用注释/#mode=READWRITE/或/#mode=READONLY/。

默认情况下,读流量会优先路由到从库,从库之间按照轮询策略在各个从库之间做负载均衡;一旦所有从库均不可用,会路由到主库上。目前Cetus的各个从库暂不支持按照权重做负载。

  • 对于a、b、c点,Cetus会将查询语句直接路由主库;
  • 对于d点,如果设置read-master-percentage=100,所有的查询流量均路由到主库;如果设置read-master-percentage=[0, 100),Cetus会将读流量按照该比例路由到主库和从库(注意,这里的从库指的是全部的从库,即该比例指的是主库和全部从库的比例);
  • 对于e点,如果使用注释/#mode=READWRITE/,读流量会路由到主库;如果使用注释/#mode=READONLY/读流量会路由从库,如果所有从库均不可用时才会路由到主库。

上面的各个因素的优先级,注释的优先级最高,其次是参数master-preferred,最后是参数read-master-percentage。

三、总结

MySQL数据库中间件的主要特性是对客户端发送的SQL进行路由,而其中负载均衡便是路由策略中的重要部分。通过了解Cetus的负载均衡机制,可以在后续维护过程中,更好的对数据库中间件进行调优,更灵活地控制SQL的路由。

Cetus中间件开源地址:https://github.com/Lede-Inc/cetus/blob/master/doc/cetus-quick-try.md

Mysql主从架构-主库宕机如何恢复业务

在我们日常工作场景,首先要做到架构无单点隐患,其次在优化【安全、性能、高可用、高并发等】,Mysql这款关系型数据库稳定、高效,所以使用广泛,如果企业架构是1主多从,那如果Mysql主库宕机,如何解决?

MySQL 主从同步原理图

未分类

一、Mysql主库宕机情况分类:

1)硬件问题,(服务器、ecs、虚拟主机等等)宕机

2)service问题,Mysql宕机,服务异常,端口异常等

二、硬件问题处理思路

硬件问题我们可以查看IDC巡检记录,或通过远程控制卡查看硬件运行状态,根据事实情况就行硬件故障报修进行处理,恢复业务步骤:

1)查看报警信息,确认业务是否收到影响,必要时切从库进行数据交互
2)IDC询问排查
3)确认硬件故障,短时间无法修复开Case处理
4)通知部门领导,处理进度,并实时记录
5)事件处理完成后,拟写故障报告,会议通报。

三、MySQL service问题处理思路

1)首先要做的就是判断是否影响业务,是否需要切库,保证业务运行时首要任务
2)如果此时需要切从库,安装如下步骤进行:

1>   先查看MySQL 从库状态
       show processlistG
##如果看到两个状态,说明此时的从库和主库是同步的  =====[如果不同步,建议考出binlog进行同步]
#state: waiting for master to send event   I/O线程
#state:has read all relay log;waiting for the slave I/O thread to update it    sql线程

2>   登录从库分别查看:【多个从库 哪个替代主库呢??】
cat /data/3306/data/master.info
cat /data/3307/data/master.info
##看哪个从库的哪个master.info哪个更新,就说明哪个从库一致性更高,所以此时就确定最新的库为主库。
选个pos最大的作为主库
或利用半同步的功能,直接选择做实时同步的这个从库。

3> 确保所有relay log全部更新完毕。
stop slave io_thread;show processlist;    [在每个从库上执行]
##直到看到has read all relay log;表示从库更新都执行完毕

4> 登录从库
mysql -uroot -p您的密码 -S /data/3306/mysql.sock  #sock路径根据自己的进行修改
stop slave;
reset master;
quit;

5> 进入数据库数据目录,删除master.info relay-log.info
cd /data/3306/data/master
rm -f master.info relay-log.info
## 检查授权表,类似read-only参数

6>  3306 提升从库为主库
vim /data/3306/my.cnf
开启:
log-bin =/data/3306/mysql-bin
## //如果存在log-slave-updates  read-only等参数一定要注释掉。
/data/3306/mysql  stop
/data/3306/mysql  start

到此,提升主库完毕

四、所有slave指向新的master

7> 如果主库服务器没down,需要去主库拉取bin-log补全提升主库的从库

8> 其它从库操作 【指向新的master】

已检查(同步user rep均存在)
登录从库
stop slave;
change master to master_host='192.168.1.32'; //如果不同步,就指定位置点
start slave;
show slave statusG;

以上就是关于数据库如何快速回复业务的介绍,如果大家有更好的方法可以告诉我,下面是集中数据库高可用方案:

1. MHA 高可用 http://blog.51cto.com/qiuyt/1930629

2. RDS 高可用+自动容灾 https://www.aliyun.com/product/rds/mysql【借用图】

未分类

3. 快照

方法很多,就看您如何选择。

五、另外关于ECS无法使用 MHA 说法问题解释

1、mha必须是0.56版本才支持GTID

2、阿里云ECS不支持浮动IP  阿里云HaVIP禁用
##要注意:阿里云的上ECS服务器跟我们自己的虚拟机不一样,它不支持浮动IP的

3、阿里云服务器上keepalived只能设置单播

4、keepalived配置文件里加脚本判断mysql是否启动需要首尾写好配置文件

阿里云工单反馈截图

未分类

MySQL一般查询日志或者慢查询日志历史数据的清理

general log&slow query log

对于MySQL的一般查询日志和慢查询日志,开启比较简单,其中公用的一个参数是log_output,log_output控制着慢查询和一般查询日志的输出方向
可以是表(mysql.general_log,mysql.slow_log)或者文件(有参数general_log_file和slow_query_log_file配置决定)
或者同时输出到表和文件(想不明白,什么时候需要同时输出到表和文件)。
但是两者受log_output参数影响,输出的目标总是一致的,也就是要么都写入表,或者要么都写入文件,不会一个输出到表,一个输出到文件。

--slow log 相关参数
select * 
from performance_schema.global_variables 
where variable_name in
('slow_query_log','log_output','slow_query_log_file','long_query_time')

--general log 相关参数
select * 
from performance_schema.global_variables 
where variable_name in
('general_log','log_output','general_log_file')

对于上述两种日志,系统默认不会清理,因此在开启了相关日志之后,需要人为清理。

如何清理历史general log&slow query log

1、当输出目标为表的时候

无法直接删除,如果直接删除的话,会出现“ERROR 1556 (HY000): You can’t use locks with log tables.”的错误提示

未分类

以general log为例,需要先关闭general_log,然后重命名general_log这个表,

未分类

在对重命名之后的表执行删除,最后在重命名回来,最后开启general_log(如果有必要的话)

SET GLOBAL general_log = 'OFF';
RENAME TABLE general_log TO general_log_temp;
DELETE FROM general_log_temp WHERE event_time < DATE(NOW());
RENAME TABLE general_log_temp TO general_log;
SET GLOBAL general_log = 'ON';


--slow log 同理
SET GLOBAL slow_query_log = 'OFF';
RENAME TABLE slow_log TO slow_log_temp;
DELETE FROM slow_log_temp WHERE start_time < DATE(NOW());
RENAME TABLE slow_log_temp TO slow_log;
SET GLOBAL slow_query_log = 'ON';

  如果对重命名之后的表(general_log或者是slow_log)没有再次重命名回来,会发生什么?
  参加如下截图,如果没有找到对应的表(general_log或者是slow_log),在输出目标为表的情况下,会提示无法找到对应的表,将无法开启对应的日志

未分类

2、当输出目标为文件的时候

  当输出目标为文件的时候,在linux下,直接使用rm命名删除即可,如果在开启了一般查询日志或者是慢查询日志,删除对应的日志文件,并不影响数据库的正常使用
  网上有说需要停止MySQL服务然后重命名文件然后在创建新的文件啥的,在Linux下并不是必须的,不知道在windows下是什么情况,没兴趣试。
  当然也不是说就建议始终这种暴力的方式清理日志文件,
  在Linux下,删除了默认的日志文件(或者重命名了原日志文件),要想再次生成日志文件
  1,可以使用mysqladmin flush-logs
  2,是SQL命令flush slow logs;flush general logs;
  3,重启MySQL服务
  均可重新生成对应的日志文件。

未分类

注意:当对应的文件是存在的时候,上述命名执行之后是没有影响的(也不会清理对应的日志文件)



以下偏离主题

当输出目标为表的时候的解析

  不管是general_log或者是slow_log,对应的SQL语句都是二进制格式的,需要使用convert(sql_text using UTF8)做一个转换,才变得具有可读性。

未分类

当输出目标为表的时候对性能的影响

  据个人测试,在请求量不大的数据库上,开启general_log或者是slow_log,对性能影响并没有非常明显。
  理论家们一方面强调说MySQL的处理并发上多强悍,一方面又说开启general_log对性能影响很大,会不会自相矛盾呢?
  关于general_log,在zabbix监控下,测试环境TPS不过百的情况下(每秒写入general log不超过100条数据),开启general_log之后并CPU负载几乎没有变化,CPU高点是在做其他压力测试。
  尤其是slow_log这种写入不是太频繁的日志,直接写入到表中,对性能的影响有限,比后面再去花时间解析文件……  
  当然不排除TPS在上千或者上万甚至更高之后,开启general_log会产生较大的影响,当然没事也不会闲的蛋疼去开general_log。

未分类

Mongodb和mysql的区别

1. Mongodb简介及优缺点分析

Mongodb是非关系型数据库(nosql ),属于文档型数据库。文档是mongoDB中数据的基本单元,类似关系数据库的行,多个键值对有序地放置在一起便是文档,语法有点类似javascript面向对象的查询语言,它是一个面向集合的,模式自由的文档型数据库。

存储方式:虚拟内存+持久化。
查询语句:是独特的Mongodb的查询方式。
适合场景:事件的记录,内容管理或者博客平台等等。
架构特点:可以通过副本集,以及分片来实现高可用。
数据处理:数据是存储在硬盘上的,只不过需要经常读取的数据会被加载到内存中,将数据存储在物理内存中,从而达到高速读写。
成熟度与广泛度:新兴数据库,成熟度较低,Nosql数据库中最为接近关系型数据库,比较完善的DB之一,适用人群不断在增长。

优点:
快速!在适量级的内存的Mongodb的性能是非常迅速的,它将热数据存储在物理内存中,使得热数据的读写变得十分快。高扩展性,存储的数据格式是json格式!

未分类

缺点:
① mongodb不支持事务操作。
② mongodb占用空间过大。
③ 开发文档不是很完全,完善。

未分类

2. MySQL优缺点分析

优点:
在不同的引擎上有不同 的存储方式。
查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高。
开源数据库的份额在不断增加,mysql的份额页在持续增长。

缺点:
在海量数据处理的时候效率会显著变慢。

3. Mongodb和MySQL数据库的对比

  • 传统的关系数据库一般由数据库(database)、表(table)、记录(record)三个层次概念组成,MongoDB是由数据库(database)、集合(collection)、文档对象(document)三个层次组成。
  • MongoDB对于关系型数据库里的表,但是集合中没有列、行和关系概念,这体现了模式自由的特点。

未分类

未分类

4. MongoDB常用语句

# 连接Mongo数据库,并设置数据存储地址
mongod.exe --dbpath "d:softwareMongoDBServer3.0data"

#-----------------------#1# 数据库
# 查看所有的数据库
show dbs
# 删除当前使用的数据库
db.dropDatabase()
# 使用这个数据库(只有插入数据后完成创建数据库)
use dbt
# 查看当前使用的数据库
db
db.getName()
# 查看当前数据库状态
db.stats()
# 修复当前数据库
db.repairDatabase()
# 从一个数据库复制到另一个数据库
db.copyDatabase("mydb", "temp", "127.0.0.1");

#-----------------------#2# 集合
# 查看当前数据库下所有的集合
show collections
show tables
# 创建名称为coll集合
db.createCollection('coll')
db.createCollection("coll2", {capped:true, autoIndexId:true, size:6142800, max:10000})      # 可选参数
# 查看当前集合状态
db.coll.stats()
# 删除名称为coll集合
db.coll.drop()


#-----------------------#3# 集合数据
# 插入空数据并且直接创建名称为coll集合
db.coll.insert({})
# 插入一个或多个数据
db.coll.insert({name:'tom', age:22})
db.coll.insert([{name:'adam', age:10},{name:'john', age:23}])
# 添加数据(save方法可以修改相同id的数据)
db.coll.save({name:'allen'})
# 删除一个或所有的数据
db.coll.remove({name:'tom'})
db.coll.remove({})
# 删除符合条件的数据中的第一条
db.coll.remove({name:'tom'}, 1)
# 更改数据
db.coll.update({name:'tom', age:22}, {$set:{name:'tom', age:222}})
# 查看数据
db.coll.find()
# 查看一条数据
db.coll.findOne()
db.coll.find({}, {name:1, '_id':0})     # 1表示显示,0表示不显示(find默认显示_id)
# 格式化显示数据,使数据更加清晰明了
db.coll.find().pretty()
# 使用and,or查看数据
db.coll.find({name:'tom', age:22})      # 等同and使用
db.coll.find({$or:[{name:'tom'}, {age:21}]})        # or使用



# 操作符大于,小于,等于,不等于,大于不等于,小于不等于
db.coll.find({age: {$gt: 22}})      # 大于
db.coll.find({age: {$lt: 22}})      # 大于
db.coll.find({age: 22})      # 等于
db.coll.find({age: {$ne: 22}})      # 不等于
db.coll.find({age: {$gte: 22}})      # 大于等于
db.coll.find({age: {$lte: 22}})      # 小于等于

# 显示从skip之后limit个
db.coll.find().limit(2).skip(1)

#-----------------------# # 用户
# 3.x之后版本添加用户
use admin
db.createUser({user:'nu', pwd:'nu', roles:[{role:'readWrite',db:'admin'}]})
# 用户认证
db.auth("nu", "nu");
# 显示当前所有用户
show users;
db.system.users.find()
3.x版本删除用户
db.removeUser('nu')     # 不推荐使用,已经废弃
db.dropUser("nu");

# 当前db版本
db.version();

# 当前db的链接机器地址和端口
db.getMongo();

# 备份到备份目录
mongodump

# 从备份目录恢复备份语句。
mongorestore

MySQL中Innodb如何计算索引的统计信息?

摘要: MySQL查询优化器的执行计划是根据统计信息中键值的分布选择合适的索引这是基于索引的选择性的。innodb通过抽样的方式来计算统计信息首先随机的读取少量的索引页面然后以此为样本计算索引的统计信息。老的innodb默认样本页面数为8新版本可以通过innodb_stats_transient_sample_pages5.6.3之前是innodb_stats_sample_pages来设置样本页的数量。

MySQL查询优化器的执行计划是根据统计信息中键值的分布选择合适的索引,这是基于索引的选择性的。innodb通过抽样的方式来计算统计信息,首先随机的读取少量的索引页面,然后以此为样本计算索引的统计信息。老的innodb默认样本页面数为8,新版本可以通过innodb_stats_transient_sample_pages(5.6.3之前是innodb_stats_sample_pages)来设置样本页的数量。样本页的数量设置的更大,理论上来说是可以得到更准确的统计信息,特别是对于超大的表。但是具体设置多大合适还是需要根据实际情况

innodb索引的统计信息存储方式有两种,一种是非持久性存储,既存储在内存中,如果服务器重启就会丢失;一种是持久性存储,即存储到磁盘上,可以永久保存。通过参数innodb_stats_persistent来控制。在MySQL5.6.6之后,默认是持久性存储。

两种存储方式:

非持久性存储,通过设置innodb_stats_persistent=OFF或者使用STATS_PERSISTENT=0创建,通过以下操作可以触发计算统计信息:

a) 执行analyze table

b) 在使用show table status、show index等命令的时候,或者在查询系统表INFORMATION_SCHEMA.TABLES 和 INFORMATION_SCHEMA.STATISTICS的时候。需要一个参数控制是否会触发更新统计信息,innodb_stats_on_metadata=on时。

这里需要注意的是,数据库中有大量的表或者索引的时候,会给数据库的IO带来更大的压力;并且如果频繁的更新统计信息,MySQL的执行计划的稳定性也会受到影响。

c) 在启动mysql客户端的时候采用–auto-rehash参数。

d) 一个表首次被打开的时候。

e) 表发生非常大的变化的时候(大小变化超过1/16或者新插入20亿行数据)。

持久性存储,设置innodb_stats_persistent=ON,或者STATS_PERSISTENT=1创建。

持久化的信息存储在MySQL的系统表mysql.innodb_table_stats 和mysql.innodb_index_stats 中。

因为是持久性存储到磁盘上,所以在表一段时间之后或者是进行大的改动的时候需要手动执行analyze table来更新统计信息。

总结:建议设置持久性存储到磁盘上,可以得到更稳定的执行计划,并且在系统重启之后可以更快速的生成统计信息。但是需要周期性的执行analyze table来手动更新统计信息,否则统计信息永远不变。

MySQL InnoDB MVCC实现

数据多版本(MVCC)是MySQL实现高性能的一个主要的一个主要方式,通过对普通的SELECT不加锁,直接利用MVCC读取指版本的值,避免了对数据重复加锁的过程,今天我们就用最简单的方式,来分析下MVCC具体的原理,先解释几个概念:

隐藏列

在分析MVCC原理之前,先看下InnoDB中数据行的结构:

未分类

在InnoDB中,每一行都有2个隐藏列DATA_TRX_ID和DATA_ROLL_PTR(如果没有定义主键,则还有个隐藏主键列):

  • DATA_TRX_ID表示最近修改该行数据的事务ID
  • DATA_ROLL_PTR则表示指向该行回滚段的指针,该行上所有旧的版本,在undo中都通过链表的形式组织,而该值,正式指向undo中该行的历史记录链表

整个MVCC的关键就是通过DATA_TRX_ID和DATA_ROLL_PTR这两个隐藏列来实现的。

事务链表

MySQL中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,这是一个基本的链表结构:

未分类

事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。

ReadView

有了前面隐藏列和事务链表的基础,接下去就可以构造MySQL实现MVCC的关键——ReadView。

ReadView说白了就是一个数据结构,在SQL开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView{low_trx_id, up_trx_id, trx_ids},在并发情况下,一个事务在启动时,trx_sys链表中存在部分还未提交的事务,那么哪些改变对当前事务是可见的,哪些又是不可见的,这个需要通过ReadView来进行判定,首先来看下ReadView中的3个成员各自代表的意思:

  • low_trx_id表示该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号;
  • up_trx_id表示该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务;
  • trx_ids表示所有事务链表中事务的id集合。

上述3个成员组成了ReadView中的主要部分,简单图示如下:

未分类

根据上图所示,所有数据行上DATA_TRX_ID小于up_trx_id的记录,说明修改该行的事务在当前事务开启之前都已经提交完成,所以对当前事务来说,都是可见的。而对于DATA_TRX_ID大于low_trx_id的记录,说明修改该行记录的事务在当前事务之后,所以对于当前事务来说是不可见的。

注意,ReadView是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadView的up_trx_id和low_trx_id也都是不一样的,至于DATA_TRX_ID大于low_trx_id本身出现也只有当多个SQL并发的时候,在一个SQL构造完ReadView之后,另外一个SQL修改了数据后又进行了提交,对于这种情况,数据其实是不可见的。

最后,至于位于(up_trx_id, low_trx_id)中间的事务是否可见,这个需要根据不同的事务隔离级别来确定。对于RC的事务隔离级别来说,对于事务执行过程中,已经提交的事务的数据,对当前事务是可见的,也就是说上述图中,当前事务运行过程中,trx1~4中任意一个事务提交,对当前事务来说都是可见的;而对于RR隔离级别来说,事务启动时,已经开始的事务链表中的事务的所有修改都是不可见的,所以在RR级别下,low_trx_id基本保持与up_trx_id相同的值即可。

最后用一张图来解释MySQL中的MVCC实现:

未分类

CentOS(linux) 下MySQL8.0.11的安装

系统
CentOS 7.4

安装软件
MySQL8.0.11

  • 下载MySQL yum源
wget https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
  • 安装yum源
yum localinstall mysql80-community-release-el7-1.noarch.rpm
  • 更新yum源
yum clean all
yum makecache
  • 创建Mysql账户
groupadd mysql
useradd -g mysql mysql
  • 开始安装MySQL
yum install mysql-community-server
  • 启动MySQL
systemctl start mysqld
  • 查看初始化密码
cat /var/log/mysqld.log | grep password

未分类

  • 登录MySQL
mysql -u root -p
  • 修改初始化密码(密码一定要大小写字母+数字+符号,如:Aa-123456789)
ALTER USER 'root'@'localhost' IDENTIFIED BY 'yourpassword';
  • 远程设置
use mysql;
update user set host='%' where user='root';
  • 允许任何主机访问数据库
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'WITH GRANT OPTION;
FLUSH PRIVILEGES;
  • 允许myuser用户使用mypassword密码从任何主机连接到mysql服务器
GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY 'mypassword' WITH GRANT OPTION;
  • 允许用户myuser从ip为192.168.1.6的主机连接到mysql服务器,并使用mypassword作为密码
GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'192.168.1.3'IDENTIFIED BY 'mypassword' WITH GRANT OPTION;

MySQL状态变量Aborted_connects与Aborted_clients浅析

关于MySQL的状态变量Aborted_clients & Aborted_connects分别代表的意义,以及哪些情况或因素会导致这些状态变量变化呢?下文通过实验测试来验证一下,首先我们来看看状态变量的描述:

Aborted Connect

Aborted Connect表示尝试连接到MySQL服务器失败的次数。这个状态变量可以结合host_cache表和其错误日志一起来分析问题。 引起这个状态变量激增的原因如下:

  1. 客户端没有权限但是尝试访问MySQL数据库。

  2. 客户端输入的密码有误。

  3. A connection packet does not contain the right information.

  4. 超过连接时间限制,主要是这个系统变量connect_timeout控制(mysql默认是10s,基本上,除非网络环境极端不好,一般不会超时。)

官方解释如下:

If a client is unable even to connect, the server increments the Aborted_connects status variable. Unsuccessful connection attempts can occur for the following reasons:

  • A client attempts to access a database but has no privileges for it.

  • A client uses an incorrect password.

  • A connection packet does not contain the right information.

  • It takes more than connect_timeout seconds to obtain a connect packet. See Section 5.1.7, “Server System Variables”.

Aborted Clients

Aborted Clients表示由于客户端没有正确关闭连接而中止的连接数。官方解释如下:

The number of connections that were aborted because the client died without closing the connection properly. See Section B.5.2.10, “Communication Errors and Aborted Connections”

当Aborted Clients增大的时候意味着有客户端成功建立连接,但是由于某些原因断开连接或者被终止了,这种情况一般发生在网络不稳定的环境中。主要的可能性有:

  1. 客户端程序在退出之前未调用mysql_close()正确关闭MySQL连接。

  2. 客户端休眠的时间超过了系统变量wait_timeout和interactive_timeout的值,导致连接被MySQL进程终止

  3. 客户端程序在数据传输过程中突然结束

官方文档B.5.2.10 Communication Errors and Aborted Connections的介绍如下:

If a client successfully connects but later disconnects improperly or is terminated, the server increments the Aborted_clients status variable, and logs an Aborted connection message to the error log. The cause can be any of the following:

  • The client program did not call mysql_close() before exiting.

  • The client had been sleeping more than wait_timeout or interactive_timeout seconds without issuing any requests to the server. See Section 5.1.7, “Server System Variables”.

  • The client program ended abruptly in the middle of a data transfer.

Other reasons for problems with aborted connections or aborted clients:

  • The max_allowed_packet variable value is too small or queries require more memory than you have allocated for mysqld. See Section B.5.2.9, “Packet Too Large”.

Use of Ethernet protocol with Linux, both half and full duplex. Some Linux Ethernet drivers have this bug. You should test for this bug by transferring a huge file using FTP between the client and server machines. If a transfer goes in burst-pause-burst-pause mode, you are experiencing a Linux duplex syndrome. Switch the duplex mode for both your network card and hub/switch to either full duplex or to half duplex and test the results to determine the best setting.

  • A problem with the thread library that causes interrupts on reads.

  • Badly configured TCP/IP.

  • Faulty Ethernets, hubs, switches, cables, and so forth. This can be diagnosed properly only by replacing hardware.

如上介绍所示,有很多因素引起这些状态变量的值变化,那么我们来一个个分析、演示一下吧。首先,我们来测试一下导致Aborted Connect状态变量增加的可能因素

1、 客户端没有权限但是尝试访问MySQL数据库。

其实这里所说的没有权限,个人理解是:客户端使用没有授权的账号访问数据库 。打个比方,你尝试用账号kkk访问MySQL数据库,其实你也不知道数据库是否存在这个用户,实际上不存在这个用户。

实验对比测试前,先将状态变量清零。

mysql> flush status;
Query OK, 0 rows affected (0.01 sec)
mysql> show status like 'Abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 0     |
+------------------+-------+
2 rows in set (0.01 sec)

mysql> 
mysql> select host,user from mysql.user;
+-------------------------------+-----------+
| host                          | user      |
+-------------------------------+-----------+
| %                             | mydba     |
| %                             | root      |
| %                             | test      |
| 127.0.0.1                     | root      |
| 192.168.%                     | mydbadmin |
| 192.168.103.18,192.168.103,22 | LimitIP   |
| ::1                           | root      |
| db-server.localdomain         | root      |
| localhost                     | backuser  |
| localhost                     | root      |
+-------------------------------+-----------+

在本机的SecureCRT的另外一个窗口,使用不存在的账号kkk访问MySQL后,你会发现状态变量Aborted_connects变为1了。

[root@DB-Server ~]# mysql -u kkk -p
Enter password:
ERROR 1045 (28000): Access denied for user 'kkk'@'localhost' (using password: YES)

未分类

也有可能,这个账号本身存在,但是只允许特定IP地址才能访问,实际环境中,可能是有人在进行尝试暴力破解。可能性非常多。我们来测试一下限制IP访问的情况

mysql> grant all on MyDB.* to mydbadmin@'10.20.%' identified by '123456';
Query OK, 0 rows affected (0.01 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql>  show status like 'Abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 0     |
+------------------+-------+
2 rows in set (0.00 sec)

如上所示,创建一个mydbadmin的行号,只允许10.20段的IP访问,然后我们从192.168段的IP访问MySQL数据库

# mysql -h 10.20.57.24 -u mydbadmin -p
Enter password:
ERROR 1045 (28000): Access denied for user 'mydbadmin'@'192.168.7.208' (using password: YES)

此时,状态变量Aborted_connects就变为1了。

未分类

2、 客户端输入的密码有误或者根本就是尝试各个密码。(A client uses an incorrect password)

如下所示,使用test账号访问MySQL数据,但是输入了一个错误密码

[root@DB-Server ~]# mysql -u test -p
Enter password:
ERROR 1045 (28000): Access denied for user 'test'@'localhost' (using password: YES)
[root@DB-Server ~]#

你检查状态变量Aborted_connects就会发现状态变量Aborted_connects变为2了。

mysql>  show status like 'Abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 2     |
+------------------+-------+
2 rows in set (0.00 sec)

3: A connection packet does not contain the right information.

这个比较容易构造,可以对MySQL的端口进行端口测试(ping 端口),因为psping的包不包含正确的信息(right information),测试之前,先将状态变量清空。

mysql> flush status;
 Query OK, 0 rows affected (0.00 sec)
mysql> show status like 'abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 0     |
+------------------+-------+
2 rows in set (0.00 sec)

在客户端对MySQL服务所在的主机进行端口连通性验证(psping)

未分类

如上所示,psping测试后,Aborted_connects变成了5,如果继续进行psping测试,那么这个状态变量就会继续增长。

未分类

另外,如果超过max_connect_error的限制后,某一个客户端持续访问MySQL,这个是否会引起状态变量Aborted_connects变化呢,实验测试的答案是不会。有兴趣的可以验证一下,很奇怪,网上有不少文章都说如果连接数满了,也会导致Aborted_connects状态变量增加,实际上这个是不会引起状态变量Aborted_connects变化的。

4、 超过连接时间限制,主要是这个参数connect_timeout控制(mysql默认是10s,基本上,除非网络环境极端不好,一般不会超时。)

首先在一台MySQL数据库服务器上执行下面命令,我们用Linux下的netem与tc命令模拟构造出复杂环境下的网络传输延时案例,延时11秒。

# tc qdisc add dev eth0 root netem delay 11000ms

在另外一台MySQL服务器ping这台MySLQ服务器,如下所示,你会看到网络时延为11秒

# ping 10.20.57.24
PING 10.20.57.24 (10.20.57.24) 56(84) bytes of data.
64 bytes from 10.20.57.24: icmp_seq=1 ttl=61 time=11001 ms
64 bytes from 10.20.57.24: icmp_seq=2 ttl=61 time=11001 ms
64 bytes from 10.20.57.24: icmp_seq=3 ttl=61 time=11001 ms
64 bytes from 10.20.57.24: icmp_seq=4 ttl=61 time=11001 ms
64 bytes from 10.20.57.24: icmp_seq=5 ttl=61 time=11001 ms

此时访问MySQL数据库,由于网络时延为11秒,超出了系统变量connect_timeout的10秒,就会出现下面错误,此时状态变量Aborted_connects的值变化!

# mysql -h 10.20.57.24 -u test -p
Enter password:
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading authorization packet', system error: 0

那么如何区分状态变量Aborted Connect是那个引起的呢? 单从状态变量本身是无法区分的,但是可以结合performance_schema.host_cache来稍微做判别、甄别。

COUNT_NAMEINFO_PERMANENT_ERRORS IP到主机名称DNS解析期间的永久性错误数。
COUNT_AUTHENTICATION_ERRORS 验证失败导致的错误数量
SUM_CONNECT_ERRORS: 被视为“ 阻塞 ”的连接错误的数量 (根据max_connect_errors系统变量进行评估)。只有协议握手错误才会被计数,只有通过验证(HOST_VALIDATED = YES)的主机才会被计数

1、 客户端没有权限但是尝试访问MySQL数据库。

每次都会引起COUNT_AUTHENTICATION_ERRORS增1 ,第一次会引起COUNT_NAMEINFO_PERMANENT_ERRORS也增1

2、 客户端输入的密码有误

每次都会引起COUNT_AUTHENTICATION_ERRORS增1 ,第一次会引起COUNT_NAMEINFO_PERMANENT_ERRORS也增1

其实对于与1和2,两者无法判别,最简单有效的将系统变量log_warnings设置为2,然后分析、查看错误日志信息:

mysql> set global log_warnings=2;

Query OK, 0 rows affected (0.00 sec)



mysql> 

那么此时1和2都会记录到错误日志里面去,然后你就可以通过分析错误日志,结合状态变量Aborted Connect来分析, 如下测试案例所示:

2018-06-20 22:44:16 18026 [Warning] IP address '192.168.xxx.xxx' could not be resolved: Name or service not known
2018-06-20 22:44:16 18026 [Warning] Access denied for user 'kkkk'@'192.168.xxx.xxx' (using password: YES)
2018-06-20 22:45:18 18026 [Warning] Access denied for user 'test'@'192.168.xxx.xxx' (using password: YES)

3、 A connection packet does not contain the right information

每次引起COUNT_HANDSHAKE_ERRORS增1,
每次引起SUM_CONNECT_ERRORS增1

C:Users>psping 10.20.57.24:3306



PsPing v2.10 - PsPing - ping, latency, bandwidth measurement utility

Copyright (C) 2012-2016 Mark Russinovich

Sysinternals - www.sysinternals.com



TCP connect to 10.20.57.24:3306:

5 iterations (warmup 1) ping test:

Connecting to 10.20.57.24:3306 (warmup): from 192.168.103.34:55327: 1.93ms

Connecting to 10.20.57.24:3306: from 192.168.103.34:55328: 10.08ms

Connecting to 10.20.57.24:3306: from 192.168.103.34:55329: 3.35ms

Connecting to 10.20.57.24:3306: from 192.168.103.34:55330: 3.71ms

Connecting to 10.20.57.24:3306: from 192.168.103.34:55331: 2.32ms



TCP connect statistics for 10.20.57.24:3306:

  Sent = 4, Received = 4, Lost = 0 (0% loss),

  Minimum = 2.32ms, Maximum = 10.08ms, Average = 4.87ms

未分类

4、 超过连接时间限制

如果是超时引起,那么就会出现下面状况:

  • 每次引起SUM_CONNECT_ERRORS增1,
  • 每次引起COUNT_HANDSHAKE_ERRORS增1
  • 第一次会引起COUNT_NAMEINFO_PERMANENT_ERRORS增1

注意: 3与4不会写入错误日志,3与4的区别可以通过COUNT_NAMEINFO_PERMANENT_ERRORS的值来区别。

未分类

下面我们来实验测试一下状态变量Aborted Clients的变化因素,

1、 客户端程序在退出之前未调用mysql_close()正确关闭MySQL连接。

在实验前,使用flush status清理一下状态变量

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> show status like 'Abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 0     |
+------------------+-------+
2 rows in set (0.00 sec)

mysql> 

写一个简单的Python测试脚本python_mysql.py,如下所示,将关闭数据库连接的地方dbcon.close注释掉,

import mysql.connector

try:

 dbcon=mysql.connector.connect(
 host='127.0.0.1',
 user='root' ,
 passwd='xxxxxxx',
 database='information_schema'
 )

 cursor= dbcon.cursor()
 sql_tex='select count(*) from MyDB.test'
 cursor.execute(sql_tex)
 dtlist= cursor.fetchall()
 print dtlist
except mysql.connector.Error as e:

  print('operation the sql fail!{0}'.format(e))

finally:

  cursor.close;
 # dbcon.close;

然后执行一下脚本,检查状态变量Aborted_clients,然后发现状态变量Aborted_clients的值增1了。

[root@DB-Server kerry]# python python_mysql.py
[(99999,)]

未分类

2、 客户端休眠的时间超过了系统变量wait_timeout和interactive_timeout的值,导致连接被MySQL进程终止

mysql> show global variables like 'interactive_timeout';
+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| interactive_timeout | 28800 |
+---------------------+-------+
1 row in set (0.00 sec)

mysql> show global variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

mysql> 

将全局系统变量interactive_timeout 和wait_timeout设置为4秒

mysql> set global interactive_timeout=4;
Query OK, 0 rows affected (0.00 sec)

mysql> set global wait_timeout=4;
Query OK, 0 rows affected (0.00 sec)

mysql> show status like 'Abort%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_clients  | 0     |
| Aborted_connects | 0     |
+------------------+-------+
2 rows in set (0.00 sec)

然后在客户端连接到MySQL数据库,不做任何操作,过来4秒后,你去操作就会出现错误“ERROR 2013 (HY000): Lost connection to MySQL server during query”

# mysql -h 10.20.57.24 -u test -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 43
Server version: 5.6.20-enterprise-commercial-advanced-log MySQL Enterprise Server - Advanced Edition (Commercial)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> select current_user();
ERROR 2013 (HY000): Lost connection to MySQL server during query
mysql> 

在MySQL服务器你就会看到状态变量Aborted_clients变为1了。

mysql> show status like 'Abort%';

+------------------+-------+

| Variable_name    | Value |

+------------------+-------+

| Aborted_clients  | 1     |

| Aborted_connects | 0     |

+------------------+-------+

2 rows in set (0.00 sec

还有其他一些原因(客户端异常中断或查询超出了max_allowed_packet值)由于不方便构造,在此略过。另外,其实我们还可以通过tcpdump抓包工具来追踪分析。下面举个例子(这里

简单介绍一下tcpdump,后续文章再做展开分析)

在MySQL服务器使用tcpdump抓包

[root@DB-Server ~]# tcpdump -i eth0  port 3306  -s 1500 -w tcpdump.log

然后在另外一台MySQL服务器,使用不存在的账号或错误的密码访问MySQL数据库

# mysql -h 10.20.57.24 -u kkk  -p
Enter password:
ERROR 1045 (28000): Access denied for user 'kkk'@'192.168.7.208' (using password: YES)
# mysql -h 10.20.57.24 -u test -p
Enter password:
ERROR 1045 (28000): Access denied for user 'test'@'192.168.7.208' (using password: YES)
[root@GETLNX28 ~]#

执行完命令后,你可以使用CTRL + C结束抓包分析,然后查看分析。如下截图所示:

[root@DB-Server ~]# tcpdump -i eth0  port 3306  -s 1500 -w tcpdump.log
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 1500 bytes
28 packets captured
28 packets received by filter
0 packets dropped by kernel
[root@DB-Server ~]# strings tcpdump.log

未分类

未分类

MySQL索引优化分析

为什么你写的sql查询慢?为什么你建的索引常失效?通过本章内容,你将学会MySQL性能下降的原因,索引的简介,索引创建的原则,explain命令的使用,以及explain输出字段的意义。助你了解索引,分析索引,使用索引,从而写出更高性能的sql语句。还在等啥子?撸起袖子就是干!

案例分析

我们先简单了解一下非关系型数据库和关系型数据库的区别。
MongoDB是NoSQL中的一种。NoSQL的全称是Not only SQL,非关系型数据库。它的特点是性能高,扩张性强,模式灵活,在高并发场景表现得尤为突出。但目前它还只是关系型数据库的补充,它在数据的一致性,数据的安全性,查询的复杂性问题上和关系型数据库还存在一定差距。
MySQL是关系性数据库中的一种,查询功能强,数据一致性高,数据安全性高,支持二级索引。但性能方面稍逊与MongoDB,特别是百万级别以上的数据,很容易出现查询慢的现象。这时候需要分析查询慢的原因,一般情况下是程序员sql写的烂,或者是没有键索引,或者是索引失效等原因导致的。
公司ERP系统数据库主要是MongoDB(最接近关系型数据的NoSQL),其次是Redis,MySQL只占很少的部分。现在又重新使用MySQL,归功于阿里巴巴的奇门系统和聚石塔系统。考虑到订单数量已经是百万级以上,对MySQL的性能分析也就显得格外重要。

我们先通过两个简单的例子来入门。后面会详细介绍各个参数的作用和意义。
说明:需要用到的sql已经放在了github上了,喜欢的同学可以点一下star,哈哈。https://github.com/ITDragonBlog/daydayup/tree/master/MySQL/

场景一:订单导入,通过交易号避免重复导单

业务逻辑:订单导入时,为了避免重复导单,一般会通过交易号去数据库中查询,判断该订单是否已经存在。

最基础的sql语句

mysql> select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+
| id    | transaction_id     | gross | net  | stock_id | order_status | descript | finance_descript | create_type | order_level | input_user | input_date          |
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+
| 10000 | 81X97310V32236260E |   6.6 | 6.13 |        1 |           10 | ok       | ok               | auto        |           1 | itdragon   | 2017-08-18 17:01:49 |
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+

mysql> explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table               | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+

查询的本身没有任何问题,在线下的测试环境也没有任何问题。可是,功能一旦上线,查询慢的问题就迎面而来。几百上千万的订单,用全表扫描?啊?哼!
怎么知道该sql是全表扫描呢?通过explain命令可以清楚MySQL是如何处理sql语句的。打印的内容分别表示:
id : 查询序列号为1。
select_type : 查询类型是简单查询,简单的select语句没有union和子查询。
table : 表是 itdragon_order_list。
partitions : 没有分区。
type : 连接类型,all表示采用全表扫描的方式。
possible_keys : 可能用到索引为null。
key: 实际用到索引是null。
key_len : 索引长度当然也是null。
ref: 没有哪个列或者参数和key一起被使用。
Extra: 使用了where查询。
因为数据库中只有三条数据,所以rows和filtered的信息作用不大。这里需要重点了解的是type为ALL,全表扫描的性能是最差的,假设数据库中有几百万条数据,在没有索引的帮助下会异常卡顿。

初步优化:为transaction_id创建索引

mysql> create unique index idx_order_transaID on itdragon_order_list (transaction_id);
mysql> explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+
| id | select_type | table               | partitions | type  | possible_keys      | key                | key_len | ref   | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | const | idx_order_transaID | idx_order_transaID | 453     | const |    1 |      100 | NULL  |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+

这里创建的索引是唯一索引,而非普通索引。
唯一索引打印的type值是const。表示通过索引一次就可以找到。即找到值就结束扫描返回查询结果。
普通索引打印的type值是ref。表示非唯一性索引扫描。找到值还要继续扫描,直到将索引文件扫描完为止。(这里没有贴出代码)
显而易见,const的性能要远高于ref。并且根据业务逻辑来判断,创建唯一索引是合情合理的。

再次优化:覆盖索引

mysql> explain select transaction_id from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+
| id | select_type | table               | partitions | type  | possible_keys      | key                | key_len | ref   | rows | filtered | Extra       |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | const | idx_order_transaID | idx_order_transaID | 453     | const |    1 |      100 | Using index |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+

这里将select * from 改为了 select transaction_id from 后
Extra 显示 Using index,表示该查询使用了覆盖索引,这是一个非常好的消息,说明该sql语句的性能很好。若提示的是Using filesort(使用内部排序)和Using temporary(使用临时表)则表明该sql需要立即优化了。
根据业务逻辑来的,查询结构返回transaction_id 是可以满足业务逻辑要求的。

场景二,订单管理页面,通过订单级别和订单录入时间排序

业务逻辑:优先处理订单级别高,录入时间长的订单。
既然是排序,首先想到的应该是order by, 还有一个可怕的 Using filesort 等着你。

最基础的sql语句

mysql> explain select * from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table               | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |      100 | Using filesort |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+

首先,采用全表扫描就不合理,还使用了文件排序Using filesort,更加拖慢了性能。
MySQL在4.1版本之前文件排序是采用双路排序的算法,由于两次扫描磁盘,I/O耗时太长。后优化成单路排序算法。其本质就是用空间换时间,但如果数据量太大,buffer的空间不足,会导致多次I/O的情况。其效果反而更差。与其找运维同事修改MySQL配置,还不如自己乖乖地建索引。

初步优化:为order_level,input_date 创建复合索引

mysql> create index idx_order_levelDate on itdragon_order_list (order_level,input_date);
mysql> explain select * from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table               | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |      100 | Using filesort |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+

创建复合索引后你会惊奇的发现,和没创建索引一样???都是全表扫描,都用到了文件排序。是索引失效?还是索引创建失败?我们试着看看下面打印情况

mysql> explain select order_level,input_date from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+
| id | select_type | table               | partitions | type  | possible_keys | key                 | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | index | NULL          | idx_order_levelDate | 68      | NULL |    3 |      100 | Using index |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+

将select * from 换成了 select order_level,input_date from 后。type从all升级为index,表示(full index scan)全索引文件扫描,Extra也显示使用了覆盖索引。可是不对啊!!!!检索虽然快了,但返回的内容只有order_level和input_date 两个字段,让业务同事怎么用?难道把每个字段都建一个复合索引?
MySQL没有这么笨,可以使用force index 强制指定索引。在原来的sql语句上修改 force index(idx_order_levelDate) 即可。

mysql> explain select * from itdragon_order_list force index(idx_order_levelDate) order by order_level,input_date;
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+
| id | select_type | table               | partitions | type  | possible_keys | key                 | key_len | ref  | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | index | NULL          | idx_order_levelDate | 68      | NULL |    3 |      100 | NULL  |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+

再次优化:订单级别真的要排序么?
其实给订单级别排序意义并不大,给订单级别添加索引意义也不大。因为order_level的值可能只有,低,中,高,加急,这四种。对于这种重复且分布平均的字段,排序和加索引的作用不大。
我们能否先固定 order_level 的值,然后再给 input_date 排序?如果查询效果明显,是可以推荐业务同事使用该查询方式。

mysql> explain select * from itdragon_order_list where order_level=3 order by input_date;
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table               | partitions | type | possible_keys       | key                 | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | itdragon_order_list | NULL       | ref  | idx_order_levelDate | idx_order_levelDate | 5       | const |    1 |      100 | Using index condition |
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+

和之前的sql比起来,type从index 升级为 ref(非唯一性索引扫描)。索引的长度从68变成了5,说明只用了一个索引。ref也是一个常量。Extra 为Using index condition 表示自动根据临界值,选择索引扫描还是全表扫描。总的来说性能远胜于之前的sql。

上面两个案例只是快速入门,我们需严记一点:优化是基于业务逻辑来的。绝对不能为了优化而擅自修改业务逻辑。如果能修改当然是最好的。

索引简介

官方定义:索引(Index) 是帮助MySQL高效获取数据的数据结构。
大家一定很好奇,索引为什么是一种数据结构,它又是怎么提高查询的速度?我们拿最常用的二叉树来分析索引的工作原理。看下面的图片:
未分类
创建索引的优势
1 提高数据的检索速度,降低数据库IO成本:使用索引的意义就是通过缩小表中需要查询的记录的数目从而加快搜索的速度。
2 降低数据排序的成本,降低CPU消耗:索引之所以查的快,是因为先将数据排好序,若该字段正好需要排序,则真好降低了排序的成本。

创建索引的劣势
1 占用存储空间:索引实际上也是一张表,记录了主键与索引字段,一般以索引文件的形式存储在磁盘上。
2 降低更新表的速度:表的数据发生了变化,对应的索引也需要一起变更,从而减低的更新速度。否则索引指向的物理数据可能不对,这也是索引失效的原因之一。
3 优质索引创建难:索引的创建并非一日之功,也并非一直不变。需要频繁根据用户的行为和具体的业务逻辑去创建最佳的索引。

索引分类

我们常说的索引一般指的是BTree(多路搜索树)结构组织的索引。其中还有聚合索引,次要索引,复合索引,前缀索引,唯一索引,统称索引,当然除了B+树外,还有哈希索引(hash index)等。

单值索引:一个索引只包含单个列,一个表可以有多个单列索引
唯一索引:索引列的值必须唯一,但允许有空值
复合索引:一个索引包含多个列,实际开发中推荐使用
实际开发中推荐使用复合索引,并且单表创建的索引个数建议不要超过五个

基本语法:
创建:

create [unique] index indexName on tableName (columnName...)
alter tableName add [unique] index [indexName] on (columnName...)

删除:

drop index [indexName] on tableName

查看:

show index from tableName

哪些情况需要建索引:
1 主键,唯一索引
2 经常用作查询条件的字段需要创建索引
3 经常需要排序、分组和统计的字段需要建立索引
4 查询中与其他表关联的字段,外键关系建立索引

哪些情况不要建索引:
1 表的记录太少,百万级以下的数据不需要创建索引
2 经常增删改的表不需要创建索引
3 数据重复且分布平均的字段不需要创建索引,如 true,false 之类。
4 频发更新的字段不适合创建索引
5 where条件里用不到的字段不需要创建索引

性能分析

MySQL 自身瓶颈

MySQL自身参见的性能问题有磁盘空间不足,磁盘I/O太大,服务器硬件性能低。
1 CPU:CPU 在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候
2 IO:磁盘I/O 瓶颈发生在装入数据远大于内存容量的时候
3 服务器硬件的性能瓶颈:top,free,iostat 和 vmstat来查看系统的性能状态

explain 分析sql语句

使用explain关键字可以模拟优化器执行sql查询语句,从而得知MySQL 是如何处理sql语句。

+----+-------------+-------+------------+------+---------------+-----+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-----+---------+------+------+----------+-------+

id

select 查询的序列号,包含一组可以重复的数字,表示查询中执行sql语句的顺序。一般有三种情况:
第一种:id全部相同,sql的执行顺序是由上至下;
第二种:id全部不同,sql的执行顺序是根据id大的优先执行;
第三种:id既存在相同,又存在不同的。先根据id大的优先执行,再根据相同id从上至下的执行。

select_type

select 查询的类型,主要是用于区别普通查询,联合查询,嵌套的复杂查询
simple:简单的select 查询,查询中不包含子查询或者union
primary:查询中若包含任何复杂的子查询,最外层查询则被标记为primary
subquery:在select或where 列表中包含了子查询
derived:在from列表中包含的子查询被标记为derived(衍生)MySQL会递归执行这些子查询,把结果放在临时表里。
union:若第二个select出现在union之后,则被标记为union,若union包含在from子句的子查询中,外层select将被标记为:derived
union result:从union表获取结果的select

partitions

表所使用的分区,如果要统计十年公司订单的金额,可以把数据分为十个区,每一年代表一个区。这样可以大大的提高查询效率。

type

这是一个非常重要的参数,连接类型,常见的有:all , index , range , ref , eq_ref , const , system , null 八个级别。
性能从最优到最差的排序:system > const > eq_ref > ref > range > index > all
对java程序员来说,若保证查询至少达到range级别或者最好能达到ref则算是一个优秀而又负责的程序员。
all:(full table scan)全表扫描无疑是最差,若是百万千万级数据量,全表扫描会非常慢。
index:(full index scan)全索引文件扫描比all好很多,毕竟从索引树中找数据,比从全表中找数据要快。
range:只检索给定范围的行,使用索引来匹配行。范围缩小了,当然比全表扫描和全索引文件扫描要快。sql语句中一般会有between,in,>,< 等查询。
ref:非唯一性索引扫描,本质上也是一种索引访问,返回所有匹配某个单独值的行。比如查询公司所有属于研发团队的同事,匹配的结果是多个并非唯一值。
eq_ref:唯一性索引扫描,对于每个索引键,表中有一条记录与之匹配。比如查询公司的CEO,匹配的结果只可能是一条记录,
const:表示通过索引一次就可以找到,const用于比较primary key 或者unique索引。因为只匹配一行数据,所以很快,若将主键至于where列表中,MySQL就能将该查询转换为一个常量。
system:表只有一条记录(等于系统表),这是const类型的特列,平时不会出现,了解即可

possible_keys

显示查询语句可能用到的索引(一个或多个或为null),不一定被查询实际使用。仅供参考使用。

key

显示查询语句实际使用的索引。若为null,则表示没有使用索引。

key_len

显示索引中使用的字节数,可通过key_len计算查询中使用的索引长度。在不损失精确性的情况下索引长度越短越好。key_len 显示的值为索引字段的最可能长度,并非实际使用长度,即key_len是根据表定义计算而得,并不是通过表内检索出的。

ref

显示索引的哪一列或常量被用于查找索引列上的值。

rows

根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数,值越大越不好。

extra

Using filesort: 说明MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为“文件排序” 。出现这个就要立刻优化sql。
Using temporary: 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和 分组查询 group by。 出现这个更要立刻优化sql。
Using index: 表示相应的select 操作中使用了覆盖索引(Covering index),避免访问了表的数据行,效果不错!如果同时出现Using where,表明索引被用来执行索引键值的查找。如果没有同时出现Using where,表示索引用来读取数据而非执行查找动作。
覆盖索引(Covering Index) :也叫索引覆盖,就是select 的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select 列表中的字段,而不必根据索引再次读取数据文件。
Using index condition: 在5.6版本后加入的新特性,优化器会在索引存在的情况下,通过符合RANGE范围的条数 和 总数的比例来选择是使用索引还是进行全表遍历。
Using where: 表明使用了where 过滤
Using join buffer: 表明使用了连接缓存
impossible where: where 语句的值总是false,不可用,不能用来获取任何元素
distinct: 优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作。

filtered

一个百分比的值,和rows 列的值一起使用,可以估计出查询执行计划(QEP)中的前一个表的结果集,从而确定join操作的循环次数。小表驱动大表,减轻连接的次数。

通过explain的参数介绍,我们可以得知:
1 表的读取顺序(id)
2 数据读取操作的操作类型(type)
3 哪些索引被实际使用(key)
4 表之间的引用(ref)
5 每张表有多少行被优化器查询(rows)

性能下降的原因

从程序员的角度
1 查询语句写的不好
2 没建索引,索引建的不合理或索引失效
3 关联查询有太多的join
从服务器的角度
1 服务器磁盘空间不足
2 服务器调优配置参数设置不合理

总结

1 索引是排好序且快速查找的数据结构。其目的是为了提高查询的效率。
2 创建索引后,查询数据变快,但更新数据变慢。
3 性能下降的原因很可能是索引失效导致。
4 索引创建的原则,经常查询的字段适合创建索引,频繁需要更新的数据不适合创建索引。
5 索引字段频繁更新,或者表数据物理删除容易造成索引失效。
6 擅用 explain 分析sql语句
7 除了优化sql语句外,还可以优化表的设计。如尽量做成单表查询,减少表之间的关联。设计归档表等。

到这里,MySQL的索引优化分析就结束了,有什么不对的地方,大家可以提出来。如果觉得不错可以点一下推荐。

Mysql中-Xtrabackup备份和恢复应用

关于Xtrabackup(又或innobackupex)的介绍,详细参考官方文档https://www.percona.com/doc/percona-xtrabackup/2.1/index.html

Xtrabackup安装指南

文件准备

[root@wuxiang11 ~]# cd percona-xtrabackup/
[root@wuxiang11 percona-xtrabackup]# ls
libev-4.15-1.el6.rf.x86_64.rpm  percona-release-0.1-4.noarch.rpm  percona-xtrabackup-24-2.4.4-1.el7.x86_64.rpm

开始安装依赖文件

[root@wuxiang11 percona-xtrabackup]# rpm -ivh libev-4.15-1.el6.rf.x86_64.rpm 
warning: libev-4.15-1.el6.rf.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 6b8d79e6: NOKEY
Preparing...                ########################################### [100%]
   1:libev                  ########################################### [100%]

安装-01

[root@wuxiang11 percona-xtrabackup]# rpm -ivH percona-release-0.1-4.noarch.rpm
Preparing packages for installation...
percona-release-0.1-4
[root@wuxiang11 percona-xtrabackup]# yum list | grep percona

马上就安装完成

[root@wuxiang11 percona-xtrabackup]# yum install percona-xtrabackup-24

Loaded plugins: fastestmirror, security
Setting up Install Process
Loading mirror speeds from cached hostfile
Resolving Dependencies
--> Running transaction check
---> Package percona-xtrabackup-24.x86_64 0:2.4.11-1.el6 will be installed
--> Processing Dependency: perl(DBD::mysql) for package: percona-xtrabackup-24-2.4.11-1.el6.x86_64
--> Running transaction check
---> Package perl-DBD-MySQL.x86_64 0:4.013-3.el6 will be installed
--> Processing Dependency: perl(DBI::Const::GetInfoType) for package: perl-DBD-MySQL-4.013-3.el6.x86_64
--> Processing Dependency: perl(DBI) for package: perl-DBD-MySQL-4.013-3.el6.x86_64
--> Processing Dependency: libmysqlclient.so.16(libmysqlclient_16)(64bit) for package: perl-DBD-MySQL-4.013-3.el6.x86_64
--> Processing Dependency: libmysqlclient.so.16()(64bit) for package: perl-DBD-MySQL-4.013-3.el6.x86_64
--> Running transaction check
---> Package Percona-Server-shared-51.x86_64 0:5.1.73-rel14.12.625.rhel6 will be installed
---> Package perl-DBI.x86_64 0:1.609-4.el6 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

===============================================================================================================================================================================================
 Package                                           Arch                            Version                                               Repository                                       Size
===============================================================================================================================================================================================
Installing:
 percona-xtrabackup-24                             x86_64                          2.4.11-1.el6                                          percona-release-x86_64                          8.1 M
Installing for dependencies:
 Percona-Server-shared-51                          x86_64                          5.1.73-rel14.12.625.rhel6                             percona-release-x86_64                          2.1 M
 perl-DBD-MySQL                                    x86_64                          4.013-3.el6                                           centos6.7_64                                    134 k
 perl-DBI                                          x86_64                          1.609-4.el6                                           centos6.7_64                                    705 k

Transaction Summary
===============================================================================================================================================================================================
Install       4 Package(s)

Total download size: 11 M
Installed size: 12 M
Is this ok [y/N]: y
Downloading Packages:
(1/4): Percona-Server-shared-51-5.1.73-rel14.12.625.rhel6.x86_64.rpm                                                                                                    | 2.1 MB     00:02     
(2/4): percona-xtrabackup-24-2.4.11-1.el6.x86_64.rpm                                                                                                                    | 8.1 MB     00:01     
(3/4): perl-DBD-MySQL-4.013-3.el6.x86_64.rpm                                                                                                                            | 134 kB     00:00     
(4/4): perl-DBI-1.609-4.el6.x86_64.rpm                                                                                                                                  | 705 kB     00:00     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total                                                                                                                                                          2.5 MB/s |  11 MB     00:04     
warning: rpmts_HdrFromFdno: Header V4 DSA/SHA1 Signature, key ID cd2efd2a: NOKEY
Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Percona
Importing GPG key 0xCD2EFD2A:
 Userid : Percona MySQL Development Team <[email protected]>
 Package: percona-release-0.1-4.noarch (installed)
 From   : /etc/pki/rpm-gpg/RPM-GPG-KEY-Percona
Is this ok [y/N]: y
Running rpm_check_debug
Running Transaction Test
Transaction Test Succeeded
Running Transaction
Warning: RPMDB altered outside of yum.
** Found 3 pre-existing rpmdb problem(s), 'yum check' output follows:
2:postfix-2.6.6-6.el6_5.x86_64 has missing requires of libmysqlclient.so.16()(64bit)
2:postfix-2.6.6-6.el6_5.x86_64 has missing requires of libmysqlclient.so.16(libmysqlclient_16)(64bit)
2:postfix-2.6.6-6.el6_5.x86_64 has missing requires of mysql-libs
  Installing : perl-DBI-1.609-4.el6.x86_64                                                                                                                                                 1/4 
  Installing : Percona-Server-shared-51-5.1.73-rel14.12.625.rhel6.x86_64                                                                                                                   2/4 
  Installing : perl-DBD-MySQL-4.013-3.el6.x86_64                                                                                                                                           3/4 
  Installing : percona-xtrabackup-24-2.4.11-1.el6.x86_64                                                                                                                                   4/4 
  Verifying  : percona-xtrabackup-24-2.4.11-1.el6.x86_64                                                                                                                                   1/4 
  Verifying  : perl-DBD-MySQL-4.013-3.el6.x86_64                                                                                                                                           2/4 
  Verifying  : Percona-Server-shared-51-5.1.73-rel14.12.625.rhel6.x86_64                                                                                                                   3/4 
  Verifying  : perl-DBI-1.609-4.el6.x86_64                                                                                                                                                 4/4 

Installed:
  percona-xtrabackup-24.x86_64 0:2.4.11-1.el6                                                                                                                                                  

Dependency Installed:
  Percona-Server-shared-51.x86_64 0:5.1.73-rel14.12.625.rhel6                      perl-DBD-MySQL.x86_64 0:4.013-3.el6                      perl-DBI.x86_64 0:1.609-4.el6                     

Complete!

检验是否安装成功

[root@wuxiang11 percona-xtrabackup]# rpm -qa|grep xtrabackup
percona-xtrabackup-24-2.4.11-1.el6.x86_64
[root@wuxiang11 percona-xtrabackup]#

备份思路

Xtrabackup提供了全量备份和增量备份两种方式,全量就不解释了,增量是指其可以只备份指定位置后的新增数据。本文介绍的增量备份方法使用到了LSN(Log Sequence Number),可以从备份成功文件夹的xtrabackup_checkpoints文件中获取,其中to_lsn就是下次增量备份的起始位置。
结合两者,我们可以得到下图的备份思路。

未分类

解释如下:

由于全量备份和增量备份的命令参数不同,所以首先要判断是否需要全量备份。
如果是全量备份,那么便执行命令,备份成功后,记录xtrabackup_checkpoints文件中的to_lsn,以便后面进行增量备份
如果是增量备份,那么首先读取上次备份时记录下的to_lsn,如果读取失败,报错并结束。否则执行相应命令,备份成功后记录to_lsn。
编写一个简单的脚本:db-backup.sh,如下所示:

#!/bin/sh

# xtrabackup的相关配置
INNOBACKUPEX="innobackupex "
MY_CNF="/home/config/mysql/3307.backup.cnf"
MY_USER="xtrabackup"
MY_PASSWORD="xtrabackup"
MY_SOCKET="/home/socket/mysql/3307.sock"

# 远程备份机 文件名配置
REMOTE_HOST="dbbackup"
REMOTE_DIR="/home/backup/mysql/test"
LOCAL_LSN_FILE="~/.to_lsn_important"
DATE_NAME=`date +%Y-%m-%d-%H-%M-%S`
REMOTE_FILE=$DATE_NAME.tar.gz
LOCK_FILE="~/.mysql.backup.lock"
LOCAL_BACKUP_DIR="/home/backup/mysql/test/$DATE_NAME"

# 输出帮助信息
function usage()
{
    echo "Usage:"
    echo "-f db will be backuped fully with this parameter. If not , incrementally. "
}

#防止同时执行两个备份命令,发生冲突
if [ -f $LOCK_FILE ] ;then
    echo 'Mysql backup lockfile is locked!'
    exit 0
fi

full=0

while getopts "fh" arg #选项后面的冒号表示该选项需要参数
do
    case $arg in
        f)  
            full=1
            ;;
        h)  # 输出帮助信息
            usage
            exit 0
            ;;
    esac
done

echo "backup dir is $REMOTE_DIR/$REMOTE_FILE"

# backup up db to remote host
echo "start to backup db!"`date +%Y-%m-%d-%H-%M-%S`
if [ "$full"x = "1"x ] ;then
    # 全量备份
    echo '1' > $LOCK_FILE
    $INNOBACKUPEX --defaults-file=$MY_CNF --user=$MY_USER --password=$MY_PASSWORD --socket=$MY_SOCKET ./ --stream=tar | gzip | ssh $REMOTE_HOST "cat - > $REMOTE_DIR/FULL-$REMOTE_FILE"
    ssh $REMOTE_HOST "cd $REMOTE_DIR;rm -f xtrabackup_checkpoints;tar zxfi $REMOTE_DIR/FULL-$REMOTE_FILE xtrabackup_checkpoints "
    toLSN=$( ssh $REMOTE_HOST "cat $REMOTE_DIR/xtrabackup_checkpoints|grep to_lsn|awk -F= '{gsub(/ /,"",$2);print $2}'" )
    if [ $toLSN ] ;then
        echo $toLSN > $LOCAL_LSN_FILE
    else
        echo 'no lsn from remote host!please check!'
    fi
else
    # 增量备份
    if [ -f $LOCAL_LSN_FILE ] ;then
        toLSN=`cat $LOCAL_LSN_FILE`
    fi

    if [ ! $toLSN ] ;then
        echo 'last LSN is not set !please check!'
        exit 0
    fi

    echo '1' > $LOCK_FILE
    mkdir -p $LOCAL_BACKUP_DIR
    echo "last to lsn is "$toLSN
    $INNOBACKUPEX --parallel=6 --defaults-file=$MY_CNF --user=$MY_USER --password=$MY_PASSWORD --socket=$MY_SOCKET --incremental --incremental-lsn=$toLSN $LOCAL_BACKUP_DIR 2>/tmp/innobackexLog
    toLSN=$( cd $LOCAL_BACKUP_DIR/*; cat xtrabackup_checkpoints|grep to_lsn|awk -F= '{gsub(/ /,"",$2);print $2}' )
    echo "new to lsn is "$toLSN;
    if [ $toLSN ] ;then
        echo $toLSN > $LOCAL_LSN_FILE
        cd $LOCAL_BACKUP_DIR/*;tar zc .|ssh $REMOTE_HOST "cat - > $REMOTE_DIR/$REMOTE_FILE"
        echo "save file to $REMOTE_HOST @ $REMOTE_DIR/$REMOTE_FILE"
    else
        echo 'no lsn from local backup file!delete remote backup file!'$LOCAL_BACKUP_DIR/$REMOTE_FILE
    fi
    echo "remove $LOCAL_BACKUP_DIR"
    rm -rf $LOCAL_BACKUP_DIR
fi

rm -f $LOCK_FILE
echo "end to backup db!"`date +%Y-%m-%d-%H-%M-%S`

将上述脚本配置好后,放到mysql主机上,执行 sh db-backup.sh -f进行全量备份,之后将sh backup.sh添加到crontab中每小时增量备份一次就可以了。

注意–parallel=6使用是有条件的,参考文档https://www.percona.com/doc/percona-xtrabackup/2.1/innobackupex/parallel_copy_ibk.html

恢复思路

一旦线上数据库发生宕机之类的灾难性事故,备份数据就要立马发挥作用了。打开备份文件夹一看,如上面的图,每小时一个压缩文件,时间久了,茫茫多,上百个压缩文件,简直是一定的,这必须得来个脚本自动化恢复数据才成。思路很简单,见下图:

未分类

解释如下:

  1. 先解压,没啥说的。
  2. 找到全量备份的数据,因为增量备份也是以此为基准进行的。上面备份的时候,全量备份文件命名以FULL开头便是为了此处便于查找。
  3. 先恢复全量备份数据,然后按照时间顺序恢复增量备份的数据。
  4. 将数据恢复到mysql中
  5. 启动mysql,连上看看有没问题。

简单的实现脚本,我们叫它restore.sh,如下所示

#!/bin/sh
BACK_DIR="/home/backup/mysql/test"
RESTORE_DIR="/home/tmpback/test"

# xtrabackup恢复时使用的配置文件可能与数据库配置文件不同
RESTORE_MY_CNF="/home/config/mysql/3307.backup.cnf"

# 启动mysql需要的配置文件,按需设定
MY_CNF="/home/config/mysql/3307.cnf"
MY_DATA_DIR="/home/data/mysql/3307"
MY_SOCK_DIR="/home/socket/mysql/"
MY_LOG_DIR="/home/logs/mysqld/3307"

#extract all tar file
fullFileName=""
for file in $( ls $BACK_DIR | grep "tar.gz" )
do
    fileName=${file%.tar.gz}
    full=${file%%-*}
    if [ "${full}"x = "FULL"x ] ;then
        fullFileName=$fileName
    fi
    DEST_DIR="$RESTORE_DIR/$fileName"
    if [ -d $DEST_DIR ] ;then
        echo "$DEST_DIR exists!"
    else
        mkdir -p $DEST_DIR
        echo "start to extract file $file to $DEST_DIR"
        tar zxif $BACK_DIR/$file -C $RESTORE_DIR/$fileName
        echo "end to extract file "$file
    fi
done

echo "full backup dir is "$fullFileName

echo "**start to repare full backup dir"

# 恢复全量数据
echo "innobackupex --apply-log --redo-only --use-memory=4G $RESTORE_DIR/$fullFileName"
innobackupex --apply-log --redo-only --use-memory=4G "$RESTORE_DIR/$fullFileName"

echo "**end to repare full backup dir"

# 恢复增量数据
for file in $( ls $RESTORE_DIR|grep -v "FULL" )
do
    echo "**start to repare $file"
    echo "innobackupex --apply-log --redo-only --use-memory=4G $RESTORE_DIR/$fullFileName --incremental-dir=$RESTORE_DIR/$file"
    innobackupex --apply-log --redo-only --use-memory=4G "$RESTORE_DIR/$fullFileName" --incremental-dir="$RESTORE_DIR/$file"

    echo "**end to repare $file"

done

echo "**start to copy back data to mysql"
rm -rf $MY_DATA_DIR
mkdir $MY_DATA_DIR
mkdir $MY_SOCK_DIR
mkdir $MY_LOG_DIR

#将数据恢复到mysql中
echo "innobackupex --defaults-file=$RESTORE_MY_CNF --copy-back $RESTORE_DIR/$fullFileName"
innobackupex --defaults-file=$RESTORE_MY_CNF --copy-back $RESTORE_DIR/$fullFileName
echo "**end to copy back data to mysql"

chown -R mysql:mysql $MY_DATA_DIR
chown -R mysql:mysql $MY_SOCK_DIR
chown -R mysql:mysql $MY_LOG_DIR

echo "All data has been restored! Try to start mysql now"

echo "mysqld_multi --defaults-file=$MY_CNF start 1"

使用方法很简单,配置好后,直接运行sh restore.sh就可以了。