[MySQL FAQ]系列 — 为什么InnoDB表要建议用自增列做主键

我们先了解下InnoDB引擎表的一些关键特征:

  • InnoDB引擎表是基于B+树的索引组织表(IOT);
  • 每个表都需要有一个聚集索引(clustered index);
  • 所有的行记录都存储在B+树的叶子节点(leaf pages of the tree);
  • 基于聚集索引的增、删、改、查的效率相对是最高的;
  • 如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择其作为聚集索引;
  • 如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引;
  • 如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。

综上总结,如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的,也就是下面这几种情况的存取效率最高:

  • 使用自增列(INT/BIGINT类型)做主键,这时候写入顺序是自增的,和B+数叶子节点分裂顺序一致;
  • 该表不指定自增列做主键,同时也没有可以被选为主键的唯一索引(上面的条件),这时候InnoDB会选择内置的ROWID作为主键,写入顺序和ROWID增长顺序一致;
  • 除此以外,如果一个InnoDB表又没有显示主键,又有可以被选择为主键的唯一索引,但该唯一索引可能不是递增关系时(例如字符串、UUID、多字段联合唯一索引的情况),该表的存取效率就会比较差。

实际情况是如何呢?经过简单TPCC基准测试,修改为使用自增列作为主键与原始表结构分别进行TPCC测试,前者的TpmC结果比后者高9%倍,足见使用自增列做InnoDB表主键的明显好处,其他更多不同场景下使用自增列的性能提升可以自行对比测试下。

附图:

1、B+树典型结构

B+tree

2、InnoDB主键逻辑结构

Innodb-primary-key

 

延伸阅读:

1、TPCC-MySQL使用手册

2、B+Tree index structures in InnoDB

3、B+Tree Indexes and InnoDB – Percona

4、MySQL官方手册: Clustered and Secondary Indexes

常用PC服务器阵列卡、硬盘健康监控

通常,我们使用的DELL/HP/IBM三家的机架式PC级服务器阵列卡是从LSI的卡OEM出来的,DELL和IBM两家的阵列卡原生程度较高,没有做太多封装,可以用原厂提供的阵列卡管理工具进行监控;而HP的阵列卡一般都做过封装了,因此需要使用自身特有的管理工具来监控。

本文以几种常用的阵列卡为例,展示其阵列卡及硬盘监控的方法。
DELL SAS 6/iR卡,全称LSI Logic SAS1068E,只支持RAID 0, RAID 1, RAID 1+0, 不支持RAID 5等高级RAID特性,不支持阵列卡电池。
DELL PERC PERC H700卡,全称LSI Logic MegaRAID SAS 2108,支持各种RAID级别及高级特性,可选配阵列卡电池。
DELL PERC H310 Mini卡 ,全称LSI Logic / Symbios Logic MegaRAID SAS 2008,支持常见RAID级别,不支持高级RAID特性,不支持阵列卡电池。
IBM ServeRAID M5014 SAS/SATA Controller卡,全称LSI Logic / Symbios Logic MegaRAID SAS 2108,支持各种RAID级别及高级特性,可选配阵列卡电池。
IBM ServeRAID-MR10i SAS/SATA Controller卡,全称LSI Logic / Symbios Logic MegaRAID SAS 1078,支持常见RAID级别,不支持高级RAID特性,可选配阵列卡电池,这个卡其实和DELL的PERC 6/i卡是一样的,都是基于LSI MegaRAID SAS 1078基础上OEM出来的。

上面是几种常见的阵列卡型号,更多的可以自行查看官方的技术手册。

下面我们要继续的是,这些阵列卡以及硬盘如何监控,阵列卡的管理也请查看官方技术手册,不在本文讨论范畴,或者查看作者的一个分享PPT:PC服务器阵列卡管理简易手册。

一般地,支持RAID 5的卡,我们称其为阵列卡,都可以使用LSI官方提供的MegaCli工具来管理,而不支持RAID 5的卡,我们称其为SAS卡,使用lsiutil工具来管理。HP的服务器使用其特有的hpacucli工具来管理。

1、MegaCli工具
a) MegaCli -adpallinfo -aall — 查看阵列卡信息
-a 参数指定阵列卡的编号,一般服务器上只会配一个阵列卡,因此我们通常指定为 -a0(阵列卡适配器编号,从0开始) 即可,主要关注下面几个信息:

状态值 对应含义
Product Name : PERC H710 Mini  阵列卡名称
FW Package Build: 21.2.0-0007  阵列卡firmware版本号,版本如果太低,建议升级以提高稳定性及性能
BBU : Present  是否有配BBU电池

b) MegaCli -cfgdsply -aall — 查看阵列配置

状态值 对应含义
Memory: 512MB  阵列卡cache大小,2的N次方,如果不是,说明阵列卡有异常
Number of dedicated Hotspares: 0  阵列是否有专用/独享热备盘(如果有多个逻辑磁盘组/disk group,则可以指定一个硬盘用于全局热备,那么该disk group上的专用热备盘数量为0也不用担心),除了RAID 1/RAID 1+0一般不指定热备盘以外,其他几个阵列级别建议都要指定热备盘
State : Optimal  阵列状态,如果不是 Optimal 就要关注了
Current Cache Policy: WriteBack, ReadAheadNone, Direct, Write Cache OK if Bad BBU  阵列读写cache策略,建议写策略设置为FORCE WB,最起码是WB,预读策略可以关掉,意义不大,几乎没影响
Disk Cache Policy : Disabled  硬盘cache策略,建议关闭,防止意外时数据丢失
Current Power Savings Policy: None  节电策略,建议关闭
Media Error Count: 0  三个错误计数器,任何一个值大于100就要立刻引起关注,尤其要关注起增长速度。1T以上SATA盘,计数值不够精确,可能所有盘上该值都会大于0,一般重启就会重新清0,如果重启后还是大于0的话,赶紧报修吧。SAS盘的计数值则比较准确。
Other Error Count: 0
Predictive Failure Count: 0
Firmware state: Online, Spun Up  查看硬盘状态,如果是unconfigured表示该硬盘未分配加入到阵列中;如果是 unconfigured(bad)表示该盘不但是未分配,而且还坏了,正是“出师未捷身先死”;如果是failed,表示该盘故障无法识别;如果是rebuilding,表示该盘正在重建数据

