日志文件记录了影响 MySQL数据库的各种类型活动。 MySQL数据库中常见的日志文件有:
- 错误日志(error log)
- 二进制日志(binlog)
- 慢查询日志(slow query log)
- 查询日志(log)
这些日志文件可以帮助DBA对 MySQL数据库的运行状态进行诊断,从而更好地进行数据库层面的优化。
1、错误日志
错误日志文件对 MySQL的启动、运行、关闭过程进行了记录。 MySQL DBA在遇到问题时应该首先查看该文件以便定位问题。该文件不仅记录了所有的错误信息,也记录一些警告信息或正确的信息。用户可以通过命令SHOW VARIABLES LIKE 'log_error'来定位该文件,如
mysql> SHOW VARIABLES LIKE 'log_error';
+---------------+----------------------------------------+
| Variable_name | Value |
+---------------+----------------------------------------+
| log_error | /usr/local/mysql/data/mysqld.local.err |
+---------------+----------------------------------------+
1 row in set (0.00 sec)
当出现MySQL数据库不能正常启动时,第一个必须查找的文件应该就是错误日志文件,该文件记录了错误信息,能很好地指导用户发现问题。当数据库不能重启时,通过查错误日志文件可以得到如下内容:
这里,错误日志文件提示了找不到权限库 mysql,所以启动失败。有时用户可以直接在错误日志文件中得到优化的帮助,因为有些警告(warning)很好地说明了问题所在。而这时可以不需要通过查看数据库状态来得知,例如,下面的错误文件中的信息可能告诉用户需要增大 InnoDB存储引擎的 redo log:
2、慢查询日志
可以通过错误日志得到一些关于数据库优化的信息,而慢查询日志(slow log)可帮助DBA定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。例如,可以在 MySQL启动时设一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志文件中。DBA每天或每过一段时间对其进行检查,确认是否有SQL语句需要进行优化。该阈值可以通过参数 long_query_time来设置,默认值为10,代表10秒。
在默认情况下, MySQL数据库并不启动慢查询日志,用户需要手工将这个参数设为ON:
mysql> SHOW VARIABLES LIKE 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'log_slow_queries';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| log_slow_queries| ON |
+-----------------+-----------+
mysql5.6版本以上,取消了参数log_slow_queries,更改为slow_query_log,还需要加上 slow_query_log = on 否则,还是没用。
mysql> SHOW VARIABLES LIKE 'slow_query_log';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | OFF |
+----------------+-------+
1 row in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'slow_query_log_file';
+---------------------+------------------------------------------------------+
| Variable_name | Value |
+---------------------+------------------------------------------------------+
| slow_query_log_file | /usr/local/mysql/data/my-computer-slow.log |
+---------------------+------------------------------------------------------+
这里有两点需要注意。首先,设置 long_query_time这个阈值后, MySQL数据库会记录运行时间超过该值的所有SQL语句,但运行时间正好等于 long_query_time的情况并不会被记录下。也就是说,在源代码中判断的是大于long_query_time,而非大于等于。其次,从 MySQL5.1开始, long_query_time开始以微秒记录SQL语句运行的时间,之前仅用秒为单位记录。而这样可以更精确地记录SQL的运行时间,供DBA分析。对DBA来说,一条SQL语句运行0.5秒和0.05秒是非常不同的,前者可能已经进行了表扫,后面可能是进行了索引。
另一个和慢查询日志有关的参数是 log_queries_not_using_indexes,如果运行的SQL语句没有使用索引,则 MySQL数据库同样会将这条SQL语句记录到慢查询日志文件。
首先确认打开了log_queries_not_using_indexes:
mysql> SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | ON |
+-------------------------------+-------+
1 row in set (0.00 sec)
My SQL5.6.5版本开始新增了一个参数 log_throttle_queries_not_using_indexes,用来表示每分钟允许记录到 slow log的且未使用索引的SQL语句次数。该值默认为0,表示没有限制。在生产环境下,若没有使用索引,此类SQL语句会频繁地被记录到slow log,从而导致 slow log文件的大小不断增加,故DBA可通过此参数进行配置。
mysql> SHOW VARIABLES LIKE 'log_throttle_queries_not_using_indexes';
+----------------------------------------+-------+
| Variable_name | Value |
+----------------------------------------+-------+
| log_throttle_queries_not_using_indexes | 0 |
+----------------------------------------+-------+
1 row in set (0.00 sec)
DBA可以通过慢查询日志来找出有问题的SQL语句,对其进行优化。然而随着MySQL数据库服务器运行时间的增加,可能会有越来越多的SQL查询被记录到了慢查询日志文件中,此时要分析该文件就显得不是那么简单和直观的了。而这时 MySQL数据库提供的 mysqldumpslow命令,可以很好地帮助DBA解决该问题。
如果用户希望得到执行时间最长的10条SQL语句,可以运行如下命令:
mysqldumpslow -s al -n l0 david.log
MySQL5.1开始可以将慢查询的日志记录放入一张表中,这使得用户的查询更加方便和直观。慢查询表在mysql架构下,名为 slow_log,其表结构定义如下:
mysql> show create table mysql.slow_log\G
*************************** 1. row ***************************
Table: slow_log
Create Table: CREATE TABLE `slow_log` (
`start_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`user_host` mediumtext NOT NULL,
`query_time` time(6) NOT NULL,
`lock_time` time(6) NOT NULL,
`rows_sent` int(11) NOT NULL,
`rows_examined` int(11) NOT NULL,
`db` varchar(512) NOT NULL,
`last_insert_id` int(11) NOT NULL,
`insert_id` int(11) NOT NULL,
`server_id` int(10) unsigned NOT NULL,
`sql_text` mediumblob NOT NULL,
`thread_id` bigint(21) unsigned NOT NULL
) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='Slow log'
1 row in set (0.00 sec)
参数 log_output指定了慢查询输出的格式,默认为FILE,可以将它设为 TABLE,然后就可以查询 mysql架构下的slow_log表了,如:
mysql> show variables like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set global log_output='TABLE';
Query OK, 0 rows affected (0.01 sec)
mysql> show variables like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | TABLE |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select sleep(11);
+-----------+
| sleep(11) |
+-----------+
| 0 |
+-----------+
1 row in set (10.02 sec)
mysql> select * from mysql.slow_log\G
*************************** 1. row ***************************
start_time: 2018-10-09 15:36:30.948543
user_host: root[root] @ localhost []
query_time: 00:00:11.001354
lock_time: 00:00:00.000000
rows_sent: 1
rows_examined: 0
db: information_schema
last_insert_id: 0
insert_id: 0
server_id: 0
sql_text: select sleep(11)
thread_id: 4
1 row in set (0.00 sec)
参数 log_output是动态的,并且是全局的,因此用户可以在线进行修改。在上表中人为设置了睡眠(sleep)10秒,那么这句SQL语句就会被记录到 slow_log表了。
查看 slow_log表的定义会发现该表使用的是CSV引擎,对大数据量下的查询效率可能不高。用户可以把 slow_log表的引擎转换到 MyISAM,并在 start_time列上添加索引以进一步提高査询的效率。但是,如果已经启动了慢查询,将会提示错误:
mysql>ALTER TABLE mysql.slow_log ENGINE=MyISM;
ERROR 1580 (HY000): You cannot 'ALTER a log table if logging is enabled
mysql>SET GLOBAL slow query log=off;
Query OK, 0 rows affected (0.00 sec)
mysql>ALTER TABLE mysql slow log ENGINE=MyISAM;
Query OK,1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
不能忽视的是,将 slow_log表的存储引擎更改为MyAM后,还是会对数据库造成额外的开销。不过好在很多关于慢查询的参数都是动态的,用户可以方便地在线进行设置或修改。
MySQL的 slow_log通过运行时间来对SQL语句进行捕获,这是一个非常有用的优化技巧。但是当数据库的容量较小时,可能因为数据库刚建立,此时非常大的可能是数据全部被缓存在缓冲池中,SQL语句运行的时间可能都是非常短的,一般都是0.5秒。
InnoSQL版本加强了对于SQL语句的捕获方式。在原版 MySQL的基础上在slow log中增加了对于逻辑读取(logical reads)和物理读取(physical reads)的统计。这里的物理读取是指从磁盘进行IO读取的次数,逻辑读取包含所有的读取,不管是磁盘还是缓冲池。例如
从上面的例子可以看到该子查询的逻辑读取次数是91584次,物理读取为19次。从逻辑读与物理读的比例上看,该SQL语句可进行优化。
用户可以通过额外的参数 long_query_io将超过指定逻辑IO次数的SQL语句记录到slow log中。该值默认为100,即表示对于逻辑读取次数大于100的SQL语句,记录到slow log中。而为了兼容原 MySQL数据库的运行方式,还添加了参数 slow_query_type,用来表示启用 slow log的方式,可选值为:
- 0表示不将SQL语句记录到 slow log
- 1表示根据运行时间将SQL语句记录到 slow log
- 2表示根据逻辑IO次数将SQL语句记录到 slow log
- 3表示根据运行时间及逻辑IO次数将SQL语句记录到 slow log
3、查询日志
查询日志记录了所有对 MySQL数据库请求的信息,无论这些请求是否得到了正确的执行。默认文件名为:主机名.log。如查看一个查询日志路径:
mysql> SHOW VARIABLES LIKE 'general_log%';
+------------------+-------------------------------------------------+
| Variable_name | Value |
+------------------+-------------------------------------------------+
| general_log | OFF |
| general_log_file | /usr/local/mysql/data/my-computer.log |
+------------------+-------------------------------------------------+
2 rows in set (0.00 sec)
查询日志。它记录所有MySQL活动,在诊断问题是非常有用。日志文件可能会很快的变得非常大,因此不应该长期使用它。此日志通常名为hostname.log。
同样地,从 MySQL5.1开始,可以将查询日志的记录放入mysq架构下的 general_log表中,该表的使用方法和前面小节提到的 slow log基本一样,这里不再赘述。
4、二进制日志
二进制日志(binary log)记录了对 MySQL数据库执行更改的所有操作,但是不包括 SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改。然而,若操作本身并没有导致数据库发生变化,那么该操作可能也会写入二进制日志。例如:
从上述例子中可以看到, MySQL数据库首先进行 UPDATE操作,从返回的结果看到 Changed为0,这意味着该操作并没有导致数据库的变化。但是通过命令SHOW BINLOG EVENT可以看出在二进制日志中的确进行了记录。
如果用户想记录 SELECT和SHOW操作,那只能使用查询日志,而不是二进制日志。此外,二进制日志还包括了执行数据库更改操作的时间等其他额外信息。总的来说,二进制日志主要有以下几种作用。
- 恢复(recovery):某些数据的恢复需要二进制日志,例如,在一个数据库全备文件恢复后,用户可以通过二进制日志进行 point-in-time的恢复。
- 复制(replication):其原理与恢复类似,通过复制和执行二进制日志使一台远程的 MySQL数据库(一般称为slave或 standby)与一台 MySQL数据库(一般称为 master或 primary)进行实时同步。
- 审计(audit):用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。
查看到binlog日志为OFF关闭状态:
mysql> show variables like 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | OFF |
+---------------+-------+
1 row in set (0.00 sec)
通过配置参数 log-bin[=name]可以启动二进制日志。
退出MySQL,使用vi编辑器修改MySQL的my.cnf配置文件。
server-id表示单个结点的id,这里由于只有一个结点,所以可以把id随机指定为一个数,这里将id设置成1。若集群中有多个结点,则id不能相同, 第二句是指定binlog日志文件的名字为mysql-bin,以及其存储路径。
此时再次进入MySQL,查看binlog日志的状态。显示binlog日志为ON开启状态。
如果不指定name则默认二进制日志文件名为主机名,后缀名为二进制日志的序列号,所在路径为数据库所在目录( datadir)。如:
这里的 bin_log.00001即为二进制日志文件,我们在配置文件中指定了名字,所以没有用默认的文件名。 bin_log.index为二进制的索引文件,用来存储过往产生的二进制日志序号,在通常情况下,不建议手工修改这个文件。
二进制日志文件在默认情况下并没有启动,需要手动指定参数来启动。可能有人会质疑,开启这个选项是否会对数据库整体性能有所影响。不错,开启这个选项的确会影响性能,但是性能的损失十分有限。根据 MySQL官方手册中的测试表明,开启二进制日志会使性能下降1%但考虑到可以使用复制( replication)和 point-in-time的恢复,这些性能损失绝对是可以且应该被接受的。
以下配置文件的参数影响着二进制日志记录的信息和行为:
- max_binlog_size
- binlog_cache_size
- sync_binlog
- binlog_do_db
- binlog_ignore_db
- log_slave_update
- binlog_format
参数 max_binlog_size指定了单个二进制日志文件的最大值,如果超过该值,则产生新的二进制日志文件,后缀名+1,并记录到 index文件。从 MySQL5.0开始的默认值为1073741824,代表1G(在之前版本中 max_binlog_size默认大小为1.1G)。
当使用事务的表存储引擎(如 InnodB存储引擎)时,所有未提交( uncommitted)的二进制日志会被记录到一个缓存中去,等该事务提交(committed)时直接将缓冲中的二进制日志写入二进制日志文件,而该缓冲的大小由 binlog_cache_size决定,默认大小为32K。此外, binlog_cache_size是基于会话( session)的,也就是说,当一个线程开始一个事务时,MyQL会自动分配一个大小为 binlog_cache_size的缓存,因此该值的设置需要相当小心,不能设置过大。当一个事务的记录大于设定的 binlog_cache_size时, MySQL会把缓冲中的日志写人一个临时文件中,因此该值又不能设得太小。通过SHOW GLOBAL STATUS命令查看 binlog_cache_use、 binlog_cache_disk_use的状态,可以判断当前 binlog_cache_size的设置是否合适。 binlog_cache_use记录了使用缓冲写进制日志的次数, binlog_cache_disk_use记录了使用临时文件写二进制日志的次数。现在来看一个数据库的状态:
使用缓冲次数为33553,临时文件使用次数为0。看来32KB的缓冲大小对于当前这个 MySQL数据库完全够用,暂时没有必要增加 binlog_cache_size的值。
在默认情况下,二进制日志并不是在每次写的时候同步到磁盘(用户可以理解为缓冲写)。因此,当数据库所在操作系统发生宕机时,可能会有最后一部分数据没有写入二进制日志文件中,这会给恢复和复制带来问题。参数 sync_binlog=[N]表示每写缓冲多少次就同步到磁盘。如果将N设为1,即 sync_binlog=1表示采用同步写磁盘的方式来写二进制日志,这时写操作不使用操作系统的缓冲来写二进制日志。 sync_binlog的默认值为0,如果使用 InnoDB存储引擎进行复制,并且想得到最大的高可用性,建议将该值设为ON。不过该值为ON时,确实会对数据库的IO系统带来一定的影响。
但是,即使将 sync_binlog设为1,还是会有一种情况导致问题的发生。当使用InnoDB存储引擎时,在一个事务发出 COMMIT动作之前,由于 sync_binlog为1,因此会将二进制日志立即写入磁盘。如果这时已经写人了二进制日志,但是提交还没有发生,并且此时发生了宕机,那么在 MySQL数据库下次启动时,由于 COMMIT操作并没有发生,这个事务会被回滚掉。但是二进制日志已经记录了该事务信息,不能被回滚。这个问题可以通过将参数 innodb_support_xa设为1来解决,虽然 innodb_support_xa与XA事务有关,但它同时也确保了二进制日志和 InnoDB存储引擎数据文件的同步。
参数 binlog-do-db和 binlog-ignore-db表示需要写入或忽略写入哪些库的日志。默认为空,表示需要同步所有库的日志到二进制日志。
如果当前数据库是复制中的 slave角色,则它不会将从 master取得并执行的二进制日志写入自己的二进制日志文件中去。如果需要写人,要设置log-slave-update。如果需要搭建 master=> slave=> slave架构的复制,则必须设置该参数。
binlog_format参数十分重要,它影响了记录二进制日志的格式。在 MySQL5.1版本之前,没有这个参数。所有二进制文件的格式都是基于SQL语句(statement)级别的,因此基于这个格式的二进制日志文件的复制( Replication)和 Oracle的逻辑 Standby有点相似。同时,对于复制是有一定要求的。如在主服务器运行rand、uuid等函数,又或者使用触发器等操作,这些都可能会导致主从服务器上表中数据的不一致(not sync)另一个影响是,会发现 InnoDB存储引擎的默认事务隔离级别是 REPEATABLE READ。
这其实也是因为二进制日志文件格式的关系,如果使用 READ COMMITTED的事务隔离级别(大多数数据库,如 Oracle, Microsoft SQL Server数据库的默认隔离级别),会出现类似丢失更新的现象,从而出现主从数据库上的数据不一致。
MySQL5.1开始引入了 binlog_format参数,该参数可设的值有 STATEMENT、ROW和 MIXED。
(1)STATEMENT格式和之前的 MySQL版本一样,二进制日志文件记录的是日志的逻辑SQL语句。
(2)在ROW格式下,二进制日志记录的不再是简单的SQL语句了,而是记录表的行更改情况。基于ROW格式的复制类似于 Oracle的物理 Standby(当然,还是有些区别)。同时,对上述提及的 Statement格式下复制的问题予以解决。从 MySQL5.1版本开始,如果设置了 binlog_format为ROW,可以将 InnoDB的事务隔离基本设为READ COMMITTED,以获得更好的并发性。
(3)在MXED格式下, MySQL默认采用 STATEMENT格式进行二进制日志文件的记录,但是在一些情况下会使用ROW格式,可能的情况有:
- 1)表的存储引擎为NDB,这时对表的DML操作都会以ROW格式记录。
- 2)使用了UUID()、 USER()、 CURRENT_USER()、 FOUND_ROWS()、 ROW_COUNT()
- 等不确定函数。
- 3)使用了 INSERT DELAY语句。
- 4)使用了用户定义函数(UDF)
- 5)使用了临时表(temporary table)
此外, binlog_format参数还有对于存储引擎的限制,如表所示。
存储引擎 | Row格式 | Statement格式 |
---|---|---|
存储引擎 | Row格式 | Statement格式 |
InnoDB | Yes | Yes |
MyISAM | Yes | Yes |
HEAP | Yes | Yes |
MERGE | Yes | Yes |
NDB | Yes | No |
Archive | Yes | Yes |
CSV | Yes | Yes |
Federate | Yes | Yes |
Blockhole | No | Yes |
binlog_format是动态参数,因此可以在数据库运行环境下进行更改,例如,我们可以将当前会话的 binlog_format设为ROW,如:
mysql> SET @@session.binlog_format='ROW';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@session.binlog_format;
+-------------------------+
| @@session.binlog_format |
+-------------------------+
| ROW |
+-------------------------+
1 row in set (0.00 sec)
当然,也可以将全局的 binlog_format设置为想要的格式,不过通常这个操作会带来问题,运行时要确保更改后不会对复制带来影响。如:
mysql> SET @@global.binlog_format='ROW';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@global.binlog_format;
+------------------------+
| @@global.binlog_format |
+------------------------+
| ROW |
+------------------------+
1 row in set (0.00 sec)
在通常情况下,我们将参数 binlog_format设置为ROW,这可以为数据库的恢复和复制带来更好的可靠性。但是不能忽略的一点是,这会带来二进制文件大小的增加,有些语句下的ROW格式可能需要更大的容量。比如我们有两张一样的表,大小都为100W,分别执行 UPDATE操作,观察二进制日志大小的变化:
可以看到,在 binlog format格式为 STATEMENT的情况下,执行 UPDATE语句后二进制日志大小只增加了200字节(306-106)。如果使用ROW格式,同样对t2表进行操作,可以看到:
这时会惊讶地发现,同样的操作在ROW格式下竟然需要13782094字节,二进制日志文件的大小差不多增加了13MB,要知道t2表的大小也不超过17MB。而且执行时间也有所增加(这里我设置了sync_binlog=1)。这是因为这时 MySQL数据库不再将逻辑的SQL操作记录到二进制日志中,而是记录对于每行的更改。
上面的这个例子告诉我们,将参数 binlog_format设置为ROW,会对磁盘空间要求有一定的增加。而由于复制是采用传输二进制日志方式实现的,因此复制的网络开销也有所增加。
进制日志文件的文件格式为二进制(好像有点废话),不能像错误日志文件、慢查询日志文件那样用cat、head、tail等命令来查看。要查看二进制日志文件的内容,必须通过 MySQL提供的工具 mysqlbinlog。对于 STATEMENT格式的二进制日志文件,在使用 mysqlbinlog后,看到的就是执行的逻辑SQL语句,如:
通过SQL语句 UPDATE t2 SET username= UPPER( username) WHERE id=1可以看到,二进制日志的记录采用SQL语句的方式。在这种情况下, mysqlbinlog和 Oracle LogMiner类似。但是如果这时使用ROW格式的记录方式,会发现 mysqlbinlog的结果变得“不可读”( unreadable),如:
这里看不到执行的SQL语句,反而是一大串用户不可读的字符。其实只要加上参数或-vv就能清楚地看到执行的具体信息了。-vv会比-v多显示出更新的类型。加上-vv选项,可以得到:
现在 mysqlbinlog向我们解释了它具体做的事情。可以看到,一句简单的 update t2 set username= upper( username) where id=1语句记录了对于整个行更改的信息,这也解释了为什么前面更新了10W行的数据,在ROW格式下,二进制日志文件会增大13MB。