MySQL:MGR(单主)备节点MTS并发原理
最近遇到一个案例MGR备节点出现了hang死的问题,考虑到和备节点的并发有关,则学习了一下这部分。现记录如下。这里约定,
- last commit:代表gtid event中的last commit
- seq number:代表gtid event中的seq number
- gno:代表gtid uuid:1 中1,就是gno
一、SQL线程的并发原则
这部分实际上不管是在MGR还是在主从中都是基于last commit进行并发的,在主从中我们可以通过设置参数,
- transaction_write_set_extraction=XXHASH64 :设置writeset算法,一旦设置了这个参数则主库会计算事务中每行数据关于主键和唯一键的writeset(add_pke函数)。
- binlog_transaction_dependency_tracking=WRITESET:主库根据Writeset_history在哦commit_order算法的基础上继续下降last commit。
然后从库接收到这些基于WRITESET生成的event后,SQL线程会判断last commit来判断并发度。但是在MGR中确要复杂一些了,因为MGR中在传输event的时候主库的gtid event根本就没有生成,更别说生成last commit了,它传输的event也不是简单的读取binlog文件,下面我们来看看MGR中的备节点节点如何生成last commit的。
二、MGR中last commit/seq number和GTID序号(gno)的生成方式
这里直接跳过了各种gcs和xcom处理,当做了一个黑盒。
- 主库在group_replication_trans_before_commit 处建立一个gtid event,这个gtid event并没有包含任何有用的gtid信息。
- 主库在group_replication_trans_before_commit 将writeset封装到Transaction_context_log_event中。
- 连同事务产生的event一起交给gcs和xcom进行传输和协商
- 备库xcom收到协商好的事务信息后进行解析后通过进行pipeline处理
- applier 通道中,pipeline中的Certification_handler::handle_event进行处理填充认证数据库/进行验证/生成gtid event中的信息
- applier 通道中,pipeline中的Applier_handler::handle_event将事务的信息写入到relay log。
- applier 通道中,SQL线程进行分发和worker线程进行应用
那么这里我们需要重点关注的就是Certification_handler::handle_event中如何生成last commit和gno的。
- 首先Certification_handler::handle_event如果发现是Transaction_context_log_event,则进行缓存
- 如果Certification_handler::handle_event如果发现是Gtid_log_event,则调用Certification_handler::handle_transaction_id进行处理。
这个步骤首先调用Certifier::certify,其主要功能完成认证并且完成Gtid last commit和gno的生成,其中认证步骤为多主需要的这里不考虑。首先处理last commit,将其设置为冲突认证数据库中维护的最低值如下,
int64 transaction_last_committed = parallel_applier_last_committed_global
然后通过函数get_group_next_available_gtid分配关于group name的gno,这里Gtid信息就生成了,并生成snapshot_version,然后更新没有冲突的gtid信息。
接着就需要将这些writeset写入到冲突验证数据库,写入的时候同时完成last commit是否存在冲突的可能,也就是和Writeset_history完成功能一样,但是也有区别,因为这里的计算有60秒的时效性,后面在说。
如果在冲突验证数据库中不存在一样的值那么last commit就可以设置为parallel_applier_last_committed_global,否则是不能在备节点并发的,需要更新last commit,如下,
for (std::list<const char *>::iterator it = write_set->begin();
it != write_set->end(); ++it) { //循环整个writeset
int64 item_previous_sequence_number = -1;
add_item(*it, snapshot_version_value, &item_previous_sequence_number);//逐行加入,并且获取存在冲突的上一行的seq number到item_previous_sequence_number
/*
Exclude previous sequence number that are smaller than global
last committed and that are the current sequence number.
transaction_last_committed is initialized with
parallel_applier_last_committed_global on the beginning of
this method.
*/
if (item_previous_sequence_number > transaction_last_committed && //注意这里是大于才修改,也可能item_previous_sequence_number < transaction_last_committed ,什么时候小于后面再说
item_previous_sequence_number != parallel_applier_sequence_number)
transaction_last_committed = item_previous_sequence_number; //根据加入冲突验证数据的结果设置last commit }
- 备库提升parallel_applier_sequence_number计数器
如果是备库,就需要提升这个计数器(Certifier::certify的末尾),用于后面计算Gtid event的seq number 如下,并且保存计算好的last commit和seq number ,后面会用到。
if (!local_transaction) {
...
gle->last_committed = transaction_last_committed; //设置last commit
gle->sequence_number = parallel_applier_sequence_number;//设置seq number
increment_parallel_applier_sequence_number(
!has_write_set || update_parallel_applier_last_committed_global); //增加seq number计数器
- 接下来就要根据是否为本地事务还是备库事务进行分别处理了,这部分依旧在Certification_handler::handle_transaction_id函数中
这一步中如果是本地事务(主库),则唤醒本地提交,在这之前生成gno信息会保存在一个事务上下文中,在正式事务提交的时候的会用到这个gno信息,如下,
transaction_termination_ctx.m_generated_gtid = true;
transaction_termination_ctx.m_sidno = group_sidno;
transaction_termination_ctx.m_gno = seq_number; //设置了gtid 跳过了gtid 生成gno的步骤
if (set_transaction_ctx(transaction_termination_ctx))
...
当提交的时候这部分信息会重用如下,
MYSQL_BIN_LOG::assign_automatic_gtids_to_flush_group:
if (gtid_state->generate_automatic_gtid(
head,
head->get_transaction()->get_rpl_transaction_ctx()->get_sidno(),
head->get_transaction()->get_rpl_transaction_ctx()->get_gno(),//MGR这里的gno已经生成
&locked_sidno) != RETURN_STATUS_OK)
这里带入了MGR中生成的gno信息,因此主库提交的时候这个gno就是前面主库完成认证步骤后生成的GTID信息。但是last commit并没有保存,依旧是按照原有流程进行生成。
如果是远端事务(备库),那么就简单了因为seq number和last commit都生成了那么只要通过它们进行Gtid event的构建就好了如下,
/*
Remote transaction.
*/
if (seq_number > 0) {
const rpl_sid *sid = nullptr;
rpl_sidno sidno = group_sidno;
rpl_gno gno = seq_number; //认证中获取的Gtid信息
if (!tcle->is_gtid_specified()) {
// Create new GTID event.
Gtid gtid = {sidno, gno};
Gtid_specification gtid_specification = {ASSIGNED_GTID, gtid};//定义gtid已经分配
Gtid_log_event *gle_generated = new Gtid_log_event(
gle->server_id, gle->is_using_trans_cache(), gle->last_committed,//认证中获取的last commit信息
gle->sequence_number, gle->may_have_sbr_stmts,
gle->original_commit_timestamp, gle->immediate_commit_timestamp,
gtid_specification, gle->original_server_version,
gle->immediate_server_version);
- 接下备库就是执行next pipeline 写入到relay log,写入到relay log后applier通道的SQL线程就可以根据last commit进行并发了。
三、关于parallel_applier_last_committed_global的变更
这个变量直接影响到last commit的计算,前文已经提到。实际上这值的变更来自各个节点定期60秒,通过broadcast线程通过类型为Plugin_gcs_message::CT_CERTIFICATION_MESSAGE的消息,将各自节点已经执行的gtid 发送给其他节点。
if (broadcast_counter % broadcast_gtid_executed_period == 0)//60秒
broadcast_gtid_executed(); //广播GTID EXECUTED CT_CERTIFICATION_MESSAGE
当各个节点收到来自远端的Plugin_gcs_message::CT_CERTIFICATION_MESSAGE消息后(Certifier::handle_certifier_data),如果收集到全部节点的则会计算一个stable_gtid_set,也就是各个节点都执行过的事务集合,用于冲突验证数据的清理,Certifier::garbage_collect函数有如下:
while (it != certification_info.end()) { //清理冲突验证数据库,得到信息是 全部节点都已经提交的GTID,通过广播线程 广播
if (it->second->is_subset_not_equals(stable_gtid_set)) { //循环整个冲突认证数据库
if (it->second->unlink() == 0) delete it->second;
certification_info.erase(it++); //进行清理
} else
++it;
}
清理完成后会进行parallel_applier_last_committed_global的抬高,这个时候我们发现抬高视乎比较粗野,并没有考虑到冲突验证库,而是直接抬高到parallel_applier_sequence_number,前面我们说这个值就是gtid event中的gno,官方其实也知道因为有注释如下:
/*
We need to update parallel applier indexes since we do not know
what write sets were purged, which may cause transactions
last committed to be incorrectly computed.
*/
increment_parallel_applier_sequence_number(true);
从注释来看这是没有考虑到writeset的清理的,但是需要注意这是粗野大抬高,其影响的只能是降低MGR备库的并发,而不是提高,因为也没什么关系。
四、总结
从这里的流程不难看出,MGR中的last commit和seq number以及gno在主库和备库是不同的,其类似主从中基于writeset生成的last commit,但是又有区别,总结如下,
- 冲突验证数据库除了多主(当然没用过多主)下验证事务冲突以外,在单主下60秒内还充当了验证并发冲突的角色。
- 冲突验证数据库每60秒就会进行一次冲突验证数据库的清理,清理主要根据的是各个节点都执行过的事务,然后执行清理。
- MGR的GTID和普通的主从有本质的区别,为各个节点自己生成。
- 主库的last commit和seq number完全基于原有的原则生成,也就是如果binlog_transaction_dependency_tracking设置为writeset则是writeset的方式,如果是commit order则是commit order的方式。
- 主库的gno(GTID序号) 则和备节点一样,在MGR内部完成认证的时候生成。
- 备库的last commit和seq number则是在认证的时候生成,而last commit比较复杂,主要包含
- 60秒内根据冲突验证数据库进行验证生成,如果没有冲突则last commit设置一个最低值parallel_applier_last_committed_global。
- 每60秒parallel_applier_last_committed_global会被提升到一个当前认证完成事务最大的seq number也就是计数器parallel_applier_sequence_number-1,如果这个时候检测到冲突,因为冲突验证数据库的seq number必然小于parallel_applier_last_committed_global,则last commit保存不变。
- 备点的gno(GTID序号)和主库一样,在MGR内部完成认证的时候生成。
五、测试
这部分测试我们主要测试60秒抬升备库last commit的事实。我们制造一些没有冲突的事务,然后不断的在主库插入,同时堵塞备库执行事务,则这个时候有如下:
- 冲突验证数据库无法更新,因为备库没有执行事务
- 认证模块不断在往relay log中写binlog
- 因为没有冲突因此所有的事务的last commit都是parallel_applier_last_committed_global
- parallel_applier_last_committed_global每60秒会抬升到通过认证的事务的个数
那么我们理论上可以看到relay log中的gtid event last commit成片成片的更新。测试结果如下,截取交界处的部分:
image.png
这里貌似gtid event header的timestamp在relay log并不准确(图中10:22:58),稍微看了一下,relay log gtid event的head timestamp没有初始化,导致根据线程的时间来初始化,
time_t Log_event::get_time() {
/* Not previously initialized */
if (!common_header->when.tv_sec && !common_header->when.tv_usec) {
THD *tmp_thd = thd ? thd : current_thd;
if (tmp_thd)
common_header->when = tmp_thd->start_time; //线程的启动时间
那么这个线程是applier线程因此早就启动了,所以不准。