c) MegaCli -adpbucmd -aall — 查看阵列卡电池信息

状态值 对应含义
Temperature: 39 C  查看电池温度,如果相比上一次查看高出不少,就需要关注了,或者可以根据经验设置一个基线值
Battery State: Optimal  电池状态,如果不是为Optimal,就需要关注了
Charger Status: Complete  电池充放电状态
isSOHGood: Yes  电池状态,如果不是为Yes,需要关注
Relative State of Charge: 93 %  当前电量,当电量低于15%,或者电池坏掉时,默认都会将写策略从WB改成WT,除非设定为FORCE WB策略
Max Error = 0 %  电池是否有错误信息
Next Learn time: Tue Oct 14 22:06:50 2014  电池充放电时间,注意这是美国时间。另外,新的阵列卡电池很多改成电容式的了,也就不需要重复充放电了

d) MegaCli -fwtermlog -dsply -aALL 查看阵列卡日志,关注里面的error/fail/warn等多个关键字

2、lsiutil工具
lsiutil有交互和非交互两种方式,作为监控,我们肯定选择非交互模式。想要使用交互模式的,可以根据非交互模式自行练习。
a) lsiutil -p 1 -a 20,12,0,0 — 查看硬盘计数器
Invalid DWord Count 2,563 — 任何一个值大于0,都需要引起关注
Running Disparity Error Count 2,366
Loss of DWord Synch Count 0
Phy Reset Problem Count 0

b) lsiutil -p 1 -a 21,1,0,0,0 — 查看逻辑卷状态

状态值 对应含义
Volume State: optimal, enabled  逻辑卷健康状况
Volume draws from Hot Spare Pools: 0  是否有热备
Volume Size 139392 MB, 2 Members  由几块硬盘组成
Primary is PhysDisk 1 (Bus 0 Target 9)  物理硬盘1
Secondary is PhysDisk 0 (Bus 0 Target 3)  物理硬盘0

c) lsiutil -p 1 -a 21,2,0,0,0 — 查看物理硬盘状态

状态值 对应含义
PhysDisk 0 is Bus 0 Target 3  编号
PhysDisk State: online  状态
Error Count 13, Last Error: Command = 28h, Key = 3, ASC/ASCQ = 11h/00h  错误计数器,大于0的话,就需要引起关注

3、hpacucli工具
hpacucli工具查看阵列、硬盘、电池信息,其实就只要一条指令:
hpacucli ctrl all show config detail — 查看阵列详细信息、配置

状态值 对应含义
Controller Status: OK 阵列卡状态
Firmware Version: 1.18 firmware版本,太低了建议升级,以提高稳定性及性能
Cache Board Present: True 是否配备了cache模块
Cache Status: OK cache模块状态
Cache Ratio: 100% Read / 0% Write cache策略,此处只有读cache,不用于写cache,因为没有bbu电池,见下方结果
Drive Write Cache: Disabled 关闭磁盘cache
Total Cache Size: 256 MB cache大小
Total Cache Memory Available: 208 MB 实际可用cache大小,和理论cache大小不一样,说明cache模块可能有问题
No-Battery Write Cache: Disabled 关闭FORCEWB策略
Battery/Capacitor Count: 0 阵列卡BBU电池数量为0,也就是没有BBU模块
Battery/Capacitor Status: Failed (Replace Batteries) 阵列卡BBU电池状态,这里显示是错误状态,需要及时更换
Array: A 第一个乌列阵列,编号从A开始,依次是A、B、C
Status: OK 物理阵列状态
Logical Drive: 1 第一个逻辑卷,编号从1开始
Fault Tolerance: RAID 5 第一个逻辑卷的阵列级别
Status: OK 第一个逻辑卷状态
Caching: Enabled 第一个逻辑卷是否启用了cache策略
physicaldrive 1I:1:1 第一块物理硬盘,编号从1开始
Status: OK 第一块物理硬盘状态
Firmware Revision: HPDA 第一块物理硬盘firmware,如果太低,也需要及时升级,HP的硬盘每个批次都有不同的firmware

延伸阅读:

http://www.lsi.com/downloads/Public/Obsolete/Obsolete%20Common%20Files/mr_sas_stor_ug.pdf
http://www.hp.com/ctg/Manual/c00709035.pdf
http://www.wafl.co.uk/tag/sasadmin/
http://docs.oracle.com/cd/E19121-01/sf.x4200/819-1157-23/F_BIOS_RAID.html

[MySQL FAQ]系列 — 线上环境到底要不要开启query cache

Query Cache(查询缓存,以下简称QC)存储SELECT语句及其产生的数据结果,特别适用于:频繁提交同一个语句,并且该表数据变化不是很频繁的场景,例如一些静态页面,或者页面中的某块不经常发生变化的信息。QC有可能会从InnoDB Buffer Pool或者MyISAM key buffer里读取结果。

由于QC需要缓存最新数据结果,因此表数据发生任何变化(INSERT、UPDATE、DELETE或其他可能产生数据变化的操作),都会导致QC被刷新。

