关于Group Commit网上的资料其实已经足够多了,我这里只简单的介绍一下。 php
众所周知,在MySQL5.6以前的版本,因为引入了Binlog/InnoDB的XA,Binlog的写入和InnoDB commit彻底串行化执行,大概的执行序列以下: html
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 mysql
Oracle MySQL 在5.6版本开始也支持Binlog Group Commit,使用了和Mariadb相似的思路,但将Group Commit的过程拆分红了三个阶段:flush stage 将各个线程的binlog从cache写到文件中; sync stage 对binlog作fsync操做(若是须要的话);commit stage 为各个线程作引擎层的事务commit。每一个stage同时只有一个线程在操做。 算法
Tips:当引入Group Commit后,sync_binlog的含义就变了,假定设为1000,表示的不是1000个事务后作一次fsync,而是1000个事务组。 sql
Oracle MySQL的实现的优点在于三个阶段能够并发执行,从而提高效率。 数据库
XA Recover 缓存
在Binlog打开的状况下,MySQL默认使用MySQL_BIN_LOG来作XA协调者,大体流程为: 性能优化
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老是提交的。 session
问题
其实问题很简单:每一个事务都要保证其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;
这种场景就是目前由于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锁:
另外,除了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获取排他锁的设计,须要考虑的就是两个问题:
在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,大概能够描述为:
显式设置
用户经过设置session级别变量gtid_next能够显式指定一个GTID,流程以下:
备库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,应该在参数文件中指定
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
须要注意的细节
整体上说,5.6 的并行复制打破了5.5 单线程的复制的行为,只是在单库下用处不大,而且5.6的并行复制的改动引入了一些重量级的bug
(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
本文说明一个物理升级致使的 "数据丢失"。
现象
在MySQL 5.1下新建key分表,能够正确查询数据。
drop table t1; create table t1 (c1 int , c2 int) PARTITION BY KEY (c2) partitions 5; insert into t1 values(1,1785089517),(2,null); MySQL> select * from t1 where c2=1785089517; +------+------------+ | c1 | c2 | +------+------------+ | 1 | 1785089517 | +------+------------+ 1 row in set (0.00 sec) MySQL> select * from t1 where c2 is null; +------+------+ | c1 | c2 | +------+------+ | 2 | NULL | +------+------+ 1 row in set (0.00 sec)
而直接用MySQL5.5或MySQL5.6启动上面的5.1实例,发现(1,1785089517)这行数据不能正确查询出来。
alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2) partitions 5; MySQL> select * from t1 where c2 is null; +------+------+ | c1 | c2 | +------+------+ | 2 | NULL | +------+------+ 1 row in set (0.00 sec) MySQL> select * from t1 where c2=1785089517; Empty set (0.00 sec)
缘由分析
跟踪代码发现,5.1 与5.5,5.6 key hash算法是有区别的。
5.1 对于非空值的处理算法以下
void my_hash_sort_bin(const CHARSET_INFO *cs __attribute__((unused)), const uchar *key, size_t len,ulong *nr1, ulong *nr2) { const uchar *pos = key; key+= len; for (; pos < (uchar*) key ; pos++) { nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * ((uint)*pos)) + (nr1[0] << 8); nr2[0]+=3; } }
经过此算法算出数据(1,1785089517)在第3个分区
5.5和5.6非空值的处理算法以下
void my_hash_sort_simple(const CHARSET_INFO *cs, const uchar *key, size_t len, ulong *nr1, ulong *nr2) { register uchar *sort_order=cs->sort_order; const uchar *end; /* Remove end space. We have to do this to be able to compare 'A ' and 'A' as identical */ end= skip_trailing_space(key, len); for (; key < (uchar*) end ; key++) { nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * ((uint) sort_order[(uint) *key])) + (nr1[0] << 8); nr2[0]+=3; } }
经过此算法算出数据(1,1785089517)在第5个分区,所以,5.5,5.6查询不能查询出此行数据。
5.1,5.5,5.6对于空值的算法仍是一致的,以下
if (field->is_null()) { nr1^= (nr1 << 1) | 1; continue; }
都能正确算出数据(2, null)在第3个分区。所以,空值能够正确查询出来。
那么是什么致使非空值的hash算法走了不一样路径呢?在5.1下,计算字段key hash固定字符集就是my_charset_bin,对应的hash 函数就是前面的my_hash_sort_simple。而在5.5,5.6下,计算字段key hash的字符集是随字段变化的,字段c2类型为int对应my_charset_numeric,与之对应的hash函数为my_hash_sort_simple。具体能够参考函数Field::hash
那么问题又来了,5.5后为何算法会变化呢?缘由在于官方关于字符集策略的调整,详见WL#2649 。
兼容处理
前面讲到,因为hash 算法变化,用5.5,5.6启动5.1的实例,致使不能正确查询数据。那么5.1升级5.5,5.6就必须兼容这个问题.MySQL 5.5.31之后,提供了专门的语法 ALTER TABLE ... PARTITION BY ALGORITHM=1 [LINEAR] KEY ... 用于兼容此问题。对于上面的例子,用5.5或5.6启动5.1的实例后执行
MySQL> alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2) partitions 5; Query OK, 2 rows affected (0.02 sec) Records: 2 Duplicates: 0 Warnings: 0
MySQL> select * from t1 where c2=1785089517; +------+------------+ | c1 | c2 | +------+------------+ | 1 | 1785089517 | +------+------------+ 1 row in set (0.00 sec)
数据能够正确查询出来了。
而实际上5.5,5.6的MySQL_upgrade升级程序已经提供了兼容方法。MySQL_upgrade 执行check table xxx for upgrade 会检查key分区表是否用了老的算法。若是使用了老的算法,会返回
MySQL> CHECK TABLE t1 FOR UPGRADE\G *************************** 1. row *************************** Table: test.t1 Op: check Msg_type: error Msg_text: KEY () partitioning changed, please run: ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2) PARTITIONS 5 *************************** 2. row *************************** Table: test.t1 Op: check Msg_type: status Msg_text: Operation failed 2 rows in set (0.00 sec)
检查到错误信息后会自动执行如下语句进行兼容。
ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2) PARTITIONS 5。
背景
客户使用MySQLdump导出一张表,而后使用MySQL -e 'source test.dmp'的过程当中client进程crash,爆出内存的segment fault错误,致使没法导入数据。
问题定位
test.dmp文件大概50G左右,查看了一下文件的前几行内容,发现:
A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database If you don't want to restore GTIDs pass set-gtid-purged=OFF. To make a complete dump, pass... -- MySQL dump 10.13 Distrib 5.6.16, for Linux (x86_64) -- -- Host: 127.0.0.1 Database: carpath -- ------------------------------------------------------ -- Server version 5.6.16-log /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
问题定位到第一行出现了不正常warning的信息,是因为客户使用MySQLdump命令的时候,重定向了stderr。即:
MySQLdump ...>/test.dmp 2>&1
致使error或者warning信息都重定向到了test.dmp, 最终致使失败。
问题引伸
问题虽然定位到了,但却有几个问题没有弄清楚:
问题1. 不正常的sql,执行失败,报错出来就能够了,为何会致使crash?
MySQL.cc::add_line函数中,在读第一行的时候,读取到了don't,发现有一个单引号,因此程序死命的去找匹配的另一个单引号,致使不断的读取文件,分配内存,直到crash。
假设没有这个单引号,MySQL读到第六行,发现;号,就会执行sql,并正常的报错退出。
问题2. 那代码中对于大小的边界究竟是多少?好比insert语句支持batch insert时,语句的长度多少,又好比遇到clob字段呢?
因此,正常状况下,max_allowed_packet如今的最大字段长度和MAX_BATCH_BUFFER_SIZE限制的最大insert语句,是匹配的。
RDS问题修复原则
从问题的定位上来看,这一例crash属于客户错误使用MySQLdump致使的问题,Aliyun RDS分支对内存致使的crash问题,都会定位并反馈给用户。 但此例不作修复,而是引导用户正确的使用MySQLdump工具。
bug描述
Oracle 最新发布的版本 5.6.22 中有这样一个关于GTID的bugfix,在主备场景下,若是咱们在主库上 SET GLOBAL GTID_PURGED = "some_gtid_set",而且 some_gtid_set 中包含了备库还没复制的事务,这个时候若是备库接上主库的话,预期结果是主库返回错误,IO线程挂掉的,可是实际上,在这种场景下主库并不报错,只是默默的把本身 binlog 中包含的gtid事务发给备库。这个bug的形成的结果是看起来复制正常,没有错误,但实际上备库已经丢事务了,主备极可能就不一致了。
背景知识
binlog 中记录的和GTID相关的事件主要有2种,Previous_gtids_log_event 和 Gtid_log_event,前者表示以前的binlog中包含的gtid的集合,后者就是一个gtid,对应一个事务。一个 binlog 文件中只有一个 Previous_gtids_log_event,放在开头,有多个 Gtid_log_event,以下面所示
Previous_gtids_log_event // 此 binlog 以前的全部binlog文件包含的gtid集合 Gtid_log_event // 单个gtid event Transaction Gtid_log_event Transaction . . . Gtid_log_event Transaction
咱们知道备库的复制线程是分IO线程和SQL线程2种的,IO线程经过GTID协议或者文件位置协议拉取主库的binlog,而后记录在本身的relay log中;SQL线程经过执行realy log中的事件,把其中的操做都本身作一遍,记入本地binlog。在GTID协议下,备库向主库发送拉取请求的时候,会告知主库本身已经有的全部的GTID的集合,Retrieved_Gtid_Set + Executed_Gtid_Set,前者对应 realy log 中全部的gtid集合,表示已经拉取过的,后者对应binlog中记录有的,表示已经执行过的;主库在收到这2个总集合后,会扫描本身的binlog,找到合适的binlog而后开始发送。
主库将备库发送过来的总合集记为 slave_gtid_executed,而后调用 find_first_log_not_in_gtid_set(slave_gtid_executed),这个函数的目的是从最新到最老扫描binlog文件,找到第一个含有不存在 slave_gtid_executed 这个集合的gtid的binlog。在这个扫描过程当中并不须要从头至尾读binlog中全部的gtid,只须要读出 Previous_gtids_log_event ,若是Previous_gtids_log_event 不是 slave_gtid_executed的子集,就继续向前找binlog,直到找到为止。
这个查找过程总会中止的,中止条件以下:
- 找到了这样的binlog,其Previous_gtids_log_event 是slave_gtid_executed子集
- 在往前读binlog的时候,发现没有binlog文件了(如被purge了),可是还没找到知足条件的Previous_gtids_log_event,这个时候主库报错
- 一直往前找,发现Previous_gtids_log_event 是空集
在条件2下,报错信息是这样的
Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires.
其实上面的条件3是条件1的特殊状况,这个bugfix针对的场景就是条件3这种,但并非全部的符合条件3的场景都会触发这个bug,下面就分析下什么状况下才会触发bug。
bug 分析
假设有这样的场景,咱们要用已经有MySQL实例的备份从新作一对主备实例,无论是用 xtrabackup 这种物理备份工具或者MySQLdump这种逻辑备份工具,都会有2步操做,
- 导入数据
- SET GLOBAL GTID_PURGED ="xxxx"
步骤2是为了保证GTID的完备性,由于新实例已经导入了数据,就须要把生成这些数据的事务对应的GTID集合也设置进来。
正常的操做是主备都要作这2步的,若是咱们只在主库上作了这2步,备库什么也不作,而后就直接用 GTID 协议把备库连上来,按照咱们的预期这个时候是应该出错的,主备不一致,而且主库的binlog中没东西,应该报以前中止条件2报的错。可是使人大跌眼镜的是主库不报错,复制看起来是彻底正常的。
为啥会这样呢,SET GLOBAL GTID_PURGED 操做会调用 MySQL_bin_log.rotate_and_purge切换到一个新的binlog,并把这个GTID_PURGED 集合记入新生成的binlog的Previous_gtids_log_event,假设原有的binlog为A,新生成的为B,主库刚启动,因此A就是主库的第一个binlog,它以前啥也没有,A的Previous_gtids_log_event就是空集,而且A中也不包含任何GTID事件,不然SET GLOBAL GTID_PURGED是作不了的。按照以前的扫描逻辑,扫到A是确定会停下来的,而且不报错。
bug 修复
官方的修复就是在主库扫描查找binlog以前,判断一下 gtid_purged 集合不是不比slave_gtid_executed大,若是是就报错,错误信息和条件2同样 Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires。
问题描述
当单个 MySQL 实例的数据增加到不少的时候,就会考虑经过库或者表级别的拆分,把当前实例的数据分散到多个实例上去,假设原实例为A,想把其中的5个库(db1/db2/db3/db4/db5)拆分到5个实例(B1/B2/B3/B4/B5)上去。
拆分过程通常会这样作,先把A的相应库的数据导出,而后导入到对应的B实例上,可是在这个导出导入过程当中,A库的数据仍是在持续更新的,因此还需在导入完后,在全部的B实例和A实例间创建复制关系,拉取缺失的数据,在业务不繁忙的时候将业务切换到各个B实例。
在复制搭建时,每一个B实例只须要复制A实例上的一个库,因此只须要重放对应库的binlog便可,这个经过 replicate-do-db 来设置过滤条件。若是咱们用备库上执行 show slave status\G 会看到Executed_Gtid_Set是断断续续的,间断很是多,致使这一列很长很长,看到的直接效果就是被刷屏了。
为啥会这样呢,由于设了replicate-do-db,就只会执行对应db对应的event,其它db的都不执行。主库的执行是不分db的,对各个db的操做互相间隔,记录在binlog中,因此备库作了过滤后,就出现这种断断的现象。
除了这个看着不舒服外,还会致使其它问题么?
假设咱们拿B1实例的备份作了一个新实例,而后接到A上,若是主库A又按期purge了老的binlog,那么新实例的IO线程就会出错,由于须要的binlog在主库上找不到了;即便主库没有purge 老的binlog,新实例还要把主库的binlog都从头从新拉过来,而后执行的时候又都过滤掉,不如不拉取。
有没有好的办法解决这个问题呢?SQL线程在执行的时候,发现是该被过滤掉的event,在不执行的同时,记一个空事务就行了,把原事务对应的GTID位置占住,记入binlog,这样备库的Executed_Gtid_Set就是连续的了。
bug 修复
对这个问题,官方有一个相应的bugfix,参见 revno: 5860 ,有了这个patch后,备库B1的 SQL 线程在遇到和 db2-db5 相关的SQL语句时,在binlog中把对应的GTID记下,同时对应记一个空事务。
这个 patch 只是针对Query_log_event,即 statement 格式的 binlog event,那么row格式的呢? row格式原来就已是这种行为,经过check_table_map 函数来过滤库或者表,而后生成一个空事务。
另外这个patch还专门处理了下 CREATE/DROP TEMPORARY TABLE 这2种语句,咱们知道row格式下,对临时表的操做是不会记入binlog的。若是主库的binlog格式是 statement,备库用的是 row,CREATE/DROP TEMPORARY TABLE 对应的事务传到备库后,就会消失掉,Executed_Gtid_Set集合看起来是不连续的,可是主库的binlog记的gtid是连续的,这个 patch 让这种状况下的CREATE/DROP TEMPORARY TABLE在备库一样记为一个空事务。
来自一个TokuDB用户的“投诉”:
https://mariadb.atlassian.net/browse/MDEV-6207
现象大概是:
用户有一个MyISAM的表test_table:
CREATE TABLE IF NOT EXISTS `test_table` ( `id` int(10) unsigned NOT NULL, `pub_key` varchar(80) NOT NULL, PRIMARY KEY (`id`), KEY `pub_key` (`pub_key`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
转成TokuDB引擎后表大小为92M左右:
47M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 45M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
执行"OPTIMIZE TABLE test_table":
63M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
再次执行"OPTIMIZE TABLE test_table":
79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
继续执行:
79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
基本稳定在这个大小。
主索引从47M-->63M-->79M,执行"OPTIMIZE TABLE"后为何会愈来愈大?
这得从TokuDB的索引文件分配方式提及,当内存中的脏页须要写到磁盘时,TokuDB优先在文件末尾分配空间并写入,而不是“覆写”原块,原来的块暂时成了“碎片”。
这样问题就来了,索引文件岂不是愈来愈大?No, TokuDB会把这些“碎片”在checkpoint时加入到回收列表,以供后面的写操做使用,看似79M的文件其实还能够装很多数据呢!
嗯,这个现象解释通了,但还有2个问题:
- 在执行这个语句的时候,TokuDB到底在作什么呢? 在作toku_ft_flush_some_child,把内节点的缓冲区(message buffer)数据刷到最底层的叶节点。
- 在TokuDB里,OPTIMIZE TABLE有用吗? 做用很是小,不建议使用,TokuDB是一个"No Fragmentation"的引擎。
本文转载自MySQL.taobao.org ,感谢淘宝数据库项目组丁奇、鸣嵩、彭立勋、皓庭、项仲、剑川、武藏、祁奚、褚霸、一工。审校:刘亚琼