• 5.7新特性


    1. 背景

    MySQL 5.7在2015-10-21发布了GA版本,即5.7.9,目前小版本已经到了5.7.12。5.7新增了许多新的feature和优化,接下来一个系列,我们就一起来尝尝鲜。首先这次主要是预览feature的变化以及兼容性问题。后面的系列,会针对重要的feature展开来学习。

    2 安全相关的特性

    2.1 认证插件

    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]:

    1. stop MySQL 5.6实例
    2. 替换5.7的mysqld二进制版本
    3. 使用5.7启动实例
    4. run mysql_upgrade升级系统表
    5. 重启MySQL 5.7实例

    2.2 密码过期

    用户可以通过 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系统表就可以使用密码过期新功能。

    2.3 账号锁定

    用户可以通过以下语法进行账号锁定,阻止这个用户进行登录:

    ALTER USER 'jeffrey'@'localhost' ACCOUNT LOCK;
    ALTER USER 'jeffrey'@'localhost' ACCOUNT UNLOCK;
    

    [兼容性] 只需要通过mysql_upgrade升级mysql.user系统表就可以使用密码过期新功能。

    2.4 SSL连接

    如果mysqld编译使用的openssl,在启动的时候,默认创建SSL, RSA certificate 和 key 文件。 但不管是openssl还是yassl,如果没有设置ssl相关的参数,mysqld都会在data directory里查找ssl认证文件,来尽量打开ssl特性。

    [兼容性] 不存在兼容性的问题

    2.5 安装数据库

    5.7开始建议用户使用 mysqld --initialize来初始化数据库,放弃之前的mysql_install_db的方式,新的方式只创建了一个root@localhost的用户,随机密码保存在~/.mysql_secret文件中,并且账号是expired,第一次使用必须reset password,并且不再创建test db。

    [兼容性] 不存在兼容性的问题

    3 sql mode变更

    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的行为不一致。

    4. online alter table

    支持online rename index操作, in_place并且不需要table copy。 [兼容性] 不存在兼容性的问题

    5. InnoDB增强

    5.1 varchar长度变更支持inplace

    变更varchar 类型字段的长度支持inplace方法,但有一个限制,即用于表示varchar字段长度的字节数不能发生变化,也就是支持比如varchar的长度在255以下变更或者255以上的范围进行变更,因为从小于255变更到大于255,其size的字节需要从1个增加到2个。

    注意:减少varchar的长度,仍然需要table copy。

    5.2 优化InnoDB临时表

    因为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临时表进行了比较大的变动,我们会专门进行一次详细的介绍。

    5.3 InnoDB原生支持DATA_GEOMETRY类型

    并且支持在spatial data types上建立index,加速查询。

    5.4 buffer pool dump

    buffer pool dump和load支持一个新的参数innodb_buffer_pool_dump_pct,即dump的比例,并且使用innodb_io_capacity 来控制load过程中的IO吞吐量。

    5.5 多线程flush dirty

    从5.7.4开始,innodb_page_cleaners参数可以设置,支持多线程flush dirty page,加快脏块的刷新。

    5.6 NVM file system

    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操作。

    5.7 InnoDB分区表

    MySQL 5.7之前的版本,InnoDB并不支持分区表,分区表的支持是在ha_partition引擎上支持的,从5.7开始,InnoDB支持原生的分区表,并且可以使用传输表空间。

    [兼容性] mysql_upgrade会扫描ha_partition引擎支持的InnoDB表,并升级成InnoDB分区表,5.7.9之后,可以通过命令ALTER TABLE … UPGRADE PARTITIONING.进行升级。如果之前的版本大量使用了分区表,要注意使用mysql_upgrade会消耗非常长的时间来升级分区表。

    5.8 动态调整buffer pool size

    MySQL 5.7.5之后,可以online动态调整buffer pool size,通过设置动态的参数innodb_buffer_pool_size来调整,并且根据Innodb_buffer_pool_resize_status状态来查看resize的进度,因为resize的过程是以chunk为大小,把pages从一个内存区域copy到另一片内存的。

    5.9 加快recovery

    MySQL 5.7.5之前,在recovery的过程中,需要扫描所有的ibd文件,获取元信息, 5.7.5之后,新加了一种redo log类型,即MLOG_FILE_NAME, 记录从上一次checkpoint以来,发生过变更的文件,这样在recovery的过程中,只需要打开这些文件就可以了。 [兼容性] 因为增加了新的log record type,需要安全的关闭5.7之前的实例,清理掉redo。

    5.10 表空间管理

    支持创建表空间,例如

    CREATE TABLESPACE `tablespace_name`
    ADD DATAFILE 'file_name.ibd'
    [FILE_BLOCK_SIZE = n]
    

    并可以在创建表的时候,指定属于哪个表空间,

    [兼容性] 因为可以任意指定空间目录,要注意升级过程中,不要漏掉目录。

    5.11 InnoDB Tablespace Encryption

    支持InnoDB数据文件加密,其依赖keyring plugin来进行秘钥的管理,后面我们单独来介绍InnoDB加密的方法,并且RDS也实现了一种InnoDB数据文件透明加密方法,并通过KMS系统来管理秘钥。例如:

    create table t(id int) encryption='y';






    ##########################################

    继上一期月报,MySQL5.7新特性之一介绍了一些新特性及兼容性问题后,本期继续进行学习。

    1. 系统变量

    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之后,默认这些表是不需要任何权限就可以访问了。

    2. sys schema

    新增了sys数据库,主要是performance_schema收集的信息,帮助DBA和开发人员方便诊断问题。 sys下的一共包括三种对象:1. view,2. procedure 3 function 这些对象都是基于performance_schema下的表,进行了可读性的聚合,没有真正存储数据,只存储了定义。

    [兼容性]
    mysql_install_db可以选择–skip-sys-schema跳过安装过程, 但默认mysql_upgrade会帮你创建sys下面的对象。不存在兼容性的问题

    3. 异常栈

    5.7开始支持异常诊断栈信息,通过GET STACKED DIAGNOSTICS可以获取栈内的信息。 具体的使用方法参考:https://dev.mysql.com/doc/refman/5.7/en/diagnostics-area.html

    4. Triggers

    支持在一个table对象上建多个trigger。

    5. Generated Columns

    5.7.6开始,支持生成列,这个列可以是虚拟的列,也可以是实体存储数据的列。 比如:

    CREATE TABLE triangle (
      sidea DOUBLE,
      sideb DOUBLE,
      sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb))
    );
    

    VIRTUAL: 表示这个字段是虚拟列,并不进行存储,查询的时候,通过计算得到
    STORED: 需要存储空间,并且可以被索引的列

    6. exchange partition不验证

    这个是在oracle分区表上支持的功能,dba在做大表维护的时候,非常有用。

    语法: ALTER TABLE ... EXCHANGE PARTITION WITHOUT VALIDATION 
    

    如果不验证,那么只有元数据信息的更改,就可以完成exchange,否则,就需要读取每一行数据进行验证,维护时间将根据这个表大小有关系。

    7. dump线程增强

    5.7.2之前,master dump线程需要持有LOCK_log锁去读取binlog然后发送到备库,而这时会阻塞client端去写入binlog。5.7.2之后,dump线程只需要持有LOCK_binlog_end_pos这个锁去读取binlog的当前的位置,来决定是否发送到备库去,这样就可以做到不阻塞任何binlog的写入。

    8. 多源复制

    多源复制可以从多个master复制到一个slave端,在数据库集群进行扩容和缩容的时候,非常有用。我们会在后面的系列单独来介绍。

    9. 在线更改replication master

    可以不用stop slave,然后在线更改replication master信息。 但这里并不是不需要slave停掉, 而是change master涉及到几个动作:

    1. 如果只是更改当前relay的信息,那么只需要sql线程是不工作的就可以了,IO thread可以继续
    2. 如果只是更改主库的信息,那么只需要IO线程不工作就可以了。 sql thread可以继续
    3. 如果需要重新启动主库和备库的恢复信息,比如master_auto_positioin=1,那么就需要IO和sql线程都停掉。

    10. Group Replication

    并行复制支持按照主库组提交的形式在备库进行回放。下一个系列进行单独来介绍

    下面单独介绍一下MySQL 5.7对临时表进行的改动。

    1. 背景

    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                                |
    

    2. 临时表空间

    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将共用这个临时表空间。

    3. 临时表优化

    临时表会伴随着大量的数据写入和读取,尤其是internal_tmp_table。所以,InnoDB专门对临时表进行了优化。
    InnoDB使用如下两个标示临时表:

    dict_tf2_temporary: 表示普通临时表  
    dict_tf2_intrinsic: 表示内部临时表  
    

    这两个标示,会在IBD文件的segment header占用两个bit位。intrinsic一定是temproary,也就是temproary上进行的优化 完全适用于intrinsic表上。

    下面来看下具体的优化:

    3.1. redo

    临时表在连接断开或者数据库实例关闭的时候,会进行删除,所以,临时表的数据不需要redo来保护,即recovery的过程中 不恢复临时表,只有临时表的metadata使用了redo保护,保护元数据的完整性,以便异常启动后进行清理工作。

    临时表的元数据,5.7之后,使用了一个独立的表进行保存,这样就不要使用redo保护,元数据也只保存在内存中。 但这有一个前提,必须使用共享的临时表空间,如果使用file-per-table,仍然需要持久化元数据,以便异常恢复清理。

    3.2 undo

    temporary table仍然需要语句级的回滚,所以,需要为数据生成undo。但intrinsic table不需要回滚,所以,intrinsic table 减少了undo的生成,性能更高。

    3.3 lock

    因为临时表只有本线程可以看见,所以减少了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功能。

    1. 背景

    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的管理:

    2. 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的空间管理简图如下:

    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记录了。

    3. undo段的分配

    undo段的分配比较简单,其过程如下:

    首先是rollback segment的分配:

    trx->rsegs.m_redo.rseg = trx_assign_rseg_low(
      srv_undo_logs, srv_undo_tablespaces,
      TRX_RSEG_TYPE_REDO);
    
    1. 使用round-robin的方式来分配rollback segment
    2. 如果有单独设置undo表空间,就不使用system表空间中的undo segment
    3. 如果设置的是truncate的就不分配
    4. 一旦分配了,就设置trx_ref_count,不允许truncate。

    具体代码参考:

    /******************************************************************//**
    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
    "
    	: "Creating update undo log segment
    ", 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);
    	}
    

    4. undo的truncate

    undo的truncate主要由下面两个参数控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。

    1. innodb_undo_log_truncate是开关参数。
    2. innodb_purge_rseg_truncate_frequency默认128,表示purge undo轮询128次后,进行一次undo的truncate。

    当设置innodb_undo_log_truncate=ON的时候, undo表空间的文件大小,如果超过了innodb_max_undo_log_size, 就会被truncate到初始大小,但有一个前提,就是表空间中的undo不再被使用。

    其主要步骤如下:

    1. 超过大小了之后,会被mark truncation,一次会选择一个
    2. 选择的undo不能再分配新给新的事务
    3. purge线程清理不再需要的rollback segment
    4. 等所有的回滚段都释放了后,truncate操作,使其成为install db时的初始状态。

    默认情况下, 是purge触发128次之后,进行一次rollback segment的free操作,然后如果全部free就进行一个truncate。

    但mark的操作需要几个依赖条件需要满足:

    1. 系统至少得有两个undo表空间,防止一个offline后,至少另外一个还能工作
    2. 除了ibdata里的segment,还至少有两个segment可用
    3. undo表空间的大小确实超过了设置的阈值

    其核心代码参考:

    /** 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的并行复制。

    1. 背景

    MySQL的master<->slave的部署结构,使用binlog日志保持数据的同步,全局有序的binlog在备库按照提交顺序进行回放。 由于新硬件的发展,SSD的引入和多core的CPU,master节点的并发处理能力持续提升,slave节点完全按照binlog写入顺序的单线程回放,已完全跟不上master节点的吞吐能力,导致HA切换和数据保护带来巨大的挑战。

    2. 并行复制的演进

    从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的实现方式:

    3. 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的过程中:

    1. 由leader线程帮助FLUSH队列中的线程完成flush binlog操作,
    2. 由leader线程帮助SYNC队列中的线程完成sync binlog操作,

    为了表示主库并行度,在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仍然对并行恢复起着非常大的作用。

    4. MySQL 5.7 并行复制参数

    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取值:

    1. DATABASE: 默认值,兼容5.6以schema维度的并行复制
    2. LOGICAL_CLOCK: MySQL 5.7基于组提交的并行复制机制

    5. MySQL 5.7 并行复制影响因素

    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      |
    +-----------------------------------------+--------+
    

    6. 一点建议

    1. 尽量使用row格式的binlog
    2. slave_parallel_workers 太多的线程会增加线程间同步的开销,建议4-8个slave线程,根据测试来最终确定。
    3. 如果客户端有并行度,不用刻意增加master的group commit,减少对主库的影响。

    另外: 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写入的顺序完全和主库一致,但可以缓解这种情况。




  • 相关阅读:
    Tomcat中有四种部署Web应用的方式
    解析Json和复合类型
    spring学习笔记001
    java环境变量
    如何下载JSTL
    servlet应用及知识点总结
    一文读懂微服务架构
    一个死锁的case
    如何在phpstorm中查看yaf框架源码
    Modify column Vs change column
  • 原文地址:https://www.cnblogs.com/igoodful/p/12151405.html
Copyright © 2020-2023  润新知