根据MySQL官方的测试,QC的优劣分别是:

1、如果对一个表执行简单的查询,但每次查询都不一样的话,打开QC后,性能反而下降了13%左右。但通常实际业务中,通常不会只有这种请求,因此实际影响应该比这个小一些。

2、如果对一个只有一行数据的表进行查询,则可以提升238%,这个效果还是非常不错的。

因此,如果是在一个更新频率非常低而只读查询频率非常高的场景下,打开QC还是比较有优势的,其他场景下,则不建议使用。而且,QC一般也维持在100MB以内就够了,没必要设置超过数百MB。

QC严格要求2次SQL请求要完全一样,包括SQL语句,连接的数据库、协议版本、字符集等因素都会影响,下面几个例子中的SQL会被认为是完全不一样而不会使用同一个QC内存块:

mysql> set names latin1; SELECT * FROM table_name;
mysql> set names latin1; select * from table_name;
mysql> set names utf8; select * from table_name;

此外,QC也不适用于下面几个场景:

1、子查询或者外层查询;
2、存储过程、存储函数、触发器、event中调用的SQL,或者引用到这些结果的;
3、包含一些特殊函数时,例如:BENCHMARK()、CURDATE()、CURRENT_TIMESTAMP()、NOW()、RAND()、UUID()等等;
4、读取mysql、INFORMATION_SCHEMA、performance_schema 库数据的;
5、类似SELECT…LOCK IN SHARE MODE、SELECT…FOR UPDATE、SELECT..INTO OUTFILE/DUMPFILE、SELECT..WHRE…IS NULL等语句;
6、SELECT执行计划用到临时表(TEMPORARY TABLE);
7、未引用任何表的查询,例如 SELECT 1+1 这种;
8、产生了 warnings 的查询;
9、SELECT语句里加了 SQL_NO_CACHE 关键字;

更加奇葩的是,MySQL在从QC中取回结果前,会先判断执行SQL的用户是否有全部库、表的SELECT权限,如果没有,则也不会使用QC。

相比下面这个,其实上面所说的都不重要。

最为重要的是,在MySQL里QC是由一个全局锁在控制,每次更新QC的内存块都需要进行锁定。
例如,一次查询结果是20KB,当前 query_cache_min_res_unit 值设置为 4KB(默认值就是4KB,可调整),那么么本次查询结果共需要分为5次写入QC,每次都要锁定,可见其成本有多高。

我们可以通过 PROFILING 功能来查看 QC 相关的一些锁竞争,例如像下面这样的:

• Waiting for query cache lock
• Waiting on query cache mutex

或者,也可以通过执行 SHOW PROCESSLIST 来看线程的状态,例如:

• checking privileges on cached query
检查用户是否有权限读取QC中的结果集

• checking query cache for query
检查本次查询结果是否已经存储在QC中

• invalidating query cache entries
由于相关表数据已经修改了,因此将QC中的内存记录被标记为失效

• sending cached result to client
从QC中,将缓存后的结果返回给客户程序

• storing result in query cache
将查询结果缓存到QC中

如果可以频繁看到上述几种状态,那么说明当前QC基本存在比较重的竞争。

说了这么多废话,其实核心要点就一个:
如果线上环境中99%以上都是只读,很少有更新,再考虑开启QC吧,否则,就别开了。
关闭方法很简单,有两种:

1、同时设置选项 query_cache_type = 0 和 query_cache_size = 0;
2、如果用源码编译MySQL的话,编译时增加参数 --without-query-cache 即可;

延伸阅读:
http://www.dbasquare.com/kb/how-query-cache-can-cause-performance-problems/
http://www.percona.com/blog/2012/09/05/write-contentions-on-the-query-cache/
http://dev.mysql.com/doc/refman/5.6/en/query-cache.html

[MySQL FAQ]系列 — 打开general log到底影响多大

我们知道,有时候为了debug或跟踪方便,会临时打开MySQL的general log。如果在线业务请求比较频繁的话,会导致瞬间产生大量的日志,一定程度上会影响IOPS性能。

此外,我们还有一种变通的办法,那就是打开slow query log,然后设置 long_query_time = 0,这样也可以记录所有请求log,而且记录的log比general log还要来的小,他产生的IOPS性能影响可能会比直接打开general log的影响来的小,可事实果真如此吗?我们来对比测试下就知道了。

测试试用MySQL版本:5.5.5-10.0.11-MariaDB-log MariaDB Server
测试工具: tpcc-mysql
测试Warehouse数: 100
warmup time: 60s
run time: 600s
并发线程数: 512

测试结果对比见下:

mysql-faq-impact-of-general-log

在“一般场景”下,我是设置 long_query_time = 1,并且关闭general log。

记录全部general log时的TpmC大约是不打开log时的73.28%,而记录全部slow log时的TpmC大约是不打开log时的59.53%。可见,直接打开general log对TpmC的影响更小一些,而且这种模式下产生的log其实也更小一些。是不是有点毁三观,哈哈O(∩_∩)O~

此外,如果 log-output = TABLE 时结果会怎样,请读者自行测试 :)

备注:MySQL 5.1及以上版本,才支持将long_query_time设置为0秒,在这之前的版本,其最小值是1秒。

[MySQL FAQ]系列 — 不同的binlog_format会导致哪些SQL不会被记录

我们都知道binlog_format有三种可选配置:STATEMENT、ROW、MIXED,相应地,基于这三种模式的Replication分别称为SBR(STATEMENT BASED Replication)、RBR、MBR。 同时,我们也知道,MySQL Replication可以支持比较灵活的binlog规则,可以设置某些库、某些表记录或者忽略不记录。

