tangjianft 2020-01-05
MySQL 5.7在2015-10-21发布了GA版本,即5.7.9,目前小版本已经到了5.7.12。5.7新增了许多新的feature和优化,接下来一个系列,我们就一起来尝尝鲜。首先这次主要是预览feature的变化以及兼容性问题。后面的系列,会针对重要的feature展开来学习。
mysql.user表中的plugin更改成not null,5.7开始不再支持mysql_old_password的认证插件,推荐全部使用mysql_native_password。从低版本升级到5.7的时候,需要处理两个兼容性问题。
[兼容性] 需要先迁移mysql_old_password的用户,然后进行user表结构的升级:
1. 迁移mysql_old_password用户 MySQL 5.7.2之前的版本,是根据password的hash value来判断使用的认证插件类型,5.7.2以后的版本,plugin字段为not null,就直接根据plugin来判断了。新的密码从password字段中,保存到新的字段authentication_string中,password字段废弃处理。
如果user是隐式的mysql_native_password。直接使用sql进行变更:
UPDATE mysql.user SET plugin = ‘mysql_native_password‘ WHERE plugin = ‘‘ AND (Password = ‘‘ OR LENGTH(Password) = 41); FLUSH PRIVILEGES;
如果user是隐式的或者显示的mysql_old_password, 首先通过以下sql进行查询:
SELECT User, Host, Password FROM mysql.user WHERE (plugin = ‘‘ AND LENGTH(Password) = 16) OR plugin = ‘mysql_old_password‘;
如果存在记录,就表示还有使用mysql_old_password的user,使用以下sql进行用户的迁移:
ALTER USER ‘user1‘@‘localhost‘ IDENTIFIED WITH mysql_native_password BY ‘DBA-chosen-password‘;
2. user表结构升级 通过mysql_upgrade直接进行升级,步骤如下[5.6->5.7]:
用户可以通过 ALTER USER ‘jeffrey‘@‘localhost‘ PASSWORD EXPIRE;
这样的语句来使用户的密码过期。 并新增加 default_password_lifetime来表示用户密码自动过期时间,从5.7.10开始,其默认值从0变更到了360,也就是默认一年过期。 可以通过以下两种方法禁止过期:
1. SET GLOBAL default_password_lifetime = 0; 2. ALTER USER ‘jeffrey‘@‘localhost‘ PASSWORD EXPIRE NEVER;
[兼容性] 只需要通过mysql_upgrade升级mysql.user系统表就可以使用密码过期新功能。
用户可以通过以下语法进行账号锁定,阻止这个用户进行登录:
ALTER USER ‘jeffrey‘@‘localhost‘ ACCOUNT LOCK; ALTER USER ‘jeffrey‘@‘localhost‘ ACCOUNT UNLOCK;
[兼容性] 只需要通过mysql_upgrade升级mysql.user系统表就可以使用密码过期新功能。
如果mysqld编译使用的openssl,在启动的时候,默认创建SSL, RSA certificate 和 key 文件。 但不管是openssl还是yassl,如果没有设置ssl相关的参数,mysqld都会在data directory里查找ssl认证文件,来尽量打开ssl特性。
[兼容性] 不存在兼容性的问题
5.7开始建议用户使用 mysqld --initialize
来初始化数据库,放弃之前的mysql_install_db的方式,新的方式只创建了一个的用户,随机密码保存在~/.mysql_secret文件中,并且账号是expired,第一次使用必须reset password,并且不再创建test db。
[兼容性] 不存在兼容性的问题
5.7 sql_mode的默认值变更为:
mode_no_engine_substitution | mode_only_full_group_by | mode_strict_trans_tables | mode_no_zero_in_date | mode_no_zero_date | mode_error_for_division_by_zero | mode_no_auto_create_user
而在5.7之前,sql_mode的默认值都只有mode_no_engine_substitution。 所以在5.7默认的情况下,比如grant不存在的用户的时候,会报一下错误:
ERROR 1133 (42000): Can‘t find any matching row in the user table
必须先使用create user,然后再使用grant user。
[兼容性] 默认sql mode发生变更会导致sql的行为不一致。
支持online rename index操作, in_place并且不需要table copy。 [兼容性] 不存在兼容性的问题
变更varchar 类型字段的长度支持inplace方法,但有一个限制,即用于表示varchar字段长度的字节数不能发生变化,也就是支持比如varchar的长度在255以下变更或者255以上的范围进行变更,因为从小于255变更到大于255,其size的字节需要从1个增加到2个。
注意:减少varchar的长度,仍然需要table copy。
因为InnoDB临时表的数据不再不受redo保护,而redo只保护临时表的元数据,所以大幅提升了临时表的性能。 并且InnoDB临时表的元数据保存在一个新的系统表中即innodb_temp_table_info, 临时表将建立一个统一的表空间,我们称之为临时表空间,其目录地址可以通过参数innodb_temp_data_file_path来设置。系统在启动的时候,都会新建这个表空间,重启会删除重建。
例如:
mysql> show global variables like ‘%temp_data_file_path%‘; +----------------------------+-----------------------+ | Variable_name | Value | +----------------------------+-----------------------+ | innodb_temp_data_file_path | ibtmp1:12M:autoextend | +----------------------------+-----------------------+
并且5.7存储引擎默认都变更成InnoDB了:
mysql> show global variables like ‘%storage_engine%‘; +----------------------------------+--------+ | Variable_name | Value | +----------------------------------+--------+ | default_storage_engine | InnoDB | | default_tmp_storage_engine | InnoDB | | disabled_storage_engines | | | internal_tmp_disk_storage_engine | InnoDB | +----------------------------------+--------+
注意: 在开启gtid的情况下,非auto commit或者显示begin的context下,create 或者drop 临时表,仍然和5.6一样:
ERROR 1787 (HY000): Statement violates GTID consistency: CREATE TEMPORARY TABLE and DROP TEMPORARY TABLE can only be executed outside transactional context.
另外, insert into t select * from t也会遇到错误,不能在一个sql语句中reference两次临时表。
备注: 因为InnoDB临时表进行了比较大的变动,我们会专门进行一次详细的介绍。
并且支持在spatial data types上建立index,加速查询。
buffer pool dump和load支持一个新的参数innodb_buffer_pool_dump_pct,即dump的比例,并且使用innodb_io_capacity 来控制load过程中的IO吞吐量。
从5.7.4开始,innodb_page_cleaners参数可以设置,支持多线程flush dirty page,加快脏块的刷新。
MySQL 一直使用double write buffer来解决一个page写入的partial write问题,但在linux系统上的Fusion-io Non-Volatile Memory (NVM) file system支持原子的写入。 这样就可以省略掉double write buffer的使用, 5.7.4以后,如果Fusion-io devices支持atomic write,那么MySQL自动把dirty block直接写入到数据文件了。这样减少了一次内存copy和IO操作。
MySQL 5.7之前的版本,InnoDB并不支持分区表,分区表的支持是在ha_partition引擎上支持的,从5.7开始,InnoDB支持原生的分区表,并且可以使用传输表空间。
[兼容性] mysql_upgrade会扫描ha_partition引擎支持的InnoDB表,并升级成InnoDB分区表,5.7.9之后,可以通过命令ALTER TABLE … UPGRADE PARTITIONING.进行升级。如果之前的版本大量使用了分区表,要注意使用mysql_upgrade会消耗非常长的时间来升级分区表。
MySQL 5.7.5之后,可以online动态调整buffer pool size,通过设置动态的参数innodb_buffer_pool_size来调整,并且根据Innodb_buffer_pool_resize_status状态来查看resize的进度,因为resize的过程是以chunk为大小,把pages从一个内存区域copy到另一片内存的。
MySQL 5.7.5之前,在recovery的过程中,需要扫描所有的ibd文件,获取元信息, 5.7.5之后,新加了一种redo log类型,即MLOG_FILE_NAME, 记录从上一次checkpoint以来,发生过变更的文件,这样在recovery的过程中,只需要打开这些文件就可以了。 [兼容性] 因为增加了新的log record type,需要安全的关闭5.7之前的实例,清理掉redo。
支持创建表空间,例如
CREATE TABLESPACE `tablespace_name` ADD DATAFILE ‘file_name.ibd‘ [FILE_BLOCK_SIZE = n]
并可以在创建表的时候,指定属于哪个表空间,
[兼容性] 因为可以任意指定空间目录,要注意升级过程中,不要漏掉目录。
支持InnoDB数据文件加密,其依赖keyring plugin来进行秘钥的管理,后面我们单独来介绍InnoDB加密的方法,并且RDS也实现了一种InnoDB数据文件透明加密方法,并通过KMS系统来管理秘钥。例如:
create table t(id int) encryption=‘y‘;##########################################
继上一期月报,MySQL5.7新特性之一介绍了一些新特性及兼容性问题后,本期继续进行学习。
5.7以后System and status 变量需要从performance_schema中进行获取,information_schema仍然保留了GLOBAL_STATUS,GLOBAL_VARIABLES两个表做兼容。
[兼容性]
如果希望沿用information_schema中进行查询的习惯,5.7提供了show_compatibility_56参数,设置为ON可以兼容5.7之前的用法,否则就会报错:
ERROR 3167 (HY000): The ‘INFORMATION_SCHEMA.GLOBAL_STATUS‘ feature is disabled; see the documentation for ‘show_compatibility_56‘
5.7.6之后,在performance_schema新增了如下的表:
performance_schema.global_variables performance_schema.session_variables performance_schema.variables_by_thread performance_schema.global_status performance_schema.session_status performance_schema.status_by_thread performance_schema.status_by_account performance_schema.status_by_host performance_schema.status_by_user
5.7.9之前,需要有SELECT_ACL权限才能进行show查询,但5.7.9之后,默认这些表是不需要任何权限就可以访问了。
新增了sys数据库,主要是performance_schema收集的信息,帮助DBA和开发人员方便诊断问题。 sys下的一共包括三种对象:1. view,2. procedure 3 function 这些对象都是基于performance_schema下的表,进行了可读性的聚合,没有真正存储数据,只存储了定义。
[兼容性]
mysql_install_db可以选择–skip-sys-schema跳过安装过程, 但默认mysql_upgrade会帮你创建sys下面的对象。不存在兼容性的问题
5.7开始支持异常诊断栈信息,通过GET STACKED DIAGNOSTICS可以获取栈内的信息。具体的使用方法参考:https://dev.mysql.com/doc/refman/5.7/en/diagnostics-area.html
支持在一个table对象上建多个trigger。
5.7.6开始,支持生成列,这个列可以是虚拟的列,也可以是实体存储数据的列。比如:
CREATE TABLE triangle ( sidea DOUBLE, sideb DOUBLE, sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) );
VIRTUAL: 表示这个字段是虚拟列,并不进行存储,查询的时候,通过计算得到
STORED: 需要存储空间,并且可以被索引的列
这个是在oracle分区表上支持的功能,dba在做大表维护的时候,非常有用。
语法: ALTER TABLE ... EXCHANGE PARTITION WITHOUT VALIDATION
如果不验证,那么只有元数据信息的更改,就可以完成exchange,否则,就需要读取每一行数据进行验证,维护时间将根据这个表大小有关系。
5.7.2之前,master dump线程需要持有LOCK_log锁去读取binlog然后发送到备库,而这时会阻塞client端去写入binlog。5.7.2之后,dump线程只需要持有LOCK_binlog_end_pos这个锁去读取binlog的当前的位置,来决定是否发送到备库去,这样就可以做到不阻塞任何binlog的写入。
多源复制可以从多个master复制到一个slave端,在数据库集群进行扩容和缩容的时候,非常有用。我们会在后面的系列单独来介绍。
可以不用stop slave,然后在线更改replication master信息。 但这里并不是不需要slave停掉, 而是change master涉及到几个动作:
并行复制支持按照主库组提交的形式在备库进行回放。下一个系列进行单独来介绍
下面单独介绍一下MySQL 5.7对临时表进行的改动。
MySQL包括两类临时表,一类是通过create temporary table创建的临时表,一类是在query过程中using temporary而创建的临时表。 5.7之前,using temporary创建的临时表,默认只能使用myisam引擎,而在5.7之后,可以选择InnoDB引擎来创建。
临时表的引擎选择使用下面的这两个参数来决定:
mysql> show global variables like ‘%tmp%‘; +----------------------------------+---------------------------------------+ | Variable_name | Value | +----------------------------------+---------------------------------------+ | default_tmp_storage_engine | InnoDB | | internal_tmp_disk_storage_engine | InnoDB |
5.7之后,使用了独立的临时表空间来存储临时表数据,但不能是压缩表。临时表空间在实例启动的时候进行创建,shutdown的时候进行删除。
例如如下的配置:
mysql> show global variables like ‘%innodb_temp%‘; +----------------------------+-----------------------+ | Variable_name | Value | +----------------------------+-----------------------+ | innodb_temp_data_file_path | ibtmp1:12M:autoextend | +----------------------------+-----------------------+
create temporary table和using temporary table将共用这个临时表空间。
临时表会伴随着大量的数据写入和读取,尤其是internal_tmp_table。所以,InnoDB专门对临时表进行了优化。
InnoDB使用如下两个标示临时表:
dict_tf2_temporary: 表示普通临时表 dict_tf2_intrinsic: 表示内部临时表
这两个标示,会在IBD文件的segment header占用两个bit位。intrinsic一定是temproary,也就是temproary上进行的优化 完全适用于intrinsic表上。
下面来看下具体的优化:
临时表在连接断开或者数据库实例关闭的时候,会进行删除,所以,临时表的数据不需要redo来保护,即recovery的过程中 不恢复临时表,只有临时表的metadata使用了redo保护,保护元数据的完整性,以便异常启动后进行清理工作。
临时表的元数据,5.7之后,使用了一个独立的表进行保存,这样就不要使用redo保护,元数据也只保存在内存中。 但这有一个前提,必须使用共享的临时表空间,如果使用file-per-table,仍然需要持久化元数据,以便异常恢复清理。
temporary table仍然需要语句级的回滚,所以,需要为数据生成undo。但intrinsic table不需要回滚,所以,intrinsic table 减少了undo的生成,性能更高。
因为临时表只有本线程可以看见,所以减少了InnoDB的加锁过程。
可以看下insert的时候,进行的分支判断:
row_insert_for_mysql( const byte* mysql_rec, row_prebuilt_t* prebuilt) { /* For intrinsic tables there a lot of restrictions that can be relaxed including locking of table, transaction handling, etc. Use direct cursor interface for inserting to intrinsic tables. */ if (dict_table_is_intrinsic(prebuilt->table)) { return(row_insert_for_mysql_using_cursor(mysql_rec, prebuilt)); } else { return(row_insert_for_mysql_using_ins_graph( mysql_rec, prebuilt)); } }
row_insert_for_mysql_using_cursor直接跳过了加锁的lock_table过程。
然后,如果是intrinsic table,就直接插入,减少了undo的生成。
如果不是,需要加lock,并生成undo信息。
if (dict_table_is_intrinsic(index->table)) { index->rec_cache.rec_size = rec_size; *rec = page_cur_tuple_direct_insert( page_cursor, entry, index, n_ext, mtr); } else { /* Check locks and write to the undo log, if specified */ err = btr_cur_ins_lock_and_undo(flags, cursor, entry, thr, mtr, &inherit);
插入的时候,如果是临时表。就关闭redo的生成。如下面的代码所示:
if (dict_table_is_temporary(index->table)) { /* Disable REDO logging as the lifetime of temp-tables is limited to server or connection lifetime and so REDO information is not needed on restart for recovery. Disable locking as temp-tables are local to a connection. */ ut_ad(flags & BTR_NO_LOCKING_FLAG); ut_ad(!dict_table_is_intrinsic(index->table) || (flags & BTR_NO_UNDO_LOG_FLAG)); mtr.set_log_mode(MTR_LOG_NO_REDO); }
未完待续,下一个系列,我们将介绍一下undo的新特性,包括online truncated undo。
###############################################################################################
继上两期月报,MySQL5.7新特性之一介绍了一些新特性及兼容性问题,MySQL 5.7新特性之二介绍了临时表的优化和实现。
这期我们一起来学习下undo空间管理,重点介绍truncate功能。
InnoDB存储引擎中,undo在完成事务回滚和MVCC之后,就可以purge掉了,但undo在事务执行过程中,进行的空间分配如何回收,就变成了一个问题。 我们亲历用户的小实例,因为一个大事务,导致ibdata file到800G大小。
我们先大致看下InnoDB的undo在不同的版本上的一些演进:
MySQL 5.5的版本上
InnoDB undo是放在系统表空间即ibdata file文件中,这样如果有比较大的事务(即需要生成大量undo的),会撑大ibdata数据文件,虽然空间可以重用, 但文件大小不能更改。
关于回滚段的,只有这个主要的参数,用来设置多少个rollback segment。
mysql> show global variables like ‘%rollback_segment%‘; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_rollback_segments | 128 | +----------------------------+-------+
MySQL 5.6的版本上
InnoDB undo支持独立表空间, 增加如下参数:
+-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | innodb_undo_directory | . | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 1 | +-------------------------+-------+
这样,在install的时候,就会在data目录下增加undo数据文件,来组成undo独立表空间,但文件变大之后的空间回收还是成为问题。
MySQL 5.7的版本上
InnoDB undo在支持独立表空间的基础上,支持表空间的truncate功能,增加了如下参数:
mysql> show global variables like ‘%undo%‘; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | innodb_max_undo_log_size | 1073741824 | | innodb_undo_directory | ./ | | innodb_undo_log_truncate | OFF | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 3 | +--------------------------+------------+ mysql> show global variables like ‘%truncate%‘; +--------------------------------------+-------+ | Variable_name | Value | +--------------------------------------+-------+ | innodb_purge_rseg_truncate_frequency | 128 | | innodb_undo_log_truncate | OFF | +--------------------------------------+-------+
InnoDB的purge线程,会根据innodb_undo_log_truncate开关的设置,和innodb_max_undo_log_size设置的文件大小阈值,以及truncate的频率来进行空间回收和rollback segment的重新初始化。
接下来我们详细看下5.7的InnoDB undo的管理:
设置innodb_undo_tablespaces的个数, 在mysql install的时候,创建指定数量的表空间。
InnoDB支持128个undo logs,这里特别说明下,从5.7开始,innodb_rollback_segments的名字改成了innodb_undo_logs,但表示的都是回滚段的个数。
从5.7.2开始,其中32个undo logs为临时表的事务分配的,因为这部分undo不记录redo,不需要recovery,另外从33-128一共96个是redo-enabled undo。
rollback segment的分配如下:
Slot-0: reserved for system-tablespace. Slot-1....Slot-N: reserved for temp-tablespace. Slot-N+1....Slot-127: reserved for system/undo-tablespace. */
其中如果是临时表的事务,需要分配两个undo logs,其中一个是non-redo undo logs;这部分用于临时表数据的回滚。 另外一个是redo-enabled undo log,是为临时表的元数据准备的,需要recovery。
而且, 其中32个rollback segment创建在临时表空间中,并且临时表空间中的回滚段在每次server start的时候,需要重建。
每一个rollback segment可以分配1024个slot,也就是可以支持96*1024个并发的事务同时, 但如果是临时表的事务,需要占用两个slot。
InnoDB undo的空间管理简图如下:
注核心结构说明:
1. rseg slot
rseg slot一共128个,保存在ibdata系统表空间中,其位置在:
/*!< the start of the array of rollback segment specification slots */ #define TRX_SYS_RSEGS (8 + FSEG_HEADER_SIZE)
每一个slot保存着rollback segment header的位置。包括space_id + page_no,占用8个bytes。其宏定义:
/* Rollback segment specification slot offsets */ /*-------------------------------------------------------------*/ #define TRX_SYS_RSEG_SPACE 0 /* space where the segment header is placed; starting with MySQL/InnoDB 5.1.7, this is UNIV_UNDEFINED if the slot is unused */ #define TRX_SYS_RSEG_PAGE_NO 4 /* page number where the segment header is placed; this is FIL_NULL if the slot is unused */ /* Size of a rollback segment specification slot */ #define TRX_SYS_RSEG_SLOT_SIZE 8
2. rseg header
rseg header在undo表空间中,每一个rseg包括1024个undo segment slot,每一个slot保存着undo segment header的位置,包括page_no,暂用4个bytes,因为undo segment不会跨表空间,所以space_id就没有必要了。
其宏定义如下:
/* Undo log segment slot in a rollback segment header */ /*-------------------------------------------------------------*/ #define TRX_RSEG_SLOT_PAGE_NO 0 /* Page number of the header page of an undo log segment */ /*-------------------------------------------------------------*/ /* Slot size */ #define TRX_RSEG_SLOT_SIZE 4
3. undo segment header
undo segment header page即段内的第一个undo page,其中包括四个比较重要的结构:
undo segment header | 进行段内空间的管理 |
undo page header | page内空间的管理,page的类型:FIL_PAGE_UNDO_LOG |
undo header | 包含undo record的链表,以便安装事务的反顺序,进行回滚 |
undo record | 剩下的就是undo记录了。 |
undo段的分配比较简单,其过程如下:
首先是rollback segment的分配:
trx->rsegs.m_redo.rseg = trx_assign_rseg_low( srv_undo_logs, srv_undo_tablespaces, TRX_RSEG_TYPE_REDO);
具体代码参考:
/******************************************************************//** Get next redo rollback segment. (Segment are assigned in round-robin fashion). @return assigned rollback segment instance */ static trx_rseg_t* get_next_redo_rseg( /*===============*/ ulong max_undo_logs, /*!< in: maximum number of UNDO logs to use */ ulint n_tablespaces) /*!< in: number of rollback tablespaces */
其次是undo segment的创建:
从rollback segment里边选择一个free的slot,如果没有,就会报错,通常是并发的事务太多。
错误日志如下:
ib::warn() << "Cannot find a free slot for an undo log. Do" " you have too many active transactions running" " concurrently?";
如果有free,就创建一个undo的segment。
核心的代码如下:
/***************************************************************//** Creates a new undo log segment in file. @return DB_SUCCESS if page creation OK possible error codes are: DB_TOO_MANY_CONCURRENT_TRXS DB_OUT_OF_FILE_SPACE */ static dberr_t trx_undo_seg_create( /*================*/ trx_rseg_t* rseg __attribute__((unused)),/*!< in: rollback segment */ trx_rsegf_t* rseg_hdr,/*!< in: rollback segment header, page x-latched */ ulint type, /*!< in: type of the segment: TRX_UNDO_INSERT or TRX_UNDO_UPDATE */ ulint* id, /*!< out: slot index within rseg header */ page_t** undo_page, /*!< out: segment header page x-latched, NULL if there was an error */ mtr_t* mtr) /*!< in: mtr */ /* fputs(type == TRX_UNDO_INSERT ? "Creating insert undo log segment\n" : "Creating update undo log segment\n", stderr); */ slot_no = trx_rsegf_undo_find_free(rseg_hdr, mtr); if (slot_no == ULINT_UNDEFINED) { ib::warn() << "Cannot find a free slot for an undo log. Do" " you have too many active transactions running" " concurrently?"; return(DB_TOO_MANY_CONCURRENT_TRXS); }
undo的truncate主要由下面两个参数控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。
当设置innodb_undo_log_truncate=ON的时候, undo表空间的文件大小,如果超过了innodb_max_undo_log_size, 就会被truncate到初始大小,但有一个前提,就是表空间中的undo不再被使用。
其主要步骤如下:
默认情况下, 是purge触发128次之后,进行一次rollback segment的free操作,然后如果全部free就进行一个truncate。
但mark的操作需要几个依赖条件需要满足:
其核心代码参考:
/** Iterate over all the UNDO tablespaces and check if any of the UNDO tablespace qualifies for TRUNCATE (size > threshold). @param[in,out] undo_trunc undo truncate tracker */ static void trx_purge_mark_undo_for_truncate( undo::Truncate* undo_trunc)
因为,只要你设置了truncate = on,MySQL就尽可能的帮你去truncate所有的undo表空间,所以它会循环的把undo表空间加入到mark列表中。
最后,循环所有的undo段,如果所属的表空间是marked truncate,就把这个rseg标志位不可分配,加入到trunc队列中,在purge的时候,进行free rollback segment。
注意:
如果是在线库,要注意影响,因为当一个undo tablespace在进行truncate的时候,不再承担undo的分配。只能由剩下的undo 表空间的rollback segment接受事务undo空间请求。
MySQL 5.7 新特性系列,下次进行group replication的分享,敬请期待######################################################################
继上三期月报: MySQL 5.7新特性之一介绍了一些新特性及兼容性问题 MySQL 5.7新特性之二介绍了临时表的优化和实现 MySQL 5.7新特性之三介绍了undo表空间的truncate功能
这期我们一起来学习下MySQL 5.7的并行复制。
MySQL的master<->slave的部署结构,使用binlog日志保持数据的同步,全局有序的binlog在备库按照提交顺序进行回放。 由于新硬件的发展,SSD的引入和多core的CPU,master节点的并发处理能力持续提升,slave节点完全按照binlog写入顺序的单线程回放,已完全跟不上master节点的吞吐能力,导致HA切换和数据保护带来巨大的挑战。
从MySQL5.5版本以后,开始引入并行复制的机制,是MySQL的一个非常重要的特性。
MySQL5.6开始支持以schema为维度的并行复制,即如果binlog row event操作的是不同的schema的对象,在确定没有DDL和foreign key依赖的情况下,就可以实现并行复制。
社区也有引入以表为维度或者以记录为维度的并行复制的版本,不管是schema,table或者record,都是建立在备库slave实时解析row格式的event进行判断,保证没有冲突的情况下,进行分发来实现并行。
MySQL5.7的并行复制,multi-threaded slave即MTS,期望最大化还原主库的并行度,实现方式是在binlog event中增加必要的信息,以便slave节点根据这些信息实现并行复制。
下面我们就来看下MySQL 5.7的实现方式:
MySQL 5.7的并行复制建立在group commit的基础上,所有在主库上能够完成prepared的语句表示没有数据冲突,就可以在slave节点并行复制。
我们先来回顾一下group commit的情况:
1. group commit的过程: 1. binlog prepare 2. InnoDB prepare 3. binlog commit(ordered commit) --3.1 Stage #1: flushing transactions to binary log --3.2 Stage #2: Syncing binary log file to disk --3.3 Stage #3: Commit all transactions in order. 4. InnoDB commit
在ordered commit的过程中:
为了表示主库并行度,在binlog row event增加了如下的标识:
#160807 15:48:10 server id 100 end_log_pos 739 CRC32 0x2237b2ef GTID last_committed=0 sequence_number=3 SET @@SESSION.GTID_NEXT= ‘8108dc48-47de-11e6-8690-a0d3c1f20ae4:3‘/*!*/;
即在gtid_event中增加两个字段:
class Gtid_event: public Binary_log_event { public: /* The transaction‘s logical timestamps used for MTS: see Transaction_ctx::last_committed and Transaction_ctx::sequence_number for details. Note: Transaction_ctx is in the MySQL server code. */ long long int last_committed; long long int sequence_number; /** Ctor of Gtid_event The layout of the buffer is as follows +-------------+-------------+------------+---------+----------------+ | commit flag | ENCODED SID | ENCODED GNO| TS_TYPE | logical ts(:s) | +-------------+-------------+------------+---------+----------------+ TS_TYPE is from {G_COMMIT_TS2} singleton set of values
代码中为每一个transaction准备了如下的字段:
class Transaction_ctx { ...... int64 last_committed; int64 sequence_number; }
MYSQL_BIN_LOG全局对象中维护了两个结构:
class MYSQL_BIN_LOG: public TC_LOG { ...... /* Committed transactions timestamp */ Logical_clock max_committed_transaction; /* "Prepared" transactions timestamp */ Logical_clock transaction_counter; }
事务中的sequence_number是一个全局有序递增的数字,每个事务递增1,来源mysql_bin_log.tranaction_counter.
和gtid一对一的关系,即在flush阶段,和gtid生成的时机一致,代码参考:
binlog_cache_data::flush { if (flags.finalized) { Transaction_ctx *trn_ctx= thd->get_transaction(); trn_ctx->sequence_number= mysql_bin_log.transaction_counter.step(); } ....... mysql_bin_log.write_gtid(thd, this, &writer))) mysql_bin_log.write_cache(thd, this, &writer); }
事务中last_committed表示在这个commit下的事务,都是可以并行的,即没有冲突, Transaction_ctx中的last_committed在每个语句prepared的时候进行初始化,来源mysql_bin_log.max_committed_transaction
static int binlog_prepare(handlerton *hton, THD *thd, bool all) { ...... Logical_clock& clock= mysql_bin_log.max_committed_transaction; thd->get_transaction()-> store_commit_parent(clock.get_timestamp()); }
而mysql_bin_log.max_committed_transaction的更新是在group commit提交的时候进行变更。
MYSQL_BIN_LOG::process_commit_stage_queue(THD *thd, THD *first) { ...... for (THD *head= first ; head ; head = head->next_to_commit) { if (thd->get_transaction()->sequence_number != SEQ_UNINIT) update_max_committed(head); } }
即获取这个group commit队列中的最大的sequence_number当成当前的max_committed_transaction。
所以,这个机制可以理解成,在group commit完成之前,所有可以成功prepared的语句,没有事实上的冲突, 分配成相同的last_committed,就可以在slave节点并行复制。
例如下面时序的事务:
session 1:insert into t1 value(100, ‘xpchild‘); session 2:insert into t1 value(101, ‘xpchild‘); session 2:commit session 1:commit
Binlog日志片段如下:
# at 1398 #160807 15:38:14 server id 100 end_log_pos 1463 CRC32 0xd6141f71 GTID last_committed=5 sequence_number=6 SET @@SESSION.GTID_NEXT= ‘8108dc48-47de-11e6-8690-a0d3c1f20ae4:6‘/*!*/; ‘/*!*/; ### INSERT INTO `tp`.`t1` ### SET ### @1=101 /* INT meta=0 nullable=0 is_null=0 */ ### @2=‘xpchild‘ /* VARSTRING(100) meta=100 nullable=1 is_null=0 */ COMMIT/*!*/; # at 1658 #160807 15:38:24 server id 100 end_log_pos 1723 CRC32 0xa24923a8 GTID last_committed=5 sequence_number=7 SET @@SESSION.GTID_NEXT= ‘8108dc48-47de-11e6-8690-a0d3c1f20ae4:7‘/*!*/; ### INSERT INTO `tp`.`t1` ### SET ### @1=100 /* INT meta=0 nullable=0 is_null=0 */ ### @2=‘xpchild‘ /* VARSTRING(100) meta=100 nullable=1 is_null=0 */
两个insert语句在prepared的时候,没有事实上的冲突,都获取当前最大的committed number = 5. 提交的过程中,保持sequence_number生成时候的全局有序性,备库恢复的时候,这两个事务就可以并行完成。
但又如下面的case:
session 1: insert into t1 value(100, ‘xpchild‘); session 2: insert into t1 value(101, ‘xpchild‘); session 2: commit; session 3: insert into t1 value(102, ‘xpchild‘); session 3: commit; session 1: commit;
产生如下的顺序:
#160807 15:47:58 server id 100 end_log_pos 219 CRC32 0x3f295e2b GTID last_committed=0 sequence_number=1 ### INSERT INTO `tp`.`t1` ### SET ### @1=101 /* INT meta=0 nullable=0 is_null=0 */ ..... #160807 15:48:05 server id 100 end_log_pos 479 CRC32 0xda52bed0 GTID last_committed=1 sequence_number=2 ### INSERT INTO `tp`.`t1` ### SET ### @1=102 /* INT meta=0 nullable=0 is_null=0 */ ...... #160807 15:48:10 server id 100 end_log_pos 739 CRC32 0x2237b2ef GTID last_committed=0 sequence_number=3 ### INSERT INTO `tp`.`t1` ### SET ### @1=100 /* INT meta=0 nullable=0 is_null=0 */ ....
session 1和session 2语句执行不冲突,分配了相同的last_committed,
session 2提交,推高了last_committed,所以session 3的laste_committed变成了1,
最后session 1提交。
注意: 这就是MySQL 5.7.3之后的改进:
在MySQL 5.7.3之前,必须在一个group commit之内的事务,才能够在slave节点并行复制,但如上面的这个case。
session 1 和session 2虽然commit的时间有差,并且不在一个group commit,生成的binlog也没有连续,但事实上是可以并行恢复执行的。
所以从MySQL 5.7.3之后,并行恢复,减少了group commit的依赖,但group commit仍然对并行恢复起着非常大的作用。
MySQL 5.7增加了如下参数:
mysql> show global variables like ‘%slave_parallel_type%‘; +---------------------+---------------+ | Variable_name | Value | +---------------------+---------------+ | slave_parallel_type | LOGICAL_CLOCK | +---------------------+---------------+ 1 row in set (0.00 sec)
slave_parallel_type取值:
group commit delay
首先,并行复制必须建立在主库的真实负载是并行的基础上,才能使MTS有机会在slave节点上完成并行复制, 其次,MySQL 5.7前面讨论的实现机制,可以人工的增加group commit的delay,打包更多的事务在一起,提升slave复制的并行度。但从5.7.3开始,已经减少了group commit的依赖, 尽量减少delay参数设置对主库的影响。
合理设置如下参数;
mysql> show global variables like ‘%group_commit%‘; +-----------------------------------------+--------+ | Variable_name | Value | +-----------------------------------------+--------+ | binlog_group_commit_sync_delay | 100000 | | binlog_group_commit_sync_no_delay_count | 0 | +-----------------------------------------+--------+
另外: booking在使用的时候遇到的如下case:
数据库的部署结构是:master->slave1->slave2
假设,当t1,t2,t3,t4四个事务在master group commit中,那么slave1线程就可以并行执行这四个事务, 但在slave1执行的过程中,分成了两个group commit,那么在slave2节点上,并行度就降低了一倍。
booking给出的后续的解法,如果slave不多,建议都挂载在master上,如果slave过多,考虑使用binlog server,来避免这样的问题。
但其实在slave1节点上进行并行恢复的时候,保持着主库的last_committed和sequence_number不变,虽然无法保证binlog写入的顺序完全和主库一致,但可以缓解这种情况。