天天看点

[MySQL 5.6] MySQL 5.6 online ddl 使用、测试及关键函数栈前言性能损耗内部实现

本文主要分为三个部分,第一部分是看文档时的笔记;第二部分使用sysbench简单测试了下性能损耗;第三部分阐述了关键函数栈,但未做深入

online ddl是mysql 5.6的重要特性之一,特别是对于不可间断的互联网服务而言意义非凡。尽管我们已经通过工具来实现了在线ddl,但由于借助了触发器来获取增量数据,很难保证不会触发bug,我们在5.1版本上广泛使用了内部开发的myddl,曾经触发了mysql6个以上的bug。

innodb允许你通过设置lock=exclusive | shared | default/none 来进行完全阻塞的ddl、只阻塞dml不阻塞查询、以及完全在线ddl,这有助于你能够在性能和速度之间进行权衡

以下是从官方文档拷贝的一张关于online ddl对于当前ddl操作的支持:

operation

in-place?

copies table?

allows concurrent dml?

allows concurrent query?

notes

<code>create index</code>,<code>add index</code>

yes*

no*

yes

<code>add fulltext index</code>

no

creating the first <code>fulltext</code> index for a table involves a table copy, unless there is a user-supplied <code>fts_doc_id</code> column. subsequent <code>fulltext</code> indexes on the same table can be created in-place.

<code>drop index</code>

set default value for a column

modifies <code>.frm</code> file only, not the data file.

modifies a value stored in memory, not the data file.

rename a column

to allow concurrent dml, keep the same data type and only change the column name.

add a column

drop a column

although <code>algorithm=inplace</code> is allowed, the data is reorganized substantially, so it is still an expensive operation.

reorder columns

change<code>row_format</code>property

change<code>key_block_size</code>property

make column<code>null</code>

make column <code>not null</code>

change data type of column

restrictions apply when you drop a primary key primary key without adding a new one in the same <code>alter table</code>statement.

convert character set

rebuilds the table if the new character encoding is different.

specify character set

rebuild with<code>force</code> option

从官方提供的这个表格来看,还是有很多操作不支持完全的在线ddl,包括增加一个全文索引,修改列的数据类型,删除一个主键,修改表的字符集等。

但对于大多数我们日常常用的ddl而言,是可以做到在线ddl的。

通常情况下,可以使用默认的语法来进行在线ddl,但你也可以通过选项来改变ddl的行为,有两个选项

<code>lock=</code>

<code>algorithm=[inplace|copy]</code> 

<code></code>

当对主键进行操作时,可以选择algorithm=inplace 比设置为copy更有效率,因为前者不会去记录undo log或者为其记录redo log;二级索引被预先排序,能够进行有序的加载;change buffer也没有被使用到,因为没有涉及到对二级索引记录的随机插入操作

你可以通过观察执行完ddl后的输出: xx rows affected,来判断是in-place 还是copy数据,为0的话就是in-place。

关于online ddl的具体使用,这里不做阐述,可以看看文档;这里只是简要阐述下其涉及到的函数堆栈

这里使用sysbench来测试,配置如下:

innodb_sort_buffer_size=2m

innodb_online_alter_log_max_size=2g  

sysbench command:

sysbench/sysbench –debug=off –test=sysbench/tests/db/update_index.lua  –oltp-tables-count=1  –oltp-point-selects=0 –oltp-table-size=1000000 –num-threads=20 –max-requests=10000000000 –max-time=7200 –oltp-auto-inc=off –mysql-engine-trx=yes –mysql-table-engine=innodb  –oltp-test-mod=complex –mysql-db=sbtest   –mysql-host=$host –mysql-port=$port –mysql-user=xx run 

a.

alter table sbtest1 drop key k;

tps :20,200

b.

alter table sbtest1 add key(k);

tps:大部分聚集在11,000~13,000,有抖动到7,000~9,000;最后出现12秒左右的tps降低为0

time cost:4 min 8.13 sec)

完成ddl后,tps稳定在13,000~14,000

alter table sbtest1 drop key k;  //tps恢复至20,200

c.

set session old_alter_table = 1;

tps:0