通常地,我们强烈建议不要设置这些规则,默认都记录就好,在Slave上也是如此,默认所有库都进行Replicate,不要设置DO、IGNORE、REWRITE规则。 如果非要设置这些规则的话,可能会导致某些场景下或者某些特定的SQL无法被记录,就需要特别注意了。

我经过比较简单的测试,不同的binlog_format可能会导致某些SQL不被记录的情况总结如下:

mysql-faq-how-binlog-format-affect-replication
上面的测试区分了两种模式,一种是连接时指定了其他数据库,一种是连接时未指定任何数据库,相当于下面的两种方式:
#假设do/ignore规则中的DB名字叫DoDB/IgnoreDB/RewriteDB的话,OtherDB是规则之外的其他DB

#一种是:连接时指定了do/ignore/rewrite规则之外的其他DB
mysql -h host -u user -p passwd -p port -A OtherDB

#还有一种是:连接时不指定任何DB
mysql -h host -u user -p passwd -p port -A

#tips,加上 -A 是--no-auto-rehash的缩写,其作用是连接后不读取数据库、表、字段信息。与其相反的选项是 --auto-rehash,也就是连接后会读取数据库、表、字段信息,以便自动补齐。
更多情况请读者自行进行测试吧 :)

[MySQL FAQ]系列 — MySQL复制中slave延迟监控

在MySQL复制环境中,我们通常只根据 Seconds_Behind_Master 的值来判断SLAVE的延迟。这么做大部分情况下尚可接受,但并不够准确,而应该考虑更多因素。

首先,我们先看下SLAVE的状态:

yejr@imysql.com [(none)]> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
***
Master_Log_File: mysql-bin.000327
Read_Master_Log_Pos: 668711237
Relay_Log_File: mysql-relay-bin.002999
Relay_Log_Pos: 214736858
Relay_Master_Log_File: mysql-bin.000327
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
***
Skip_Counter: 0
Exec_Master_Log_Pos: 654409041
Relay_Log_Space: 229039311
***
Seconds_Behind_Master: 3296
***

可以看到 Seconds_Behind_Master 的值是 3296,也就是SLAVE至少延迟了 3296 秒。

我们再来看下SLAVE上的2个REPLICATION进程状态:

yejr@imysql.com [(none)]> show full processlist\G
*************************** 1. row ***************************
Id: 6
User: system user
Host:
db: NULL
Command: Connect
Time: 22005006
State: Waiting for master to send event
Info: NULL
*************************** 2. row ***************************
Id: 7
User: system user
Host:
db: NULL
Command: Connect
Time: 3293
State: Updating
Info: UPDATE ** SET ** WHERE **

可以看到SQL线程一直在执行UPDATE操作,注意到 Time 的值是 3293,看起来像是这个UPDATE操作执行了3293秒,一个普通的SQL而已,肯定不至于需要这么久。
实际上,在REPLICATION进程中,Time 这列的值可能有几种情况:
1、SQL线程当前执行的binlog(实际上是relay log)中的timestamp和IO线程最新的timestamp的差值,这就是通常大家认为的 Seconds_Behind_Master 值,并不是某个SQL的实际执行耗时;
2、SQL线程当前如果没有活跃SQL在执行的话,Time值就是SQL线程的idle time;

而IO线程的Time值则是该线程自从启动以来的总时长(多少秒),如果系统时间在IO线程启动后发生修改的话,可能会导致该Time值异常,比如变成负数,或者非常大。

来看下面几个状态:

#设置pager,只查看关注的几个status值
yejr@imysql.com [(none)]> pager cat | egrep -i 'system user|Exec_Master_Log_Pos|Seconds_Behind_Master|Read_Master_Log_Pos'

#这是没有活跃SQL的情况,Time值是idle time,并且 Seconds_Behind_Master 为 0
yejr@imysql.com [(none)]> show processlist; show slave status\G
| 6 | system user | | NULL | Connect | 22004245 | Waiting for master to send event | NULL |
| 7 | system user | | NULL | Connect | 13 | Has read all relay log;**
Read_Master_Log_Pos: 445167889
Exec_Master_Log_Pos: 445167889
Seconds_Behind_Master: 0

#和上面一样
yejr@imysql.com [(none)]> show processlist; show slave status\G
| 6 | system user | | NULL | Connect | 22004248 | Waiting for master to send event | NULL |
| 7 | system user | | NULL | Connect | 16 | Has read all relay log;**
Read_Master_Log_Pos: 445167889
Exec_Master_Log_Pos: 445167889
Seconds_Behind_Master: 0

#这时有活跃SQL了,Time值是和 Seconds_Behind_Master 一样,即SQL线程比IO线程“慢”了1秒
yejr@imysql.com [(none)]> show processlist; show slave status\G
| 6 | system user | | NULL | Connect | 22004252 | Waiting for master to send event | NULL |
| 7 | system user | | floweradmin | Connect | 1 | Updating | update **
Read_Master_Log_Pos: 445182239
Exec_Master_Log_Pos: 445175263
Seconds_Behind_Master: 1

#和上面一样
yejr@imysql.com [(none)]> show processlist; show slave status\G
| 6 | system user | | NULL | Connect | 22004254 | Waiting for master to send event | NULL |
| 7 | system user | | floweradmin | Connect | 1 | Updating | update **
Read_Master_Log_Pos: 445207174
Exec_Master_Log_Pos: 445196837
Seconds_Behind_Master: 1

