原文: http://mp.weixin.qq.com/s?__biz=MzAxNjAzMTQyMA==&mid=209773318&idx=1&sn=e9600d3db80ba3a3811a6e672d08aded&scene=1&srcid=10132stoFdOVIDasHsVOlGzR&key=2877d24f51fa53848e3b6d036cca266f7f31c08154ea4286ecc5cd73ea382806ef4ed0b4431d5b1c2e74d11b43a1815a&ascene=0&uin=Mjk1ODMyNTYyMg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.11+build(15A284)&version=11020201&pass_ticket=arNrQT6cCQuwOIGtmblNyqDr0Ft81AyT7cWa7HHUOK1lFPgxXHS%2Brs65tOnMLmzkphp
关于Group Commit网上的资料其实已经足够多了,我这里只简单的介绍一下。html
众所周知,在MySQL5.6以前的版本,因为引入了Binlog/InnoDB的XA,Binlog的写入和InnoDB commit彻底串行化执行,大概的执行序列以下:sql
InnoDB prepare (持有prepare_commit_mutex); write/sync Binlog; InnoDB commit (写入COMMIT标记后释放prepare_commit_mutex)。
当sync_binlog=1时,很明显上述的第二步会成为瓶颈,并且仍是持有全局大锁,这也是为何性能会急剧降低。数据库
很快Mariadb就提出了一个Binlog Group Commit方案,即在准备写入Binlog时,维持一个队列,最先进入队列的是leader,后来的是follower,leader为搜集到的队列中的线程依次写Binlog文件, 并commit事务。Percona 的Group Commit实现也是Port自Mariadb。不过仍在使用Percona Server5.5的朋友须要注意,该Group Commit实现可能破坏掉Semisync的行为,感兴趣的点击 bug#1254571性能优化
Oracle MySQL 在5.6版本开始也支持Binlog Group Commit,使用了和Mariadb相似的思路,但将Group Commit的过程拆分红了三个阶段:flush stage 将各个线程的binlog从cache写到文件中; sync stage 对binlog作fsync操做(若是须要的话);commit stage 为各个线程作引擎层的事务commit。每一个stage同时只有一个线程在操做。session
Tips:当引入Group Commit后,sync_binlog的含义就变了,假定设为1000,表示的不是1000个事务后作一次fsync,而是1000个事务组。多线程
Oracle MySQL的实现的优点在于三个阶段能够并发执行,从而提高效率。并发
XA Recoveroracle
在Binlog打开的状况下,MySQL默认使用MySQL_BIN_LOG来作XA协调者,大体流程为:app
1.扫描最后一个Binlog文件,提取其中的xid;
2.InnoDB维持了状态为Prepare的事务链表,将这些事务的xid和Binlog中记录的xid作比较,若是在Binlog中存在,则提交,不然回滚事务。
经过这种方式,可让InnoDB和Binlog中的事务状态保持一致。显然只要事务在InnoDB层完成了Prepare,而且写入了Binlog,就能够从崩溃中恢复事务,这意味着咱们无需在InnoDB commit时显式的write/fsync redo log。
Tips:MySQL为什么只须要扫描最后一个Binlog文件呢 ? 缘由是每次在rotate到新的Binlog文件时,老是保证没有正在提交的事务,而后fsync一次InnoDB的redo log。这样就能够保证老的Binlog文件中的事务在InnoDB老是提交的。
问题
其实问题很简单:每一个事务都要保证其Prepare的事务被write/fsync到redo log文件。尽管某个事务可能会帮助其余事务完成redo 写入,但这种行为是随机的,而且依然会产生明显的log_sys->mutex开销。
优化
从XA恢复的逻辑咱们能够知道,只要保证InnoDB Prepare的redo日志在写Binlog前完成write/sync便可。所以咱们对Group Commit的第一个stage的逻辑作了些许修改,大概描述以下:
Step1. InnoDB Prepare,记录当前的LSN到thd中;
Step2. 进入Group Commit的flush stage;Leader搜集队列,同时算出队列中最大的LSN。
Step3. 将InnoDB的redo log write/fsync到指定的LSN
Step4. 写Binlog并进行随后的工做(sync Binlog, InnoDB commit , etc)
经过延迟写redo log的方式,显式的为redo log作了一次组写入,并减小了log_sys->mutex的竞争。
目前官方MySQL已经根据咱们report的bug#73202锁提供的思路,对5.7.6的代码进行了优化,对应的Release Note以下:
When using InnoDB with binary logging enabled, concurrent transactions written in the InnoDB redo log are now grouped together before synchronizing to disk when innodb_flush_log_at_trx_commit is set to 1, which reduces the amount of synchronization operations. This can lead to improved performance.
性能数据
简单测试了下,使用sysbench, update_non_index.lua, 100张表,每张10w行记录,innodb_flush_log_at_trx_commit=2, sync_binlog=1000,关闭Gtid
并发线程 原生 修改后 32 25600 27000 64 30000 35000 128 33000 39000 256 29800 38000
背景
项目的快速迭代开发和在线业务须要保持持续可用的要求,致使MySQL的ddl变成了DBA很头疼的事情,并且常常致使故障发生。本篇介绍RDS分支上作的一个功能改进,DDL fast fail。主要解决:DDL操做由于没法获取MDL排它锁,进入等待队列的时候,阻塞了应用全部的读写请求问题。
MDL锁机制介绍
首先介绍一下MDL(METADATA LOCK)锁机制,MySQL为了保证表结构的完整性和一致性,对表的全部访问都须要得到相应级别的MDL锁,好比如下场景:
session 1: start transaction; select * from test.t1;
session 2: alter table test.t1 add extra int;
session 3: select * from test.t1;
1. session 1对t1表作查询,首先须要获取t1表的MDL_SHARED_READ级别MDL锁。锁一直持续到commit结束,而后释放。
2. session 2对t1表作DDL,须要获取t1表的MDL_EXCLUSIVE级别MDL锁,由于MDL_SHARED_READ与MDL_EXCLUSIVE不相容,因此session 2被session 1阻塞,而后进入等待队列。
3. session 3对t1表作查询,由于等待队列中有MDL_EXCLUSIVE级别MDL锁请求,因此session3也被阻塞,进入等待队列。
这种场景就是目前由于MDL锁致使的很经典的阻塞问题,若是session1长时间未提交,或者查询持续过长时间,那么后续对t1表的全部读写操做,都被阻塞。 对于在线的业务来讲,很容易致使业务中断。
aliyun RDS分支改进
DDL fast fail并无解决真正DDL过程当中的阻塞问题,但避免了由于DDL操做没有获取锁,进而致使业务其余查询/更新语句阻塞的问题。
其实现方式以下:
alter table test.t1 no_wait/wait 1 add extra int;
在ddl语句中,增长了no_wait/wait 1语法支持。
其处理逻辑以下:
首先尝试获取t1表的MDL_EXCLUSIVE级别的MDL锁:
1. 当语句指定的是no_wait,若是获取失败,客户端将获得报错信息:ERROR : Lock wait timeout exceeded; try restarting transaction。
2. 当语句指定的是wait 1,若是获取失败,最多等待1s,而后获得报错信息:ERROR : Lock wait timeout exceeded; try restarting transaction。
另外,除了alter语句之外,还支持rename,truncate,drop,optimize,create index等ddl操做。
与Oracle的比较
在Oracle 10g的时候,DDL操做常常会遇到这样的错误信息:
ora-00054:resource busy and acquire with nowait specified 即DDL操做没法获取表上面的排它锁,而fast fail。
其实DDL获取排他锁的设计,须要考虑的就是两个问题:
雪崩,若是你采用排队阻塞的机制,那么DDL若是长时间没法获取锁,就会致使应用的雪崩效应,对于高并发的业务,也是灾难。
饿死,若是你采用强制式的机制,那么要防止DDL一直没法获取锁的状况,在业务高峰期,可能DDL永远没法成功。
在Oracle 11g的时候,引入了DDL_LOCK_TIMEOUT参数,若是你设置了这个参数,那么DDL操做将使用排队阻塞模式,能够在session和global级别设置, 给了用户更多选择。
背景
MySQL从5.6版本开始支持GTID特性,也就是所谓全局事务ID,在整个复制拓扑结构内,每一个事务拥有本身全局惟一标识。GTID包含两个部分,一部分是实例的UUID,另外一部分是实例内递增的整数。
GTID的分配包含两种方式,一种是自动分配,另一种是显式设置session.gtid_next,下面简单介绍下这两种方式:
自动分配
若是没有设置session级别的变量gtid_next,全部事务都走自动分配逻辑。分配GTID发生在GROUP COMMIT的第一个阶段,也就是flush stage,大概能够描述为:
Step 1:事务过程当中,碰到第一条DML语句须要记录Binlog时,分配一段Gtid事件的cache,但不分配实际的GTID
Step 2:事务完成后,进入commit阶段,分配一个GTID并写入Step1预留的Gtid事件中,该GTID必须保证不在gtid_owned集合和gtid_executed集合中。 分配的GTID随后被加入到gtid_owned集合中。
Step 3:将Binlog 从线程cache中刷到Binlog文件中。
Step 4:将GTID加入到gtid_executed集合中。
Step 5:在完成sync stage 和commit stage后,各个会话将其使用的GTID从gtid_owned中移除。
显式设置
用户经过设置session级别变量gtid_next能够显式指定一个GTID,流程以下:
Step 1:设置变量gtid_next,指定的GTID被加入到gtid_owned集合中。
Step 2:执行任意事务SQL,在将binlog从线程cache刷到binlog文件后,将GTID加入到gtid_executed集合中。
Step 3:在完成事务COMMIT后,从gtid_owned中移除。
备库SQL线程使用的就是第二种方式,由于备库在apply主库的日志时,要保证GTID是一致的,SQL线程读取到GTID事件后,就根据其中记录的GTID来设置其gtid_next变量。
问题
因为在实例内,GTID须要保证惟一性,所以无论是操做gtid_executed集合和gtid_owned集合,仍是分配GTID,都须要加上一个大锁。咱们的优化主要集中在第一种GTID分配方式。
对于GTID的分配,因为处于Group Commit的第一个阶段,由该阶段的leader线程为其follower线程分配GTID及刷Binlog,所以不会产生竞争。
而在Step 5,各个线程在完成事务提交后,各自去从gtid_owned集合中删除其使用的gtid。这时候每一个线程都须要获取互斥锁,很显然,并发越高,这种竞争就越明显,咱们很容易从pt-pmp输出中看到以下相似的trace:
ha_commit_trans—>MySQL_BIN_LOG::commit—>MySQL_BIN_LOG::ordered_commit—>MySQL_BIN_LOG::finish_commit—>Gtid_state::update_owned_gtids_impl—>lock_sidno
这同时也会影响到GTID的分配阶段,致使TPS在高并发场景下的急剧降低。
解决
实际上对于自动分配GTID的场景,并无必要维护gtid_owned集合。咱们的修改也很是简单,在自动分配一个GTID后,直接加入到gtid_executed集合中,避免维护gtid_owned,这样事务提交时就无需去清理gtid_owned集合了,从而能够彻底避免锁竞争。
固然为了保证一致性,若是分配GTID后,写入Binlog文件失败,也须要从gtid_executed集合中删除。不过这种场景很是罕见。
性能数据
使用sysbench,100张表,每张10w行记录,update_non_index.lua,纯内存操做,innodb_flush_log_at_trx_commit = 2,sync_binlog = 1000
并发线程 原生 修改后 32 24500 25000 64 27900 29000 128 30800 31500 256 29700 32000 512 29300 31700 1024 27000 31000
从测试结果能够看到,优化前随着并发上升,性能出现降低,而优化后则能保持TPS稳定。
问题重现
先从问题入手,重现下这个 bug
use test; drop table if exists t1; create table t1(id int auto_increment, a int, primary key (id)) engine=innodb; insert into t1 values (1,2); insert into t1 values (null,2); insert into t1 values (null,2); select * from t1; +----+------+ | id | a | +----+------+ | 1 | 2 | | 2 | 2 | | 3 | 2 | +----+------+ delete from t1 where id=2; delete from t1 where id=3; select * from t1; +----+------+ | id | a | +----+------+ | 1 | 2 | +----+------+
这里咱们关闭MySQL,再启动MySQL,而后再插入一条数据
insert into t1 values (null,2); select * FROM T1; +----+------+ | id | a | +----+------+ | 1 | 2 | +----+------+ | 2 | 2 | +----+------+
咱们看到插入了(2,2),而若是我没有重启,插入一样数据咱们获得的应该是(4,2)。 上面的测试反映了MySQLd重启后,InnoDB存储引擎的表自增id可能出现重复利用的状况。
自增id重复利用在某些场景下会出现问题。依然用上面的例子,假设t1有个历史表t1_history用来存t1表的历史数据,那么MySQLd重启前,ti_history中可能已经有了(2,2)这条数据,而重启后咱们又插入了(2,2),当新插入的(2,2)迁移到历史表时,会违反主键约束。
缘由分析
InnoDB 自增列出现重复值的缘由:
MySQL> show create table t1\G; *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=innodb AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec)
建表时能够指定 AUTO_INCREMENT值,不指定时默认为1,这个值表示当前自增列的起始值大小,若是新插入的数据没有指定自增列的值,那么自增列的值即为这个起始值。对于InnoDB表,这个值没有持久到文件中。而是存在内存中(dict_table_struct.autoinc)。那么又问,既然这个值没有持久下来,为何咱们每次插入新的值后, show create table t1看到AUTO_INCREMENT值是跟随变化的。其实show create table t1是直接从dict_table_struct.autoinc取得的(ha_innobase::update_create_info)。
知道了AUTO_INCREMENT是实时存储内存中的。那么,MySQLd 重启后,从哪里获得AUTO_INCREMENT呢? 内存值确定是丢失了。实际上MySQL采用执行相似select max(id)+1 from t1;方法来获得AUTO_INCREMENT。而这种方法就是形成自增id重复的缘由。
MyISAM自增值
MyISAM也有这个问题吗?MyISAM是没有这个问题的。myisam会将这个值实时存储在.MYI文件中(mi_state_info_write)。MySQLd重起后会从.MYI中读取AUTO_INCREMENT值(mi_state_info_read)。所以,MyISAM表重启是不会出现自增id重复的问题。
问题修复
MyISAM选择将AUTO_INCREMENT实时存储在.MYI文件头部中。实际上.MYI头部还会实时存其余信息,也就是说写AUTO_INCREMENT只是个顺带的操做,其性能损耗能够忽略。InnoDB 表若是要解决这个问题,有两种方法。
1)将AUTO_INCREMENT最大值持久到frm文件中。
2)将 AUTO_INCREMENT最大值持久到汇集索引根页trx_id所在的位置。
第一种方法直接写文件性能消耗较大,这是一额外的操做,而不是一个顺带的操做。咱们采用第二种方案。为何选择存储在汇集索引根页页头trx_id,页头中存储trx_id,只对二级索引页和insert buf 页头有效(MVCC)。而汇集索引根页页头trx_id这个值是没有使用的,始终保持初始值0。正好这个位置8个字节可存放自增值的值。咱们每次更新AUTO_INCREMENT值时,同时将这个值修改到汇集索引根页页头trx_id的位置。 这个写操做跟真正的数据写操做同样,遵照write-ahead log原则,只不过这里只须要redo log ,而不须要undo log。由于咱们不须要回滚AUTO_INCREMENT的变化(即回滚后自增列值会保留,即便insert 回滚了,AUTO_INCREMENT值不会回滚)。
所以,AUTO_INCREMENT值存储在汇集索引根页trx_id所在的位置,其实是对内存根页的修改和多了一条redo log(量很小),而这个redo log 的写入也是异步的,能够说是原有事务log的一个顺带操做。所以AUTO_INCREMENT值存储在汇集索引根页这个性能损耗是极小的。
修复后的性能对比,咱们新增了全局参数innodb_autoinc_persistent 取值on/off; on 表示将AUTO_INCREMENT值实时存储在汇集索引根页。off则采用原有方式只存储在内存。
./bin/sysbench --test=sysbench/tests/db/insert.lua --MySQL-port=4001 --MySQL-user=root \--MySQL-table-engine=innodb --MySQL-db=sbtest --oltp-table-size=0 --oltp-tables-count=1 \--num-threads=100 --MySQL-socket=/u01/zy/sysbench/build5/run/MySQL.sock --max-time=7200 --max-requests run set global innodb_autoinc_persistent=off; tps: 22199 rt:2.25ms set global innodb_autoinc_persistent=on; tps: 22003 rt:2.27ms
能够看出性能损耗在%1如下。
改进
新增参数innodb_autoinc_persistent_interval 用于控制持久化AUTO_INCREMENT值的频率。例如:innodb_autoinc_persistent_interval=100,auto_incrememt_increment=1时,即每100次insert会控制持久化一次AUTO_INCREMENT值。每次持久的值为:当前值+innodb_autoinc_persistent_interval。
测试结论
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=1时性能损耗在%1如下。
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=100时性能损耗能够忽略。
限制
innodb_autoinc_persistent=on, innodb_autoinc_persistent_interval=N>1时,自增N次后持久化到汇集索引根页,每次持久的值为当前AUTO_INCREMENT+(N-1)*innodb_autoextend_increment。重启后读取持久化的AUTO_INCREMENT值会偏大,形成一些浪费但不会重复。innodb_autoinc_persistent_interval=1 每次都持久化没有这个问题。
若是innodb_autoinc_persistent=on,频繁设置auto_increment_increment的可能会致使持久化到汇集索引根页的值不许确。由于innodb_autoinc_persistent_interval计算没有考虑auto_increment_increment变化的状况,参看dict_table_autoinc_update_if_greater。而设置auto_increment_increment的状况极少,能够忽略。
注意:若是咱们使用须要开启innodb_autoinc_persistent,应该在参数文件中指定
innodb_autoinc_persistent= on
若是这样指定set global innodb_autoinc_persistent=on;重启后将不会从汇集索引根页读取AUTO_INCREMENT最大值。
疑问:对于InnoDB表,重启经过select max(id)+1 from t1获得AUTO_INCREMENT值,若是id上有索引那么这个语句使用索引查找就很快。那么,这个能够解释MySQL 为何要求自增列必须包含在索引中的缘由。 若是没有指定索引,则报以下错误,
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key 而myisam表居然也有这个要求,感受是多余的。
前言
与oracle 不一样,MySQL 的主库与备库的同步是经过 binlog 实现的,而redo日志只作为MySQL 实例的crash recovery使用。MySQL在4.x 的时候放弃redo 的同步策略而引入 binlog的同步,一个重要缘由是为了兼容其它非事务存储引擎,不然主备同步是没有办法进行的。
redo 日志同步属于物理同步方法,简单直接,将修改的物理部分传送到备库执行,主备共用一致的 LSN,只要保证 LSN 相同便可,同一时刻,只能主库或备库一方接受写请求; binlog的同步方法属于逻辑复制,分为statement 或 row 模式,其中statement记录的是SQL语句,Row 模式记录的是修改以前的记录与修改以后的记录,即前镜像与后镜像;备库经过binlog dump 协议拉取binlog,而后在备库执行。若是拉取的binlog是SQL语句,备库会走和主库相同的逻辑,若是是row 格式,则会调用存储引擎来执行相应的修改。
本文简单说明5.5到5.7的主备复制性能改进过程。
replication improvement (from 5.5 to 5.7)
(1) 5.5 中,binlog的同步是由两个线程执行的
io_thread: 根据binlog dump协议从主库拉取binlog, 并将binlog转存到本地的relaylog;
sql_thread: 读取relaylog,根据位点的前后顺序执行binlog event,进而将主库的修改同步到备库,达到主备一致的效果; 因为在主库的更新是由多个客户端执行的,因此当压力达到必定的程度时,备库单线程执行主库的binlog跟不上主库执行的速度,进而会产生延迟形成备库不可用,这也是分库的缘由之一,其SQL线程的执行堆栈以下:
sql_thread: exec_relay_log_event apply_event_and_update_pos apply_event rows_log_event::apply_event storage_engine operation update_pos
(2) 5.6 中,引入了多线程模式,在多线程模式下,其线程结构以下
io_thread: 同5.5
Coordinator_thread: 负责读取 relay log,将读取的binlog event以事务为单位分发到各个 worker thread 进行执行,并在必要时执行binlog event(Description_format_log_event, Rotate_log_event 等)。
worker_thread: 执行分配到的binlog event,各个线程之间互不影响;
多线程原理
sql_thread 的分发原理是依据当前事务所操做的数据库名称来进行分发,若是事务是跨数据库行为的,则须要等待已分配的该数据库的事务所有执行完毕,才会继续分发,其分配行为的伪码能够简单的描述以下:
get_slave_worker if (contains_partition_info(log_event)) db_name= get_db_name(log_event); entry {db_name, worker_thread, usage} = map_db_to_worker(db_name); while (entry->usage > 0) wait(); return worker; else if (last_assigned_worker) return last_assigned_worker; else push into buffer_array and deliver them until come across a event that have partition info
须要注意的细节
内存的分配与释放。relay thread 每读取一个log_event, 则须要 malloc 必定的内存,在work线程执行完后,则须要free掉;
数据库名 与 worker 线程的绑定信息在一个hash表中进行维护,hash表以entry为单位,entry中记录当前entry所表明的数据库名,有多少个事务相关的已被分发,执行这些事务的worker thread等信息;
维护一个绑定信息的array , 在分发事务的时候,更新绑定信息,增长相应 entry->usage, 在执行完一个事务的时候,则须要减小相应的entry->usage;
slave worker 信息的维护,即每一个 worker thread执行了哪些事务,执行到的位点是在哪,延迟是如何计算的,若是执行出错,mts_recovery_group 又是如何恢复的;
分配线程是以数据库名进行分发的,当一个实例中只有一个数据库的时候,不会对性能有提升,相反,因为增长额外的操做,性能还会有一点回退;
临时表的处理,临时表是和entry绑定在一块儿的,在执行的时候将entry的临时表挂在执行线程thd下面,但没有固化,若是在临时表操做期间,备库crash,则重启后备库会有错误;
整体上说,5.6 的并行复制打破了5.5 单线程的复制的行为,只是在单库下用处不大,而且5.6的并行复制的改动引入了一些重量级的bug
MySQL slave sql thread memory leak (http://bugs.MySQL.com/bug.php?id=71197)
Relay log without xid_log_event may case parallel replication hang (http://bugs.MySQL.com/bug.php?id=72794)
Transaction lost when relay_log_info_repository=FILE and crashed (http://bugs.MySQL.com/bug.php?id=73482)
(3) 5.7中,并行复制的实现添加了另一种并行的方式,即主库在 ordered_commit中的第二阶段的时候,将同一批commit的 binlog 打上一个相同的seqno标签,同一时间戳的事务在备库是能够同时执行的,所以大大简化了并行复制的逻辑,并打破了相同 DB 不能并行执行的限制。备库在执行时,具备同一seqno的事务在备库能够并行的执行,互不干扰,也不须要绑定信息,后一批seqno的事务须要等待前一批相同seqno的事务执行完后才能够执行。
详细实现可参考: http://bazaar.launchpad.net/~MySQL/MySQL-server/5.7/revision/6256 。
reference: http://geek.rohitkalhans.com/2013/09/enhancedMTS-deepdive.html