time cost:28.39 sec

总结:

1. online ddl耗时问题,相比老的ddl方式要更耗时

2. 存在性能抖动,最后阶段的锁表时间可能比较长,这取决于具体的负载,sysbench本身的压力已经比较高了,正常情况下的线上实例不会有这么大压力。

ps

无压力负载测试:

mysql&gt; set session old_alter_table = off;

query ok, 0 rows affected (0.00 sec)

mysql&gt; alter table sbtest1 add  key (k);

query ok, 0 rows affected (10.44 sec)

records: 0  duplicates: 0  warnings: 0

mysql&gt; alter table sbtest1 drop key k;

query ok, 0 rows affected (0.01 sec)

mysql&gt; alter table sbtest1 add key(k), algorithm=copy;

query ok, 1000000 rows affected (27.72 sec)

records: 1000000  duplicates: 0  warnings: 0

mysql&gt; alter table sbtest1 add key(k), lock=shared;

query ok, 0 rows affected (9.89 sec)

mysql&gt; alter table sbtest1 add key(k), lock=exclusive;

query ok, 0 rows affected (10.07 sec)

这里我们以给一个普通的表增加一个普通二级索引为例

create table t1 (a int primary key, b int, c int);

insert into t1 values (1,2,3),(2,3,4),(3,4,5);

alter table t1 add key(b);

1.ddl 线程

ddl的入口函数是mysql_alter_table,这里我们只谈涉及到innodb层的函数。

大部分alter的接口函数都定义在文件hander0alter.cc中,关于online ddl主要分为四个阶段

a.检查存储引擎是否支持in-place 的ddl操作

8028     // ask storage engine whether to use copy or in-place

8029     enum_alter_inplace_result inplace_supported=

8030       table-&gt;file-&gt;check_if_supported_inplace_alter(altered_table,

8031                                                     &amp;ha_alter_info);

通常in-place操作比copy table的方式效率要高,如果不确定即将做的ddl是in-place的,可以拷贝一个完全一样的表,写入一两条数据,然后再做alter table,看输出是否有affected rows.没有的话说明就是in-place的。

是否支持in-place操作请参照上表,返回三个值:

ha_alter_inplace_not_supported  not supported

ha_alter_inplace_no_lock        supported

ha_alter_inplace_shared_lock_after_prepare supported, but requires lock during main phase and exclusive lock during prepare phase