好了,最后我们说下如何正确判断SLAVE的延迟情况:
1、首先看 Relay_Master_Log_FileMaster_Log_File 是否有差异;
2、如果Relay_Master_Log_FileMaster_Log_File 是一样的话,再来看Exec_Master_Log_PosRead_Master_Log_Pos 的差异,对比SQL线程比IO线程慢了多少个binlog事件;
3、如果Relay_Master_Log_FileMaster_Log_File 不一样,那说明延迟可能较大,需要从MASTER上取得binlog status,判断当前的binlog和MASTER上的差距;

因此,相对更加严谨的做法是:
在第三方监控节点上,对MASTER和SLAVE同时发起SHOW BINARY LOGSSHOW SLAVE STATUS\G的请求,最后判断二者binlog的差异,以及 Exec_Master_Log_PosRead_Master_Log_Pos 的差异。

例如:
在MASTER上执行SHOW BINARY LOGS 的结果是:

+------------------+--------------+
| Log_name | File_size |
+------------------+--------------+
| mysql-bin.000009 | 1073742063 |
| mysql-bin.000010 | 107374193 |
+------------------+--------------+

而在SLAVE上执行SHOW SLAVE STATUS\G 的结果是:

Master_Log_File: mysql-bin.000009
 Read_Master_Log_Pos: 668711237
Relay_Master_Log_File: mysql-bin.000009
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
***
Exec_Master_Log_Pos: 654409041

***
Seconds_Behind_Master: 3296
***

这时候,SLAVE实际的延迟应该是:
mysql-bin.000009 这个binlog中的binlog position 1073742063 和 SLAVE上读取到的binlog position之间的差异延迟,即:

1073742063 - 654409041 = 419333022 个binlog event

并且还要加上 mysql-bin.000010这个binlog已经产生的107374193个binlog event,共

107374193 + 419333022 = 526707215 个binlog event

后记更新:

1、可以在MASTER上维护一个监控表,它只有一个字段,存储这最新最新时间戳(高版本可以采用event_scheduler来更新,低版本可以用cron结合自动循环脚本来更新),在SLAVE上读取该字段的时间,只要MASTER和SLAVE的系统时间一致,即可快速知道SLAVE和MASTER延迟差了多少。不过,在高并发的系统下,这个时间戳可以细化到毫秒,否则哪怕时间一致,也是有可能会延迟数千个binlog event的。
2、网友(李大玉,QQ:407361231)细心支出上面的计算延迟有误,应该是mysql-bin.000009的最大事件数减去已经被执行完的事件数,即1073742063 – 654409041= 419333022个binlog event,再加上mysql-bin.000010这个binlog已经产生的107374193个binlog event,共526707215 个binlog event。

[MySQL FAQ]系列 — 你所不知的table is full那些事

当我们要写入新数据而发生“The table is full”告警错误时,先不要着急,按照下面的思路来逐步分析即可:
1、查看操作系统以及MySQL的错误日志文件
确认操作系统的文件系统没有报错,并且MySQL的错误日志文件中是否有一些最直观的可见的错误提示。
有可能是数据库文件超过操作系统层的文件大小限制,比如fat/fat32以及低版本的Linux,文件最大不可以大于2G(最大扩展到4G),这就需要转换fat32为NTFS,或升级Linux版本。

2、确认磁盘空间没有满
执行 df -h 查看剩余磁盘空间,如果发现磁盘空间确实已经用完,则尽快删除不需要的文件。

如果通过 du 计算各个目录的总和却发现根本不会用完磁盘空间时,就需要注意了,可能是某个被删除的文件还没完全释放,导致 df 看起来已经用完,但 du 却又统计不到。
这时候可以执行 lsof | grep -i deleted 找到被删除的大文件,将其对应的进程杀掉,释放该文件描述符。

如果该进程不能被杀掉,例如是 mysqld 进程在占用的话,可以在 MySQL 里找到是哪个内部线程在用,停止该线程即可。

曾经发生过这样一个例子:
用vim打开MySQL的slow query log,退出时选择了 “wq” 指令,也就是保存退出,结果悲剧发生了。
因为在其打开的那段时间内,slow query log有新日志产生,会持续写入,但他退出时采用保存退出的方式,变成了一个“新”文件(或者说新文件句柄 file handler),这个“新”文件无法被mysqld进程识别,
mysqld进程依旧将slow query log写入到原来它打开的那个文件(或者说文件句柄)里,该日志文件在持续增长,但手工保存退出的文件却再也不增长了,直接查看文件看不出任何异常。
这时候只能用 lsof -p `pidof mysqld` 才能看到该文件。
解决方法很简单,将原来的文件备份一下,执行下面的指令:

FLUSH SLOW LOGS;

备注:MySQL 5.5开始才支持 BINARY/ENGINE/ERROR/GENERAL/RELAY/SLOW 等关键字,之前的版本只能刷新全部日志。

3、确认数据表状态

  • 如果是MyISAM引擎

默认配置下,MyISAM引擎最大可支持256TB(myisam_data_pointer_size = 6,256^6 = 256TB),除非操作系统层有限制。
在MySQL5.0中,MyISAM引擎行记录默认是动态长度,单表最大可达256TB,MyISAM行指针(myisam_data_pointer_size)长度为6字节。
在这之前,MyISAM行指针默认长度为4字节,只支持4GB的数据。改行指针最大值可设为8字节。
在行指针设置较小不够用的时候,为提高MyISAM表最大容量,可以修改表定义设定MAX_ROWS的值:

ALTER TABLE `xx` ENGINE=MyISAM MAX_ROWS=nn

备注:表定义中,AVG_ROW_LENGTH 属性定义的是 BLOB/TEXT 字段类型的最大长度。

  • 如果是InnoDB引擎

