MySQL 5.6.23 InnoDB相关Bugfix


本节摘取了MySQL5.6.23的几个和InnoDB相关的主要bugfix,简单阐述下问题及解决方案。


问题一

当执行FLUSH TABLE..FOR EXPORT命令时,会暂停purge线程的操作。这一步通过设置一个标记purge_sys->state的值为PURGE_STATE_STOP来告诉purge线程该停下来歇歇了。

然而如果Purge线程当前正在函数srv_do_purge中工作,该函数会执行一个while循环,退出条件是当前server shutdown,或者上次purge的page数为0,并没有检查purge线程的状态是否被设置为PURGE_STATE_STOP; 很显然,如果当前的history list非常长,那么可能需要等待purge完成后,才能退出循环,而在用户看来,就好像hang了很久一样。推长history list 很容易:开启一个打开read view的事务(例如RR级别下执行一个SELECT)不做提交,同时有并发的DML,跑一段时间history list就上去了。


解决

在函数srv_do_purge函数的while退出条件中加上purge线程状态判断,如果被设置为PURGE_STATE_STOP,就退出循环。

补丁


问题二

在执行InnoDB crash recovery阶段,如果发现不合法的大字段,就会去调用函数ib_warn_row_too_big 去打印一条warning,函数为push_warning_printf。然而这个函数的目的是给客户端返回一条warning,而这时候系统还在崩溃恢复阶段,并没有合法的thd对象,因此造成系统崩溃。

Tips:这个bug是在升级到新版本5.6出现的,最根本的原因是5.6新版本对大字段长度做的约束。早期版本5.6及之前的版本,我们可以定义非常大的blob字段,但如果字段太长,对这些字段的修改,可能导致redo log的checkpoint点被覆盖,因为计算redo log 空间是否足够,并没有依赖即将插入的redo 记录长度,而仅仅是保留一定的比例。因此在5.6.22版本中做了限制:如果blob的长度超过innodb_log_file_size * innodb_log_files_in_group的十分之一时,就会更新失败,给用户返回DB_TOO_BIG_RECORD的错误码。这个问题在5.7版本里被彻底解决:每写4个blob外部存储页,检查一次redo log空间是否足够,如果不够用,就推进checkpoint点。

解决

在函数ib_warn_row_too_big中判断当前线程thd是否被初始化,如果为NULL,直接返回,不调用push_warning_printf。

补丁


问题三

当我们通过alter语句修改一个被外键约束的列名时,由于没有从数据词典cache中将包含老列名的cache项驱逐掉,导致重载外键约束时失败。

举个简单的例子:

root@sb1 12:37:13>CREATE TABLE t1 (a INT NOT NULL,  b INT NOT NULL,  INDEX idx(a)) ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)
root@sb1 12:37:26>CREATE TABLE t2 (a INT KEY,  b INT,  INDEX ind(b),  FOREIGN KEY (b) REFERENCES t1(a) ON DELETE CASCADE ON UPDATE CASCADE)  ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)
root@sb1 12:37:41>ALTER TABLE t1 CHANGE a id INT;
Query OK, 0 rows affected, 1 warning (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 1
root@sb1 12:37:48>show warnings;
+-------+------+-----------------------------------+
| Level | Code | Message                           |
+-------+------+-----------------------------------+
| Error | 1215 | Cannot add foreign key constraint |
+-------+------+-----------------------------------+
1 row in set (0.00 sec)
root@sb1 12:47:39>show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id` int(11) DEFAULT NULL,
  `b` int(11) NOT NULL,
  KEY `idx` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
root@sb1 12:52:08>INSERT INTO t2 VALUES (56, 6);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`sb1`.`t2`, CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`b`) REFERENCES `t1` (`a`) ON DELETE CASCADE ON UPDATE CASCADE)

可以看到,尽管t1表的a列已经被rename成 id,但打印出来的信息也并没有更正。

解决

当被外键约束的列名被修改时,将对应的外键项从数据词典cache中驱逐,当其被随后重新加载时就会使用新的对象。

补丁


问题四

如上文所提到的,在新版本InnoDB中,对blob字段的数据操作需要保证其不超过总的redo log file大小的十分之一,但是返回的错误码DB_TOO_BIG_RECORD及打印的信息太容易让人误解,大概如下:

ERROR 42000: Row size too large (> ####). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline

解决

输出更合适、更直观的错误信息,如下:

ERROR 42000: The size of BLOB/TEXT data inserted in one transaction is greater than 10% of redo log size. Increase the redo log size using innodb_log_file_size.

补丁


问题五

FLUSH TABLE操作在某些情况下可能导致实例crash。 例如如下执行序列:

mysql> CREATE TABLE t1(C TEXT CHARACTER SET UJIS) ENGINE=INNODB;
Query OK, 0 rows affected (0.00 sec)
 
mysql> LOCK TABLES t1 WRITE,t1 AS t0 READ,t1 AS t2 READ;
Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH TABLE t1;   ---- 实例CRASH

当执行FLUSH TABLE时,在重载表cache时,InnoDB层会针对每个表设置其状态(ha_innobase::store_lock)。如果执行FLUSH 操作,并且加的是读锁时,就会调用函数row_quiesce_set_state将table->quiesce设置为QUIESCE_START。在上例中,表t1的两个表名表均加读锁,造成重复设置状态为QUIESCE_START,导致断言失败。

Tips:在5.6版本中,虽然有明确的FLUSH TABLE..FOR EXPORT命令来协助转储ibd文件。但实际上,简单的FLUSH TABLE操作默认就会产生一个tbname.cfg的配置文件,拷贝该文件和ibd,可以将数据转移到其他实例上。table->quiesce用于标识操作状态,例如,如果标识为QUIESCE_START,就会在函数ha_innobase::external_lock中调用row_quiesce_table_start来启动配置文件的生成。

解决

移除断言

补丁


问题六

线上实例错误日志中偶尔出现 “UNABLE TO PURGE A RECORD”,从官方bug系统来看,很多用户都遇到了类似的问题。

当change buffer模块以如下序列来缓存索引操作时可能产生上述错误信息:

  1. 记录被标记删除(IBUF_OP_DELETE_MARK)
  2. 随后插入相同记录--IBUF_OP_INSERT
  3. Purge线程需要物理删除二级索引记录,操作被buffer--IBUF_OP_DELETE

当读入物理页时,总是需要进行ibuf merge。如果执行到IBUF_OP_DELETE这种类型的change buffer时,发现记录并没有被标记删除,就会导致错误日志报错。

显然上述的操作序列是不合理的,正确的序列应该是IBUF_OP_DELETE_MARK,IBUF_OP_DELETE,IBUF_OP_INSERT。

为了搞清楚逻辑,我们简单的理一下相关代码。

注意IBUF_OP_DELETE是由第一步的标记删除操作触发,Purge线程发起;在每个buffer pool的控制结构体中,有一个成员buf_pool->watch[BUF_POOL_WATCH_SIZE],BUF_POOL_WATCH_SIZE的值为purge线程个数,用于辅助Purge操作。

假定内存中没有对应的Page,Purge线程会做如下几件事儿:

  • 首先查询buffer pool,看看page是否已经读入内存;如果不在内存中,则将page no等信息存储到watch数组中,并插入page hash(buf_pool_watch_set)。(如果随后page被读入内存,也会删除watch标记)
  • 判断该二级索引记录是否可以被Purge(row_purge_poss_sec,当该二级索引记录对应的聚集索引记录没有delete mark并且其trx id比当前的purge view还旧时,不可以做Purge操作)
  • 随后在插入IBUF_OP_DELETE类型的ibuf记录时,还会double check下该page是否被设为sentinel (ibuf_insert_low,buf_pool_watch_occurred),如果未被设置,表明已经page已经读入内存,就可以直接去做purge,而无需缓存了。
  • 对于普通的操作类型,例如IBUF_OP_INSERT和IBUF_OP_DELETE_MARK,同样也会double check page 是否读入了内存。在函数ibuf_insert中会调用buf_page_hash_get进行检查,如果page被读入内存,则不缓存操作,如果请求的Page被设为sentinel,则从buf_page_hash_get返回NULL,因此随后判定需要缓存该类型的操作。这也正是问题的所在:
  1. 标记删除记录,写入IBUF_OP_DELETE_MARK
  2. Purge线程设置page对应的sentinel,完成合法性检查,准备调用ibuf_insert
  3. 插入相同记录,写入IBUF_OP_INSERT
  4. Purge线程写入IBUF_OP_DELETE

解决

如果记录所在的page被设置了一个sentinel,那么对该page的并发插入操作就不应该缓存到change buffer中,而是直接去尝试读取物理页。

补丁


问题七

对于非windows系统的平台上,函数os_file_pwrite和os_file_pread在碰到io错误时返回-1,并错误的作为写入/读取的字节数写在错误日志中。

解决

单独记录失败的系统调用日志,打印更可读的日志信息。

补丁


问题八

在崩溃恢复后立刻执行一次slow shutdown (innodb_fast_shutdown = 0) 可能产生断言失败crash。原因是当完成crash recovery后,对于需要回滚的事务,会起单独的线程来执行,这时候如果shutdown实例,会触发触发purge线程内部断言失败:ut_a(n_pages_purged == 0 || srv_fast_shutdown != 0);

解决

等待trx_rollback_or_clean_all_recovered完成后��再进行slow shutdown

补丁

PgSQL · 特性分析· Replication Slot

PostgreSQL 9.4 已于2014年底正式发布了(阿里云的RDS将支持PG 9.4)。在这个版本,我们看到了像Jsonb, Logical Decoding, Replication Slot等新功能。对于Replication Slot,文档上介绍的不多,乍一看让人比较难理解是做什么的。其实,Replication Slot的出现,主要是为最终在PG内核实现逻辑复制和双向复制铺路的(目前,逻辑和双向复制在内核中还缺少很多核心功能点,需要借助BDR插件,见PG官方wiki ,引入Replication Slot的背后原因见这里)。不过,作为9.4版本的一个主要功能,它不但可以用于逻辑复制,还可用于物理复制(或者叫Streaming Replication)。针对物理复制的Replication Slot称为Physical Replication Slot。

由于大家目前主要用的还只是PG自带的物理复制方式,我们就重点分析一下Physical Replication Slot。

使用Physical Replication Slot,可以达到两个效果:

A)可以确保从库(standby)需要的日志不被过早备份出去而导致从库失败,出现下面的error:

ERROR:  requested WAL segment 00000001000000010000002D has already been removed

通过Replication Slot记录的从库状态,PG会保证从库还没有apply的日志,不会从主库的日志目录里面清除或archive掉。而且,replication slot的状态信息是持久化保存的,即便从库断掉或主库重启,这些信息仍然不会丢掉或失效。

B)当允许应用连接从库做只读查询时,Replication Slot可以与运行时参数hot_standby_feedback配合使用,使主库的vacuum操作不会过早的清掉从库查询需要的记录,而出现如下错误(错误的原因下面会详细解释):

ERROR:  canceling statement due to conflict with recovery


下面看看Physical Replication Slot的用法和内核实现。


用法

下面是启用Replication Slot的步骤,很简单:

1)首先需要配置好Steaming Replication的主库从库。涉及的参数有,listen_addresses(='*'),hot_standby(=on), wal_level(=hot_standby),max_wal_senders(=1),尤其注意配置max_replication_slots大于等于1。这些参数主从库应一致。

2)在主库创建replication slot:

postgres=# SELECT * FROM pg_create_physical_replication_slot('my_rep_slot_1');
  slot_name   | xlog_position
---------------+---------------
my_rep_slot_1 |

此时replication slot还不处于active状态。

3) 在从库配置recovery.conf如下,然后重启从库:

standby_mode = 'on'
primary_slot_name = 'my_rep_slot_1'
primary_conninfo = 'user=pg001 host=10.x.x.x port=5432 sslmode=prefer sslcompression=1 krbsrvname=postgres'

4)观察主库replication slot的状态变化:

postgres=# SELECT * FROM pg_replication_slots ;
  slot_name   | plugin | slot_type | datoid | database | active | xmin | catalog_xmin | restart_lsn
---------------+--------+-----------+--------+----------+--------+------+--------------+-------------
my_rep_slot_1 |        | physical  |        |          | t      | 1812 |              | 0/3011A70

5)与hot_standby_feedback配合使用。在将从库的postgresql.conf文件中的hot_standby_feedback选项设为on,重启从库即可。


内核实现

replication slot是由下面的patch加入内核中的:

author	Robert Haas <rhaas@postgresql.org>	
Sat, 1 Feb 2014 03:45:17 +0000 (22:45 -0500)
committer	Robert Haas <rhaas@postgresql.org>	
Sat, 1 Feb 2014 03:45:36 +0000 (22:45 -0500)
Replication slots are a crash-safe data structure which can be created
on either a master or a standby to prevent premature removal of
write-ahead log segments needed by a standby, as well as (with
hot_standby_feedback=on) pruning of tuples whose removal would cause
replication conflicts.  Slots have some advantages over existing
techniques, as explained in the documentation.

这个patch改的文件不少,分析这些代码,我们重点关注下面的问题:

A)Replication Slot是如何在内核中创建的?

通过分析创建Replication Slot时调用的函数ReplicationSlotCreate,可以看出,Replication Slot实质上是内存中的一些数据结构,加上持久化保存到pg_replslot/<slot name>目录中的二进制状态文件。在PG启动的时候,预先在共享内存中分配好这些数据结构所用内存(即一个大小为max_replication_slots的数组)。这些数据结构在用户创建Replication Slot时开始被使用。一个Replication Slot被创建并使用后,其数据结构和状态文件会被WAL(Write-Ahead-Log)的发送者(wal_sender)进程更新。

另外,如果单纯从Replication Slot的名字,我们很容易觉得Replication Slot会创建新的与从库的连接,进行日志发送。实际上,创建过程中并没有创建新的与从库的连接,Replication Slot还是使用了wal_sender原有连接(由于一个从库一个wal_sender连接,所以一个从库和主库之间也只有一个active的Replication Slot)。

B) Replication Slot的状态是如何被更新的?

很容易发现,Replication Slot的状态的更新有两种情况。

第一种是在ProcessStandbyHSFeedbackMessage这个函数被更新。这个函数是在处理wal_sender所收到的从库发回的feedback reply message时调用的。通过这个函数,我们可以看出,每个wal_sender进程的Replication Slot(就是用户创建的Replication Slot)保存在MyReplicationSlot这个全局变量中。在处理从库发回的reply时,reply中的xmin信息会被提取出来,存入slot的data.xmin和effective_xmin域中,并通过函数ProcArraySetReplicationSlotXmin,最终更新到系统全局的procArray->replication_slot_xmin结构中(以使其对所有进程可见),完成slot的更新。

这里要注意,如果我们有多个Replication Slot(分别对应各自从库),则在更新全局结构procArray->replication_slot_xmin时,会选取所有slot中最小的xmin值。

第二种是在ProcessStandbyReplyMessage中。这个函数处理从库发送的restart lsn信息(即从库apply的日志的编号),会直接将其更新到replication slot的restart lsn域中,并保存到磁盘,用于主库判断是否要保留日志不被archive。

C) Replication Slot如何和hot_standby_feedback配合,来避免从库的查询冲突的?

这里,从库的查询冲突指的是下面的情况:从库上有正在运行的查询,而且运行时间很长;这时主库上在做正常的vaccum,清除掉无用的记录版本。但主库的vaccum是不知道从库的查询存在的,所以在清除时,不考虑从库的正在运行的查询,只考虑主库里面的事务状态。其结果,vacuum可能会清除掉从库查询中涉及的,仍然在使用的记录版本。当这些vaccum操作,通过日志同步到从库,而恰好从库的查询仍然没有运行完,vaccum就要等待或cancel这个查询,以保证同步正常继续和查询不出现错误的结果。这样,每当用户在从库运行长查询,就容易出现我们上面提到到query conflict error。

如何避免这种冲突呢?目前最好的解决方案是使用hot_standby_feedback + Replication Slot。其原理简单说就是,从库将它的查询所依赖的记录版本的信息,以一个事务id来表示,并放在从库发回给主库wal_sender的reply中发给主库(见函数XLogWalRcvSendHSFeedback),并最终传导给主库vaccum,让其刀下留人,暂时不清除相关记录。

具体过程是,在从库,函数XLogWalRcvSendHSFeedback调用GetOldestXmin获得xmin,放入给主库的reply中。主库的wal_sender收到后,如果使用了Replication Slot,就把这个xmin放入slot的状态信息中,并更新此时系统所有slot的最小xmin。这个系统所有slot的最小xmin怎么在主库传导给vacuum的呢?以自动触发的vacuum操作为例,其中的逻辑的顺序如下:

GetSnapshotData(vacuum事务开始时,获取slot xmin,存入全局变量) ->vacuum_set_xid_limits(调用 GetOldestXmin,通过全局变量,获取系统xmin和slot xmin,取较小值)-> vacuum_lazy (使用xmin,判断哪些记录版本可以清除)

这样,利用Replication Slot这个渠道,就解决了从库查询冲突。

注意事项

最后,介绍一下使用Replication Slot的注意事项:

1)如果收不到从库的reply,Replication Slot的状态restart lsn会保持不变,造成主库会一直保留本地日志,可能导致日志磁盘满。所以应该实时监控日志磁盘使用情况,并设置较小的wal_sender_timeout,及早发现从库断掉的情况。

2)将hot_standby_feedback设为on时,注意如果从库长时间有慢查询发生,可能导致发回到主库的xmin变化较慢,主库的vaccum操作停滞,造成主库被频繁更新的表大小暴增。

除了物理复制,Replication Slot对逻辑复制的意义更大,我们期待在可能出现逻辑复制功能的9.5版本中看到它大显身手。

 

PgSQL · 特性分析· pg_prewarm

PostgreSQL内核中引入了一个很有意思的插件,pg_prewarm。它可以用于在系统重启时,手动加载经常访问的表到操作系统的cache或PG的shared buffer,从而减少检查系统重启对应用的影响。这个插件是这个通过这个patch加入PG内核的。 pg_prewarm的开发者在设计pg_prewarm时,把它设计成一个执行单一任务的工具,尽求简单,所以我们看到的pg_prearm功能和实现都非常简单。下面我们对它进行性能实测并分析一下它的实现。


基本信息

利用下面的语句可以创建此插件:

create EXTENSION pg_prewarm;

实际上,创建插件的过程只是用下面的语句创建了pg_prewarm函数。这个函数是此插件提供的唯一函数:

CREATE FUNCTION pg_prewarm(regclass,
        mode text default 'buffer',
        fork text default 'main',
        first_block int8 default null,
        last_block int8 default null)
RETURNS int8
AS 'MODULE_PATHNAME', 'pg_prewarm'
LANGUAGE C
    

函数的第一个参数是要做prewarm的表名,第二个参数是prewarm的模式(prefetch模式表示异步预取到操作系统cache;read表示同步预取;buffer则表示同步读入到PG的shared buffer),第三个参数是relation fork的类型(一般用main,其他类型有visibilitymap和fsm,参见[1][2]),最后两个参数是开始和结束的block number(一个表的block number从0开始,block总数可以通过pg_class系统表的relpages字段获得)。


性能实测

再来看看,这个prewarm性能上能达到多大效果。我们先将PG的shared buffer设为2G,OS总的memory有7G。然后创建下面的大小近1G的表test:

pgbench=# \d test
       Table "public.test"
Column |     Type      | Modifiers
--------+---------------+-----------
 name   | character(20) |
pgbench=#  SELECT pg_size_pretty(pg_total_relation_size('test'));
 pg_size_pretty
----------------
 995 MB

在每次都清掉操作系统cache和PG的shared buffer的情况下,分别测试下面几种场景:

1)不进行pg_prewarm的情况:

pgbench=# explain analyze select count(*) from test;
                                                       QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Aggregate  (cost=377389.91..377389.92 rows=1 width=0) (actual time=22270.304..22270.304 rows=1 loops=1)
  ->  Seq Scan on test  (cost=0.00..327389.73 rows=20000073 width=0) (actual time=0.699..18287.199 rows=20000002 loops=1)
Planning time: 0.134 ms
Execution time: 22270.383 ms

可以看到,近1G的表,全表扫描一遍,耗时22秒多。

2)下面我们先做read这种模式的prewarm,test表的数据被同步读入操作系统cache(pg_prewarm返回的是处理的block数目,此处我们没指定block number,也就是读入test的所有block),然后再做全表扫:

pgbench=# select pg_prewarm('test', 'read', 'main');
pg_prewarm
------------
    127389
pgbench=# explain analyze select count(*) from test;
                                                       QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate  (cost=377389.90..377389.91 rows=1 width=0) (actual time=8577.767..8577.767 rows=1 loops=1)
  ->  Seq Scan on test  (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.086..4716.444 rows=20000002 loops=1)
Planning time: 0.049 ms
Execution time: 8577.831 ms

时间降至8秒多!这时反复执行全表扫描,时间稳定在8秒多。

3)再尝试buffer模式:

pgbench=# select pg_prewarm('test', 'buffer', 'main');
pg_prewarm
------------
    127389
pgbench=# explain analyze select count(*) from test;
                                                       QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate  (cost=377389.90..377389.91 rows=1 width=0) (actual time=8214.277..8214.277 rows=1 loops=1)
  ->  Seq Scan on test  (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.015..4250.300 rows=20000002 loops=1)
Planning time: 0.049 ms
Execution time: 8214.340 ms

比read模式时间略少,但相差不大。可见,如果操作系统的cache够大,数据取到OS cache还是shared buffer对执行时间影响不大(在不考虑其他应用影响PG的情况下)。

4)最后尝试prefetch模式,即异步预取。这里,我们有意在pg_prewarm返回后,立即执行全表查询。这样在执行全表查询时,可能之前的预取还没完成,从而使全表查询和预取并发进行,缩短了总的响应时间:

explain analyze select pg_prewarm('test', 'prefetch', 'main');
                                       QUERY PLAN
------------------------------------------------------------------------------------------
Result  (cost=0.00..0.01 rows=1 width=0) (actual time=1011.338..1011.339 rows=1 loops=1)
Planning time: 0.124 ms
Execution time: 1011.402 ms
explain analyze select count(*) from test;
                                                       QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Aggregate  (cost=377389.90..377389.91 rows=1 width=0) (actual time=8420.652..8420.652 rows=1 loops=1)
  ->  Seq Scan on test  (cost=0.00..327389.72 rows=20000072 width=0) (actual time=0.065..4583.200 rows=20000002 loops=1)
Planning time: 0.344 ms
Execution time: 8420.723 ms

可以看到,总的完成时间是9秒多,使用pg_prewarm做预取大大缩短了总时间。因此在进行全表扫描前,做一次异步的prewarm,不失为一种优化全表查询的方法。


实现

pg_prewarm的代码只有一个pg_prewarm.c文件。可以看出,prefetch模式下,对于表的每个block,调用一次PrefetchBuffer,后面的调用为:

PrefetchBuffer -> smgrprefetch  -> mdprefetch -> FilePrefetch -> posix_fadvise(POSIX_FADV_WILLNEED)

可见,它是最终调用posix_fadvise,把读请求交给操作系统,然后返回,实现的异步读取。

而在read和buffer模式(调用逻辑分别如下)中,最终都调用了系统调用read,来实现同步读入OS cache和shared buffer的(注意buffer模式实际上是先读入OS cache,再拷贝到shared buffer):

read模式:smgrread -> mdread -> FileRead  -> read
buffer模式:ReadBufferExtended -> ReadBuffer_common -> smgrread -> mdread -> FileRead -> read


问题

可能有人比较疑惑:执行1次select * from 不就可以将表的数据读入shared buffer和OS cache而实现预热了吗?岂不是比做这样一个插件更简单?实际上,对于较大的表(大小超过shared buff的1/4),进行全表扫描时,PG认为没必要为这种操作使用所有shared buffer,只会让其使用很少的一部分buffer,一般只有几百K,详细描述可以参见关于BAS_BULKREAD策略的代码和README)。所以,预热大表是不能用一个查询直接实现的,而pg_prewarm正是在这方面大大方便了用户。

MySQL · 答疑释惑· InnoDB丢失自增值

背景

在上一期的月报中,我们在InnoDB自增列重复值问题 中提到,InnoDB 自增列在重启后会丢失,因为MySQL没有持久化自增值,平时是存在内存表对象中的。如果实例重启的话,内存值丢失,其初始化过程是做了一个类似 select max(id) + 1 操作。实际上存在另外一种场景,实例即使不重启,也会导致自增值丢失。

问题说明

实例运行过种中,InnoDB表自增值是存储在表对象中的,表对象又是放在缓存中的,如果表太多而不能全部放在缓存中的话,老的表就会被置换出来,这种被置换出来的表下次再使用的时候,就要重新打开一遍,对自增列来说,这个过程就和实例重启类似,需要 select max(id) + 1 算一下自增值。

对InnoDB来说,其数据字典中表对象缓存大小由 table_definition_cache 系统变量控制,在5.6.8之后,其最小值是400。和表缓存相关的另一个系统变量是table_open_cache,这个控制的是所有线程打开表的缓存大小,这个缓存放在server层。

下面我们用testcase的方式来给出InnoDB表对象对置换出的场景:

 ##把 table_definition_cache 和 table_open_cache 都设为400
 SET GLOBAL table_definition_cache = 400;
 SET GLOBAL table_open_cache = 400;
 
 ## 创建500个InnoDB自增表,各插入一条数据,然后把自增改为100
 let $i=0;
 while($i < 500)
 {
   --eval CREATE TABLE t$i(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(30), PRIMARY KEY(id)) ENGINE=InnoDB;
   --eval INSERT INTO t$i(name) VALUES("InnoDB");
   --eval ALTER TABLE t$i AUTO_INCREMENT = 100;
   --inc $i
 }
 
 ## 最后400张表扫一遍
 let $i=100;
 while($i < 500)
 {
   --eval SELECT * FROM t$i;
   --inc $i
 }
 
 ## 稍微sleep下,等mysqld把不用的表(t0..t99)换出
 sleep 5;
 
 ## 查看t1表自增
 SHOW CREATE TABLE t1;
 
 Table   Create Table
 t1      CREATE TABLE `t1` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(30) DEFAULT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
 ...

可以看到自增值确实和重启场景一样,本应是100,却变成了 2(select max(id) + 1)了。


问题分析

原因就是缓存不够,导致表对象被换出,下次再用就要重新打开,这里给出调用栈,对代码感兴趣的同学可以看下。

将老的table置换出:

 #0  dict_table_remove_from_cache_low (table=0x2b81d054e278, lru_evict=1)
     at /path/to/mysql/storage/innobase/dict/dict0dict.cc:1804
 #1  0x00000000011cf246 in dict_make_room_in_cache (max_tables=400, pct_check=100)
     at /path/to/mysql/storage/innobase/dict/dict0dict.cc:1261
 #2  0x0000000001083564 in srv_master_evict_from_table_cache (pct_check=100)
     at /path/to/mysql/storage/innobase/srv/srv0srv.cc:2017
 #3  0x0000000001084022 in srv_master_do_idle_tasks () at /path/to/mysql/storage/innobase/srv/srv0srv.cc:2212
 #4  0x000000000108484a in srv_master_thread (arg=0x0) at /path/to/mysql/storage/innobase/srv/srv0srv.cc:2360
 #5  0x00000030cc007851 in start_thread () from /lib64/libpthread.so.0
 #6  0x00000030cbce767d in clone () from /lib64/libc.so.6

尝试从缓存加载表对象:

 #0  dict_table_check_if_in_cache_low (table_name=0x2adef847db20 "test/t1")
     at /path/to/mysql/storage/innobase/include/dict0priv.ic:114
 #1  0x00000000011cd51a in dict_table_open_on_name (table_name=0x2adef847db20 "test/t1", dict_locked=0, try_drop=1,
     ignore_err=DICT_ERR_IGNORE_NONE) at /path/to/mysql/storage/innobase/dict/dict0dict.cc:947
 #2  0x0000000000e58d8a in ha_innobase::open (this=0x2adef9747010, name=0x2adef7460780 "./test/t1", mode=2, test_if_locked=2)
     at /path/to/mysql/storage/innobase/handler/ha_innodb.cc:4776
 #3  0x000000000068668b in handler::ha_open (this=0x2adef9747010, table_arg=0x2adef742bc00, name=0x2adef7460780 "./test/t1", mode=2,
     test_if_locked=2) at /path/to/mysql/sql/handler.cc:2525
 ...
 #9  0x00000000009c2a84 in mysqld_show_create (thd=0x2adef47aa000, table_list=0x2adef74200f0)
     at /path/to/mysql/sql/sql_show.cc:867
 #10 0x00000000009553b1 in mysql_execute_command (thd=0x2adef47aa000) at /path/to/mysql/sql/sql_parse.cc:3507
 #11 0x0000000000963bbe in mysql_parse (thd=0x2adef47aa000, rawbuf=0x2adef7420010 "show create table t1", length=20,
     parser_state=0x2adef8480630) at /path/to/mysql/sql/sql_parse.cc:6623
 ...

缓存加载不到表对象,用select maxt 逻辑初始化自增:

 #0  row_search_max_autoinc (index=0x2b241d8f50f8, col_name=0x2b241d855519 "id", value=0x2b241e87d8a8)
     at /path/to/mysql/storage/innobase/row/row0sel.cc:5361
 #1  0x0000000000e58998 in ha_innobase::innobase_initialize_autoinc (this=0x2b241fbd9010)
     at /path/to/mysql/storage/innobase/handler/ha_innodb.cc:4663
 #2  0x0000000000e59bd9 in ha_innobase::open (this=0x2b241fbd9010, name=0x2b241d853780 "./test/t1", mode=2, test_if_locked=2)
     at /path/to/mysql/storage/innobase/handler/ha_innodb.cc:5089
 #3  0x000000000068668b in handler::ha_open (this=0x2b241fbd9010, table_arg=0x2b241e422000, name=0x2b241d853780 "./test/t1", mode=2,
     test_if_locked=2) at /path/to/mysql/sql/handler.cc:2525
 ...
 #9  0x00000000009c2a84 in mysqld_show_create (thd=0x2b241abaa000, table_list=0x2b241d8200f0)
     at /path/to/mysql/sql/sql_show.cc:867
 #10 0x00000000009553b1 in mysql_execute_command (thd=0x2b241abaa000) at /path/to/mysql/sql/sql_parse.cc:3507
 #11 0x0000000000963bbe in mysql_parse (thd=0x2b241abaa000, rawbuf=0x2b241d820010 "show create table t1", length=20,
     parser_state=0x2b241e880630) at /path/to/mysql/sql/sql_parse.cc:6623
 ...

处理建议

对于这个问题,一种解决方法是从源码改进,将自增值持久化,可以参考上期的月报给出的思路;如果不想改代码的话,可以这样绕过:在设定auto_increment值后,主动插入一行记录,这样不论在重启还是缓存淘汰的情况下,重新打开表仍能得到预期的值。

 

MySQL · 答疑释惑· 5.5 和 5.6 时间类型兼容问题

问题描述

5.6.4及以上版本,datetime,time,timestamp的Binlog在5.6.4以下的备库无法执行,如:

5.6.16(主库): create table t1(t datetime default now()); insert into t1 values(now());

5.5.18(备库): show slave stauts\G ;

此时备库中断,报错:Last_Errno: 1677,

描述信息:Last_Error: Column 1 of table t1.t' cannot be converted from type '<unknown type>' to type 'datetime'

详情见Bug#70085

问题原因

1) 5.5版本存储的是datetime,time,timestamp这三种数据类型的长整型的数据,insert时的BT为:

#0 TIME_to_ulonglong_datetime (my_time=0x2ad2c82e84c0) at /u01/workplace/Percona-Server-5.5.18/sql-common/my_time.c:1187
#1 0x0000000000680b6d in Field_datetime::store (this=0x2ad2d000fb10, from=0x2ad2d0014fe0 "2014-02-25 11:20:42", len=19, cs=<value optimized out>) 
#2 0x00000000005488a4 in fill_record (thd=0xa602190, ptr=<value optimized out>, values=<value optimized out>, ignore_errors=<value optimized out>, triggers=0x0, event) 
#3 fill_record_n_invoke_before_triggers (thd=0xa602190, ptr=<value optimized out>, values=<value optimized out>, ignore_errors=<value optimized out>, triggers=0x0, event)

2) 5.6.16的相应堆栈为:

#0 my_datetime_packed_to_binary (nr=1842590951223066624, ptr=0x7fa88005dea1 "\231\222\062\265*", dec=0)
#1 0x00000000009155d4 in Field_datetimef::store_packed (this=0x7fa88005dec0, nr=1842590951223066624)
#2 0x000000000091553a in Field_datetimef::store_internal (this=0x7fa88005dec0, ltime=0x7fa8d42018f0, warnings=0x7fa8d4201920)
#3 0x000000000091191a in Field_temporal_with_date::store_internal_with_round (this=0x7fa88005dec0, ltime=0x7fa8d42018f0,warnings=0x7fa8d4201920) 
#4 0x00000000009109e9 in Field_temporal::store (this=0x7fa88005dec0, str=0x7fa8800052f8 "2014-02-25 11:20:42", len=19, cs=0x168e400)
#5 0x000000000065360b in Item::save_str_value_in_field (this=0x7fa880005310, field=0x7fa88005dec0, result=0x7fa880005320)
#6 0x0000000000663ef6 in Item_string::save_in_field (this=0x7fa880005310, field=0x7fa88005dec0, no_conversions=false)
#7 0x000000000077bbc6 in fill_record (thd=0x6f24020, ptr=0x7fa88005deb8, values=..., ignore_errors=false, bitmap=0x0)
#8 0x000000000077bcf7 in fill_record_n_invoke_before_triggers (thd=0x6f24020, ptr=0x7fa88005deb0, values=..., ignore_errors=false,triggers=0x0, event)

从面的两个堆栈可以看出,在构造插入数据的时候,调用的是Field的具体函数,根据不同类型调用的方法不同;5.5与5.6之间,datetime的数据类型不一致,当5.5升级到5.6时,其堆栈不变,原因是在表的FRM中,记录了表中列的数据类型,其中5.5中的数据类型为MYSQL_TYPE_DATETIME,5.6的数据类型为MYSQL_TYPE_DATETIME2,所以对于原表升级,不影响复制,但是对于新表中如果含有这三种数据类型的表,复制到备库就会出现问题,因为5.5中,没有MYSQL_TYPE_DATETIME2这种数据类型。

解决方法

对表的DML操作或DDL操作,都是依赖于表结构而言的,这也是为什么物理5.5升级到5.6后,对于原本含有datetime,time,timestamp这三种类型的表没有影响,但是对于新建的表就会有影响,原因就是对于产生Binlog的操作或存储引擎的操作的Field来源于FRM文件,所以,当在创建表的时候,如果5.5要使用5.6的Binlog,那我们对于DDL含有这三种数据类型的操作,使用5.5可以识别的数据类型:MYSQL_TYPE_DATETIME,而不是MYSQL_TYPE_DATETIME2,这样在MySQL内部的操作过程中就不会有问题,因此我们可以为MySQL添加一个参数,当参数打开时,创建datetime,time,timestamp的数据类型为兼容5.5的数据类型,否则为新的数据类型。

TimeStamp 与 Datetime 的区别

1. 值域不同

TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC. DATETIME The supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59' TimeStamp带有时区信息,其中TimeStamp在存储时,将当前时间转化为UTC格式的时间,如北京时间,现在是2014-03-15 23:21:00,那么存储的会是2014-03-15 23:21:00 - 3600S;取数据的时候会加上当前时区时间。

2. 底层的存储结构不同

5.5 是以longlong类型存储的,而5.6 的格式如下:

timestamp:4+max(3); (变长,4-7个字节),没有Sign

datetime: 底层存储(变长,5-8个字节)

1 bit  sign            (used when on disk)
  17 bits year*13+month   (year 0-9999, month 0-12)
   5 bits day             (0-31)
   5 bits hour            (0-23)
   6 bits minute          (0-59)
   6 bits second          (0-59)
  24 bits microseconds    (0-999999)
  Total: 64 bits = 8 bytes

 

MySQL · 捉虫动态· 变量修改导致binlog错误

背景

MySQL 5.6.6 版本新加了这样一个参数——log_bin_use_v1_row_events,这个参数用来控制binlog中Rows_log_event的格式,如果这个值为1的话,就用v1版的Rows_log_event格式(即5.6.6之前的),默认是0,用新的v2版本的格式,更详细看官方文档。这个参数一般保持默认即可,但是当我们需要搭 5.6->5.5 这要的主备��时候,就需要把主库的这个值改为1,不然5.5的备库不能正确解析Rows_log_event。最近在使用这个参数的时候发现了一个bug,导致主库binlog写坏,备库复制中断,报错如下:

Last_SQL_Errno: 1594 Last_SQL_Error: Relay log read failure: Could not parse relay log event entry. The possible reasons are: the master's binary log is corrupted (you can check this by running 'mysqlbinlog' on the binary log), the slave's relay log is corrupted (you can check this by running 'mysqlbinlog' on the relay log), a network problem, or a bug in the master's or slave's MySQL code. If you want to check the master's binary log or slave's relay log, you will be able to know their names by issuing 'SHOW SLAVE STATUS' on this slave.

bug 分析

binlog event 结构

event header
event body
  -fixed part(postheader)
  -variable part (payload)

如上所示,每种binlog event 都可以分为header 和 body 2部分,body 又可以分为 fixed part 和 variable part,其中event header的长度相同并且固定,5.0开始用的v4格式的binlog,其event header固定长度为19字节,包含多个字段,具体每个字段的含义可以看这里。 event body 中post header 长度也是固定的,所以叫 fixed part,但是不同类型event这一部分的长度不一样,最后的 variable part 就是event的主体了,这个就长度不一了。 log_bin_use_v1_row_events 这个值的不同,影响的部分就是 postheader 这里的长度,如果值为1的话,用v1格式,postheader 长度是8个字节,如果是0,用v2格式,其长度为10。每个Rows_log_event的event header的type字节会标明当前event是v1还是v2,试想一下,如果event header部分标明是v2,postheader却实际上只有8个字节,或者反过来,event header部分标明是v1,postheader有10个字节,备库拿到这样的binlog,去尝试解析的时候,就完全凌乱了。

为啥会出现这种一个event前后不一致的情况呢,代码编写不严谨!

在写 Rows_log_event(Write/Update/Delete) 过程中,有2次用到 log_bin_use_v1_row_events 这个全局变量,一次是在构造函数处,一次是在写postheader时 Rows_log_event::write_data_header(),2次都是直接使用,如果正好在这2次中间,我们执行 set global log_bin_use_v1_row_events = 0|1,改变原来的值,就会导致前后逻辑判断结果不一致。如果主库有频繁的更新操作,每次更新又比较大,只要修改这个值,就很容易触发这个bug。

另外官方还有点不严谨的是,文档上说这个值是 readonly的,实际代码是dynamic 的,如果是 readonly 的话,也就不会触发上面的bug了。


bug修复

修复很简单,把2次引用全局变量改成一次就好了,在Rows_log_event::write_data_header函数里直接使用已经保存的m_type,改法如下

-  if (likely(!log_bin_use_v1_row_events))
+
+
+  if (likely(!(m_type == WRITE_ROWS_EVENT_V1 ||
+               m_type == UPDATE_ROWS_EVENT_V1 ||
+               m_type == DELETE_ROWS_EVENT_V1 )))

这样改之后,就只会在构造函数中才用到全局变量。

 

MariaDB · 特性分析· 表/表空间加密

Google向MariaDB 10.1.13(暂未Release)贡献了这个补丁,可以对表/表空间进行加密。

加密过的表可以防止某些非授权用户访问或偷取磁盘然后通过访问原始数据文件来偷取数据。当然,假设你已经把密钥文件存储在另一个系统上。但是,使用加密可能会降低差不多10%的性能。目前,只有XtraDB/InnoDB引擎能完全支持加密。

MariaDB在InnoDB/XtraDB中支持两种方式的加密:

Table encryption(表级加密): 只有在创建时指定 PAGE_ENCRYPTION=1 的表才被加密。

Tablespace encryption(表空间加密): 当前实例下的任何文件都被加密(包括日志文件)。

MariaDB中所有的加密算法都是基于AES的。但是你可以在启动的时候使用 --encryption-algorithm=name 来指定具体哪种基于AES的加密算法,有这些可选:

选项 描述
none 默认值。不进行任何加密。
aes_cbc 建议值。这是大部分欧洲政府接受的算法。
aes_ctr 一种新的块加密模式,Google自己开发的,并且在他们的MariaDB实例中使用的算法。
aes_ecb 这种加密模式用于内部计数器计算。你可以用它来加密,但它不能提供强保密性。


密钥的管理

为了保护加密过的数据,密钥必须另外保存,不要跟数据文件放在一个地方。默认情况下,MariaDB支持两种密钥管理方式,都是以Plugin的方式实现的。

1) file_key_management_plugin

file_key_management_plugin 是一个密钥管理插件,可以从文件中读取密钥。这个插件有如下配置选项:

file_key_management_plugin_filename=path-to-key-file: 密钥文件存放的位置

file_key_management_plugin_filekey: 一个可选的Key,用来解密密钥文件

一个my.cnf配置的例子:

[mysqld]
encryption-algorithm = aes_cbc
file_key_management_plugin_filename = /home/mdb/keys.enc
file_key_management_plugin_filekey = secret

这个密钥文件(/home/mdb/keys.enc)包含了AES密钥。128, 192 或 256位密钥都可以支持。ID有16个字节。 一个密钥文件内容的例子如下:

1;F5502320F8429037B8DAEF761B189D12;770A8A65DA156D24EE2A093277530142

1是密钥的标识,在建表的时候可以指定使用哪个密钥; 接着是16字节的ID,最后是一个16字节的AES密钥。密钥标识可以从0~255,但是0号密钥是保留给InnoDB日志文件使用的,不要在建表的时候用。

密钥文件本身同样可以加密,file_key_management_plugin_filekey定义的密钥可以用来解密密钥文件。OpenSSL命令行工具可以用来创建密钥文件。例如:

openssl enc –aes-256-cbc –md sha1 –k secret –in keys.txt –out keys.enc
openssl enc –aes-256-cbc –md sha1 –k <initialPwd> –in secret –out secret.enc

如果密钥文件在启动的时候读取不到(例如没提供解密密钥文件的Key),那么加密功能不再被支持,也无法访问被加密过的表。


2) example_key_management_plugin

为了获得真正高强度的加密,密钥必须周期性的变更。

example_key_management_plugin 是一个实现的例子。用你自己的密钥管理系统替换这个插件,可以创建一个非常安全的系统,例如从远程服务器来获取密钥。Google内部就是这么做的。

当然,example_key_management_plugin 仅仅是一个例子,你要根据它来编写你自己的Plugin!

加密数据

1)表级加密

表级加密意味着你可以选择哪些表需要加密。这可以让你在速度和安全性之间获得平衡。

要使用表级加密,你要这样做:

  • 设置 encryption-algorithm 选择你需要的加密算法;
  • 载入 file-key-management-plugin 插件或者其他相似的插件,添加 enable-file-key-management-plugin 到你的my.cnf文件。

要加密一个表,你需要在CREATE/ALTER TABLE 语句中使用新的参数:

选项 可选值 描述
PAGE_ENCRYPTION 0 或 1 1是启用这个功能,0是关闭
PAGE_ENCRYPTION_KEY 0-255 密钥表示。这个值表示用那个密钥。

例如:

CREATE TABLE T (id int, value varchar(255)) PAGE_ENCRYPTION=1 PAGE_ENCRYPTION_KEY=17;

这个建表语句会创建一个表T,使用标识为17的密钥加密。

ALTER TABLE T PAGE_ENCRYPTION=1 PAGE_ENCRYPTION_KEY=18;

更改表T使用标识为18的密钥来加密。如果你之前加密过这张表,那会首先解密这张表再重新加密。

ALTER TABLE T page_encryption=0;

关闭表T的加密功能。如果之前是加密的,那这个操作会做解密。


2) 表空间加密

要使用加密表空间,需要这样做:

  • 设置 encryption-algorithm 来选择加密算法;
  • 设置 innodb-encrypt-tables 为 1;
  • 载入 file-key-management-plugin 插件或者其他相似的插件,添加 enable-file-key-management-plugin 到你的 my.cnf 文件;
  • 启用 innodb-tablespaces-encryption 插件;
  • 启用 innodb-tablespaces-scrubbing 插件。

有下列变量可以配置加密项:

变量 可选值 描述
innodb-encrypt-tables Boolean (0 or 1) 是否在存储引擎加密所有表
innodb-encryption-rotate-key-age 秒数 如果获取到了新密钥多久更新一次页面加密
innodb-encryption-rotation-iop IOPS数 后台使用多少IOPS来做页面加密更新
innodb-encryption-threads 线程数 用多少个后台线程来做页面加密更新和清理
innodb-background-scrub-data-check-interval 秒数 隔多久检查空间是否需要清理
innodb-background-scrub-data-compressed Boolean (0 or 1) 打开通过后台线程来清理压缩数据
innodb-background-scrub-data-interval 秒数 隔多久调用一次清理
innodb-background-scrub-data-uncompressed Boolean (0 or 1) 打开通过后台线程来清理未压缩数据

清理(Scrubbing)意味着有一个后台线程定期的扫描所有的表并且对所有页面升级压缩密钥。

配置表空间加密的示例 my.cnf:

[mysqld]
encryption-algorithm=aes_ctr
innodb-tablespaces-encryption
innodb-tablespaces-scrubbing
innodb-encrypt-tables
innodb-encryption-threads=4

加密和压缩

加密和压缩(配合FusionIO使用的功能)可以同时使用。这可以让MariaDB先压缩数据然后再做加密,这种情况下可以很大的节省存储空间,并且数据更难被解密。

MariaDB · 特性分析· Per-query variables

自MariaDB 10.1.2起,MariaDB提供了一种"Per-query variables的方式来为Query设置语句级变量,通过 SET STATEMENT 语句可以为接下来要执行的语句设置一些系统变量值。


语法

SET STATEMENT var1=value1 [, var2=value2, ...] FOR <statement>

varN是一个系统变量,valueN是一个常量值。但是有部分变量是不支持的,在这个章节的末尾列出了所有不支持的变量。

一条 "SET STATEMENT var1=value1 FOR stmt" 语句等价与如下一系列操作:

SET @save_value=@@var1;

SET SESSION var1=value1;

stmt;

SET SESSION var1=@save_value;

MySQL服务器在执行整条语句前会先做解析,所以所有影响解析器的变量都无法达到预期的效果,因为解析完之后才能获得这些变量的值。例如字符集变量sql_mode=ansi_quotes。


一些使用特性的例子

可以限制语句的执行时间 max_statement_time: SET STATEMENT max_statement_time=1000 FOR SELECT ... ;

为一个语句临时改变优化器的规则: SET STATEMENT optimizer_switch='materialization=off' FOR SELECT ....;

为一个语句单独打开MRR/BKA特性: SET STATEMENT join_cache_level=6, optimizer_switch='mrr=on' FOR SELECT ...


下面这些变量无法使用Per-query variables特性来设置

autocommit

character_set_client

character_set_connection

character_set_filesystem

collation_connection

default_master_connection

debug_sync

interactive_timeout

gtid_domain_id

last_insert_id

log_slow_filter

log_slow_rate_limit

log_slow_verbosity

long_query_time

min_examined_row_limit

profiling

profiling_history_size

query_cache_type

rand_seed1

rand_seed2

skip_replication

slow_query_log

sql_log_off

tx_isolation

wait_timeout

TokuDB · 特性分析· 日志详解

TokuDB的日志跟InnoDB不一样,它有两类文件:

  1. redo-log文件(以.tokulog[序号]为扩展名)
  2. rollback日志文件(tokudb.rollback)

接下来就简单唠唠这两类文件的内部细节。

1) redo-log

记录的不是页而是对Fractal-Tree索引的操作日志。 log格式:

| length | command | lsn | content | crc|

content里记录的是具体的日志内容,比如insert操作,content就是:

| file-no | txnid | key | value|

TokuDB在做恢复的时候,会找到上次checkpoint时的LSN位置,然后读取log逐条恢复。

为了确保log的安全性,redo-log也支持从后往前解析。

当一个log的MAX_LSN小于已完成checkpoint的LSN时,就表明这个log文件可以安全删除了。

那么问题来了:

如果用户执行了一个“大事务”,比如delete一个大表,耗时很长,log文件岂不是非常多,一直等到事务提交再做清理?

不用的,这就是tokudb.rollback的作用了。

2) tokudb.rollback

用户的事务操作(insert/delete/update写操作)都会写一条日志到tokudb.rollback,存储的格式是:

|txnid | key|

记录日志伪码如下:

void ft_insert(txn,...)
{
   if (txn) {
       toku_logger_save_rollback_cmdinsert(...);
   }

   if (do_logging && logger) {
       toku_log_enq_insert(....);
   }
}

如果是事务,每个操作会写2条日志(1条redo,1条rollback)。

如果用户执行了commit/rollback,TokuDB会根据txnid在tokudb.rollback里查到key(如果该entry不在cache里),再根据key在索引文件里找到相应的事务信息并做相应的commit/rollback操作。

tokudb.rollback可以看做是一个事务的undo日志,记录的是<txnid,key>的关系映射。

MySQL InnoDB表--BTree基本数据结构

在MySQL的InnoDB存储引擎中count(*)函数的优化

MySQL InnoDB存储引擎锁机制实验

InnoDB存储引擎的启动、关闭与恢复

MySQL InnoDB独立表空间的配置

MySQL Server 层和 InnoDB 引擎层 体系结构图

InnoDB 死锁案例解析

MySQL Innodb独立表空间的配置

本文永久更新链接地址

相关内容