ha_alter_inplace_no_lock_after_prepare  supported, prepare phase requires exclusive lock (any transactions that have accessed the table must commit or roll back first, and no transactions can access the table while prepare_inplace_alter_table() is executing 

例如如上操作,从函数返回的值为ha_alter_inplace_no_lock_after_prepare,表示支持in-place,但在准备阶段需要排他锁,也就是说在准备阶段需要确保当前任意操作该表的事务提交或回滚。当执行ha_innobase::prepare_inplace_alter_table时,所有事务会被阻塞。

当确认支持in-place操作后,就会进入另外一个函数分支mysql_inplace_alter_table

否则继续下面的逻辑(暂且不论)

b.准备阶段

mysql_inplace_alter_table:

6388   if (table-&gt;file-&gt;ha_prepare_inplace_alter_table(altered_table,

6389                                                   ha_alter_info))6390   {

6391     goto rollback;

6392   }

在准备阶段之前,已经加了表级别锁,这时候所有并发dml会被阻塞掉。

对应innodb层的函数是ha_innobase::prepare_inplace_alter_table,主要做以下动作:

b.1. ddl合法性检查,例如索引名是否是系统保留名(innobase_index_name_is_reserved),检查索引键(innobase_check_index_keys),禁止将列rename成一个已经存在的列名,检查索引列的长度以保证其不超过限制,检查外键、全文索引、自增列等操作.

这是一段冗长的代码,涉及大量的细节

b.2.在函数的最后调用函数prepare_inplace_alter_table_dict

这也是准备阶段,在完成检查后的一个重要函数,主要做以下事情:

b.2.1.先锁住innodb数据词典(row_mysql_lock_data_dictionary,给dict_operation_lock加排他x锁,并加上dict_sys-&gt;mutex),再确认没有后台线程操作该表(dict_stats_wait_bg_to_stop_using_tables),随后调用的online_retry_drop_indexes_low暂时没搞清楚,先留着

如果是新建一个聚集索引,还需要drop掉原始表,再重新创建索引(很长一段逻辑,后续跟进)

b.2.2.更新数据词典信息,在系统表sys_indexes中创建索引(row_merge_create_index)。然后在持有新建的索引的锁的情况下,为其分配行的增量日志(row_log_allocate).

增量日志主要用于在ddl的过程中,存储dml对数据的修改,其对应的控制结构体为row_log_t,挂在index-&gt;online_log上面,初始分配的内存大小为:

       2 * srv_sort_buf_size + sizeof *log 

其中srv_sort_buf_size对应的参数为innodb_sort_buffer_size,这也是增量日志每次扩展的块大小,另外它也是创建索引时做merge排序时,一个缓冲块的大小,在老版本中被hardcode为1m,percona在5.5中也将其设置成可配置,在一定程度上能提升fast index creatition的效率。说到这个,就不得不提到另外一个变量innodb_online_alter_log_max_size,它用于限制增量日志区域的最大限制,根据文档的描述,如果超过了限制,就会导致ddl失败,并且当前所有并发未提交的事务都会回滚。

b.2.3.提交对数据词典操作的事务,然后释放数据词典锁

trx_commit_for_mysql(trx);

row_mysql_unlock_data_dictionary(trx);  

c.执行ddl阶段

6419   if (table-&gt;file-&gt;ha_inplace_alter_table(altered_table,

6420                                           ha_alter_info))

6421   {

6422     goto rollback;

6423   }

在执行真正的ddl之前,首先要对mdl锁做降级(mdl_shared_upgradable),以确保并发dml能够执行。

上述调用对应innodb层为ha_innobase::inplace_alter_table

首先读取聚集索引记录,使用merge排序生成二级索引记录,并将数据插入到新创建的索引中

函数row_merge_build_indexes除了完成上述行为,随后还会调用row_log_apply应用增量日志

这里不深入,后续再展开讨论增量日志是如何生产和应用的,这里实际上也是online ddl的核心

在完成上述步骤后,回到mysql层,会将mdl锁升级为排他锁,这意味着在下面的commit阶段将会阻塞对该表的dml操作

注意,该步骤如果等待超时,可能会引起ddl回滚。因此最好确认在ddl的时候没有逻辑备份业务

但不管是回滚还是提交,都会进入下一个阶段来完成

d.提交或回滚ddl阶段

6446   if (table-&gt;file-&gt;ha_commit_inplace_alter_table(altered_table,

6447                                                  ha_alter_info,

6448                                                  true))

6449   {

6450     goto rollback;

6451   }

对应innodb层函数:ha_innobase::commit_inplace_alter_table ,又是一段近800行的冗长函数代码,在该阶段决定是回滚ddl还是提交ddl操作;也会在该阶段执行drop index,rename column,增加或删除外键等操作,以及最终完成表的重建或索引创建的最后工作。该阶段会阻塞对表的dml操作。

对于drop index操作,会将其先在数据词典中rename掉(row_merge_rename_index_to_drop),以temp_index_prefix作为命名前缀,然后在随后的row_merge_drop_indexes_dict函数再做真正的删除, 并从dict cache中删除(dict_index_remove_from_cache)。注意,只要index在数据词典中被rename掉,在crash recovery后,也会被删除掉。

函数太长了,有空再按照不同的ddl类型来跟踪其流程。

2.dml线程

最后一个问题是,在做ddl的过程中,dml在哪里记录row log呢?

相关函数被定义在文件row0log.cc中。

所有对索引的修改,通过函数row_log_online_op来记录

当表需要进行rebuilt时,通过函数row_log_table_delete、row_log_table_update、row_log_table_insert来记录更改

例如对二级索引的update操作,调用栈为:

row_update_for_mysql-&gt;row_upd_step-&gt;row_upd-&gt;row_upd_sec_index_entry-&gt;row_log_online_op 

update操作会调用两次row_log_online_op,先删除,再插入。