ibdata*共享表空间最后一个文件没有设置成自增长,或者超过32位系统的单文件大小限制。
解决方法:
1、ibdata*的最后一个文件(非最后一个文件无法设置为自动增长)设置成自动增长;
2、检查操作系统,迁移到64位操作系统下;
3、转成独立表空间;
4、删除历史数据,重整表空间;

  • 如果是MEMORY引擎

1、适当提高max_heap_table_size设置(注意该值是会话级别,不要设置过大,例如1GB,一般不建议超过256MB);
2、执行ALTER TABLE t_mem ENGINE=MEMORY; 重整表空间,否则无法写入新数据
3、删除部分历史数据或者直接清空,重整表空间;
4、设置 big_tables = 1,将所有临时表存储在磁盘,而非内存中,缺点是如果某个SQL执行时需要用到临时表,则性能会差很多;

顺便说下,如果数据表有一列自增INT做主键,但是该ID值达到了INT最大值的话,MyISAM、MEMORY、InnoDB三种引擎的告警信息是不一样的。
InnoDB引擎的告警信息类似这样:
ERROR 1467 (HY000): Failed to read auto-increment value from storage engine

而MyISAM和MEMORY引擎则都是这样:
ERROR 1062 (23000): Duplicate entry ‘4294967295’ for key ‘PRIMARY’

参考

MySQL手册:B.5.2.12 The table is full

[MySQL FAQ]系列 — 如何安全地关闭MySQL实例

本文分析了mysqld进程关闭的过程,以及如何安全、缓和地关闭MySQL实例,对这个过程不甚清楚的同学可以参考下。

关闭过程:

  • 1、发起shutdown,发出  SIGTERM信号
  • 2、有必要的话,新建一个关闭线程(shutdown thread)

如果是客户端发起的关闭,则会新建一个专用的关闭线程

如果是直接收到 SIGTERM 信号进行关闭的话,专门负责信号处理的线程就会负责关闭工作,或者新建一个独立的线程负责这个事

当无法创建独立的关闭线程时(例如内存不足),MySQL Server会发出类似下面的告警信息:

Error: Can’t create thread to kill server

  • 3、MySQL Server不再响应新的连接请求

关闭TCP/IP网络监听,关闭Unix Socket等渠道

  • 4、逐渐关闭当前的连接、事务

空闲连接,将立刻被终止;

当前还有事务、SQL活动的连接,会将其标识为 killed,并定期检查其状态,以便下次检查时将其关闭;(参考 KILL 语法)

当前有活跃事务的,该事物会被回滚,如果该事务中还修改了非事务表,则已经修改的数据无法回滚,可能只会完成部分变更;

如果是Master/Slave复制场景里的Master,则对复制线程的处理过程和普通线程也是一样的;

如果是Master/Slave复制场景里的Slave,则会依次关闭IO、SQL线程,如果这2个线程当前是活跃的,则也会加上 killed 标识,然后再关闭;

Slave服务器上,SQL线程是允许直接停止当前的SQL操作的(为了避免复制问题),然后再关闭该线程;

在MySQl 5.0.80及以前的版本里,如果SQL线程当时正好执行一个事务到中间,该事务会回滚;从5.0.81开始,则会等待所有的操作结束,除非用户发起KILL操作。

当Slave的SQL线程对非事务表执行操作时被强制 KILL了,可能会导致Master、Slave数据不一致;

  • 5、MySQL Server进程关闭所有线程,关闭所有存储引擎;

刷新所有表cache,关闭所有打开的表;

每个存储引擎各自负责相关的关闭操作,例如MyISAM会刷新所有等待写入的操作;InnoDB会将buffer pool刷新到磁盘中(从MySQL 5.0.5开始,如果innodb_fast_shutdown不设置为 2 的话),把当前的LSN记录到表空间中,然后关闭所有的内部线程。

  • 6、MySQL Server进程退出

关于KILL指令

从5.0开始,KILL 支持指定  CONNECTION | QUERY两种可选项:

  • KILL CONNECTION和原来的一样,停止回滚事务,关闭该线程连接,释放相关资源;
  • KILL QUERY则只停止线程当前提交执行的操作,其他的保持不变;

提交KILL操作后,该线程上会设置一个特殊的 kill标记位。通常需要一段时间后才能真正关闭线程,因为kill标记位只在特定的情况下才检查:

  • 1、执行SELECT查询时,在ORDER BY或GROUP BY循环中,每次读完一些行记录块后会检查 kill标记位,如果发现存在,该语句会终止;
  • 2、执行ALTER TABLE时,在从原始表中每读取一些行记录块后会检查 kill 标记位,如果发现存在,该语句会终止,删除临时表;
  • 3、执行UPDATE和DELETE时,每读取一些行记录块并且更新或删除后会检查 kill 标记位,如果发现存在,该语句会终止,回滚事务,若是在非事务表上的操作,则已发生变更的数据不会回滚;
  • 4、GET_LOCK() 函数返回NULL;
  • 5、INSERT DELAY线程会迅速内存中的新增记录,然后终止;
  • 6、如果当前线程持有表级锁,则会释放,并终止;
  • 7、如果线程的写操作调用在等待释放磁盘空间,则会直接抛出“磁盘空间满”错误,然后终止;
  • 8、当MyISAM表在执行REPAIR TABLE 或 OPTIMIZE TABLE 时被 KILL的话,会导致该表损坏不可用,指导再次修复完成。

安全关闭MySQL几点建议

