背景

    MySQL从5.6版本开始支持GTID特性,也就是所谓全局事务ID,在整个复制拓扑结构内,每个事务拥有自己全局唯一标识。GTID包含两个部分,一部分是实例的UUID,另一部分是实例内递增的整数。

    GTID的分配包含两种方式,一种是自动分配,另外一种是显式设置session.gtid_next,下面简单介绍下这两种方式:

    自动分配

    如果没有设置session级别的变量gtid_next,所有事务都走自动分配逻辑。分配GTID发生在GROUP COMMIT的第一个阶段,也就是flush stage,大概可以描述为:

    1. Step 1:事务过程中,碰到第一条DML语句需要记录Binlog时,分配一段Gtid事件的cache,但不分配实际的GTID
    2. Step 2:事务完成后,进入commit阶段,分配一个GTID并写入Step1预留的Gtid事件中,该GTID必须保证不在gtid_owned集合和gtid_executed集合中。 分配的GTID随后被加入到gtid_owned集合中。
    3. Step 3:将Binlog 从线程cache中刷到Binlog文件中。
    4. Step 4:将GTID加入到gtid_executed集合中。
    5. Step 5:在完成sync stage commit stage后,各个会话将其使用的GTIDgtid_owned中移除。

    显式设置

    用户通过设置session级别变量gtid_next可以显式指定一个GTID,流程如下:

    1. Step 1:设置变量gtid_next,指定的GTID被加入到gtid_owned集合中。
    2. Step 2:执行任意事务SQL,在将binlog从线程cache刷到binlog文件后,将GTID加入到gtid_executed集合中。
    3. 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:

    1. 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

    1. 并发线程 原生 修改后
    2. 32 24500 25000
    3. 64 27900 29000
    4. 128 30800 31500
    5. 256 29700 32000
    6. 512 29300 31700
    7. 1024 27000 31000

    从测试结果可以看到,优化前随着并发上升,性能出现下降,而优化后则能保持TPS稳定。