想要安全关闭 mysqld 服务进程,建议按照下面的步骤来进行:

  • 0、用具有SUPER、ALL等最高权限的账号连接MySQL,最好是用 unix socket 方式连接;
  • 1、在5.0及以上版本,设置innodb_fast_shutdown = 1,允许快速关闭InnoDB(不进行full purge、insert buffer merge),如果是为了升级或者降级MySQL版本,则不要设置;
  • 2、设置innodb_max_dirty_pages_pct = 0,让InnoDB把所有脏页都刷新到磁盘中去;
  • 3、设置max_connections和max_user_connections为1,也就最后除了自己当前的连接外,不允许再有新的连接创建;
  • 4、关闭所有不活跃的线程,也就是状态为Sleep  且 Time 大于 1 的线程ID;
  • 5、执行 SHOW PROCESSLIST  确认是否还有活跃的线程,尤其是会产生表锁的线程,例如有大数据集的SELECT,或者大范围的UPDATE,或者执行DDL,都是要特别谨慎的;
  • 6、执行 SHOW ENGINE INNODB STATUS 确认History list length的值较低(一般要低于500),也就是未PURGE的事务很少,并且确认Log sequence number、Log flushed up to、Last checkpoint at三个状态的值一样,也就是所有的LSN都已经做过检查点了;
  • 7、然后执行FLUSH LOCKAL TABLES 操作,刷新所有 table cache,关闭已打开的表(LOCAL的作用是该操作不记录BINLOG);
  • 8、如果是SLAVE服务器,最好是先关闭 IO_THREAD,等待所有RELAY LOG都应用完后,再关闭 SQL_THREAD,避免 SQL_THREAD 在执行大事务被终止,耐心待其全部应用完毕,如果非要强制关闭的话,最好也等待大事务结束后再关闭SQL_THREAD;
  • 9、最后再执行 mysqladmin shutdown。
  • 10、紧急情况下,可以设置innodb_fast_shutdown = 1,然后直接执行 mysqladmin shutdown 即可,甚至直接在操作系统层调用 kill 或者 kill -9 杀掉 mysqld 进程(在innodb_flush_log_at_trx_commit = 0 的时候可能会丢失部分事务),不过mysqld进程再次启动时,会进行CRASH RECOVERY工作,需要有所权衡。

啰嗦那么多,其实正常情况下执行 mysqladmin shutdown 就够了,如果发生阻塞,再参考上面的内容进行分析和解决吧,哈哈:)

MySQL 5.6 查询优化器新特性的“BUG”

最近碰到一个慢SQL问题,解决过程有点小曲折,和大家分享下。 SQL本身不复杂,表结构、索引也比较简单,不过个别字段存在于多个索引中。

CREATE TABLE `pre_forum_post` (
  `pid` int(10) unsigned NOT NULL,
  `fid` mediumint(8) unsigned NOT NULL DEFAULT '0',
  `tid` mediumint(8) unsigned NOT NULL DEFAULT '0',
  `first` tinyint(1) NOT NULL DEFAULT '0',
  `author` varchar(40) NOT NULL DEFAULT '',
  `authorid` int(10) unsigned NOT NULL DEFAULT '0',
  `subject` varchar(80) NOT NULL DEFAULT '',
  `dateline` int(10) unsigned NOT NULL DEFAULT '0',
  `message` mediumtext NOT NULL,
  `useip` varchar(15) NOT NULL DEFAULT '',
  `invisible` tinyint(1) NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `usesig` tinyint(1) NOT NULL DEFAULT '0',
  `htmlon` tinyint(1) NOT NULL DEFAULT '0',
  `bbcodeoff` tinyint(1) NOT NULL DEFAULT '0',
  `smileyoff` tinyint(1) NOT NULL DEFAULT '0',
  `parseurloff` tinyint(1) NOT NULL DEFAULT '0',
  `attachment` tinyint(1) NOT NULL DEFAULT '0',
  `rate` smallint(6) NOT NULL DEFAULT '0',
  `ratetimes` tinyint(3) unsigned NOT NULL DEFAULT '0',
  `status` int(10) NOT NULL DEFAULT '0',
  `tags` varchar(255) NOT NULL DEFAULT '0',
  `comment` tinyint(1) NOT NULL DEFAULT '0',
  `replycredit` int(10) NOT NULL DEFAULT '0',
  `position` int(8) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`tid`,`position`),
  UNIQUE KEY `pid` (`pid`),
  KEY `fid` (`fid`),
  KEY `displayorder` (`tid`,`invisible`,`dateline`),
  KEY `first` (`tid`,`first`),
  KEY `new_auth` (`authorid`,`invisible`,`tid`),
  KEY `idx_dt` (`dateline`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


"root@localhost Fri Aug  1 11:59:56 2014 11:59:56 [test]>show table status like 'pre_forum_post'\G
*************************** 1. row ***************************
           Name: pre_forum_post
         Engine: MyISAM
        Version: 10
     Row_format: Dynamic
           Rows: 23483977
 Avg_row_length: 203
    Data_length: 4782024708
Max_data_length: 281474976710655
   Index_length: 2466093056
      Data_free: 0
 Auto_increment: 1
    Create_time: 2014-08-01 11:00:56
    Update_time: 2014-08-01 11:08:49
     Check_time: 2014-08-01 11:12:23
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options: 
        Comment: 


mysql> show index from pre_forum_post;
+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table          | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| pre_forum_post |          0 | PRIMARY      |            1 | tid         | A         |      838713 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          0 | PRIMARY      |            2 | position    | A         |    23483977 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          0 | pid          |            1 | pid         | A         |    23483977 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | fid          |            1 | fid         | A         |        1470 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | displayorder |            1 | tid         | A         |      838713 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | displayorder |            2 | invisible   | A         |      869776 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | displayorder |            3 | dateline    | A         |    23483977 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | first        |            1 | tid         | A         |      838713 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | first        |            2 | first       | A         |     1174198 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | new_auth     |            1 | authorid    | A         |     1806459 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | new_auth     |            2 | invisible   | A         |     1956998 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | new_auth     |            3 | tid         | A         |    11741988 |     NULL | NULL   |      | BTREE      |         |               |
| pre_forum_post |          1 | idx_dt       |            1 | dateline    | A         |    23483977 |     NULL | NULL   |      | BTREE      |         |               |
+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

我们来看下这个SQL的执行计划:

mysql> explain select * from pre_forum_post where tid=7932612 and `invisible` in('0','-2') order by dateline  limit 15\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pre_forum_post
         type: index
possible_keys: PRIMARY,displayorder,first
          key: idx_dt
      key_len: 4
          ref: NULL
         rows: 14042
        Extra: Using where

可以看到执行计划比较奇怪,从几个可选的索引中,最终选择了 idx_dt,结果悲剧了,这个SQL执行耗时很长:

mysql> select * from pre_forum_post where tid=7932612 and `invisible` in('0','-2') order by dateline  limit 15;
15 rows in set (26.78 sec)

看下MySQL的会话状态值:Handler_read_next

| Handler_read_next          | 17274153 |

1700多万数据中选取15条记录,结果可想而知,非常慢。 我们强制指定比较靠谱的索引再看下:

mysql> explain select * from pre_forum_post force index(displayorder) where tid=7932612 and `invisible` in('0','-2') order by dateline  limit 15\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pre_forum_post
         type: range
possible_keys: displayorder
          key: displayorder
      key_len: 4
          ref: NULL
         rows: 46131
        Extra: Using index condition; Using filesort

看下实际执行的耗时:

mysql> select * from pre_forum_post force index(displayorder) where tid=7932612 and `invisible` in('0','-2') order by dateline  limit 15;
15 rows in set (0.08 sec)

尼玛,怎么可以这么快,查询优化器未免太坑爹了吧。 再看下MySQL的会话状态值:Handler_read_next

| Handler_read_next          | 31188 |

和不强制索引的情况相比,差了553倍! 所幸,5.6以上除了EXPLAIN外,还支持OPTIMIZER_TRACE,我们来观察下两种执行计划的区别,发现不强制指定索引时的执行计划有诈,会在最后判断到 ORDER BY 子句时,修改执行计划:

          {\
            "reconsidering_access_paths_for_index_ordering": {\
              "clause": "ORDER BY",\
              "index_order_summary": {\
                "table": "`pre_forum_post`",\
                "index_provides_order": true,\
                "order_direction": "asc",\
                "index": "idx_dt",\
                "plan_changed": true,\
                "access_type": "index_scan"\
              } /* index_order_summary */\
            } /* reconsidering_access_paths_for_index_ordering */\

而在前面analyzing_range_alternativesconsidered_execution_plans阶段,都认为其他几个索引也是可选择的,直到这里才给强X了,你Y 看起来像是MySQL 5.6查询优化器的bug了,GOOGLE了一下,还真发有人已经反馈过类似的问题: MySQL bug 70245: incorrect costing for range scan causes optimizer to choose incorrect index

看完才发现,其实不是神马BUG,而是原来从5.6开始,增加了一个选项叫eq_range_index_dive_limit 的高级货,这货大概的用途是: 在较多等值查询(例如多值的IN查询)情景中,预估可能会扫描的记录数,从而选择相对更合适的索引,避免所谓的index dive问题。

当面临下面两种选择时:

1、索引代价较高,但结果较为精确;
2、索引代价较低,但结果可能不够精确;

简单说,选项 eq_range_index_dive_limit 的值设定了 IN列表中的条件个数上线,超过设定值时,会将执行计划分支从 1 变成 2

该值默认为10,但社区众多人反馈较低了,因此在5.7版本后,将默认值调整为200了。

不过,今天我们这里的案例却是想反的,因为优化器选择了看似代价低但精确的索引,实际却选择了更低效的索引。 因此,我们需要将其阈值调低,尝试设置 eq_range_index_dive_limit = 2 后(上面的例子中,IN条件里有2个值),再看下新的查询计划:

mysql> set eq_range_index_dive_limit = 2;

mysql> explain select * from pre_forum_post where tid=7932612 and `invisible` in('0','-2') order by dateline  limit 15\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pre_forum_post
         type: range
possible_keys: PRIMARY,displayorder,first
          key: displayorder
      key_len: 4
          ref: NULL
         rows: 54
        Extra: Using index condition; Using filesort

卧槽,预估扫描记录数又降了557倍,相比最开始降了接近32万倍! 在这个案例中,虽然通过修改选项 eq_range_index_dive_limit 的阈值可以达到优化效果,但事实上更靠谱的做法是:直接删除 idx_dt 索引。 是的,没错,删除这个垃圾重复索引,因为实际上这个索引的用处不大,够坑爹吧~~

参考资料:
http://blog.163.com/li_hx/blog/static/18399141320147521735442/
http://mysqlserverteam.com/you-asked-for-it-new-default-for-eq_range_index_dive_limit/ https://www.facebook.com/note.php?note_id=10151533648715933
http://bugs.mysql.com/bug.php?id=70586
http://bugs.mysql.com/bug.php?id=67980
http://bugs.mysql.com/bug.php?id=70331

个人PPT分享

个人最近几年内整理过的PPT,都放在百度文库上了,大家可以看看 :)

M​y​S​Q​L​ ​t​p​c​h​测​试​工​具​简​要​手​册

高​效​L​i​n​u​x​ ​S​A​

P​C​服​务​器​阵​列​卡​管​理​简​易​手​册​

服​务​器​基​准​测​试

M​y​S​Q​L​数​据​库​设​计​、​优​化 

M​y​S​Q​L​之​设​计​、​优​化​、​运​维