mysql 主从线程 源码_金融级MySQL切换卡主源码剖析(四)

本文分析了在金融级MySQL数据库主从切换时出现的线程堵塞问题,通过火焰图深入剖析了Binlog_dump线程中互斥锁争用导致的性能瓶颈。重点在于LOCK_plugin互斥锁的获取和释放,以及pthread_mutex_lock和pthread_mutex_unlock的不均衡时间消耗。通过分析和优化,将从库挂载时间从60s缩短至14s。
摘要由CSDN通过智能技术生成

背景

如上图所示,同事在某领先的自研高可用架构数据库平台中执行数据库的主从计划内切换,发生线程堵塞,持续两分钟左右。时间虽然不算长,但对于金融类DB来说,依然极为敏感。

分析

上一节,咱们已经得到了测试现场的火焰图。然而,并没有去仔细去分析,只是粗糙地去印证了几个热点函数调用。这一节,咱们将仔细分析和对比,来进一步确认优化计划。火焰图分析

从下往上看,依次是函数从外到里的调用关系。颜色长度代表着perf在进行采样统计时,该函数出现的频率。首先从最底层的mysqld看起,

当鼠标移动到mysqld一栏,可以看到底部的提示,"9,871 samples, 100%"。一步步往上走,到Binlog_sender::send_events函数,再往上,主要是三个函数:Binlog_transmit_delegate::after_send_event, 2469 samples, 25.01%获取LOCK_plugin互斥锁,进入钩子函数;因为是从库已经返回的Binlog坐标,几乎是直接是退出函数了Binlog_transmit_delegate::before_send_event, 2481 samples, 25.83%获取LOCK_plugin互斥锁,进入钩子函数;获取LOCK_binlog_互斥锁,对比当前读取的Binlog坐标与从库已返回的Binlog坐标,相等返回0,大于返回1,否则返回-1,很显然,此时一定会小于0;既然如此立即释放LOCK_binlog_互斥锁Binlog_transmit_delegate::reserve_header, 2522 samples, 25.55%获取LOCK_plugin互斥锁,进入钩子函数;只扩展包头部内存

这三个函数,依次对应第二节中的三个红色矩形展示的钩子函数(after_send_hook钩子函数、before_send_hook钩子函数以及reserve_header钩子函数)。从这里,可以清楚地看到,有75%左右的时间都是消耗在这里面;而且尽管三个函数在获取LOCK_plugin互斥锁之后,reserve_header钩子函数与after_send_hook几乎是很快就退出了,但是总的消耗时间没有差别;很显然,CPU时间主要消耗在了上层的调用层,

#define FOREACH_OBSERVER(r, f, thd, args) \ \Prealloced_array plugins(PSI_NOT_INSTRUMENTED); \read_lock(); \Observer_info_iterator iter= observer_info_iter(); \Observer_info *info= iter++; \for (; info; info= iter++) \{ \plugin_ref plugin= \my_plugin_lock(0, &info->plugin); \if (!plugin) \{ \r= 0; \break; \} \plugins.push_back(plugin); \if (((Observer *)info->observer)->f \&& ((Observer *)info->observer)->f args) \{ \r= 1; \sql_print_error("Run function '" #f "' in plugin '%s' failed", \info->plugin_int->name.str); \break; \} \} \unlock(); \if (!plugins.empty()) \plugin_unlock_list(0, &plugins[0], plugins.size());

再继续往上看,在每个函数中,12%左右的时间消耗在pthread_mutex_lock函数

4%左右的时间消耗在pthread_mutex_unlock函数

2%左右的时间消耗在plugin_unlock_list函数

1.5%左右的时间消耗在plugin_lock函数

看过代码后,应该可以认为,这四者,是一回事,都对应于LOCK_plugin互斥锁。在每个函数25%的CPU时间内,有20%的CPU时间消耗在此。其次,pthread_mutex_lock与pthread_mutex_unlock在每个函数中的比例都是有规律不匹配,即在每个函数中pthread_mutex_lock的比例都是12%左右,而pthread_mutex_unlock的比例都是4%左右,互斥锁的获取时间是释放时间的3倍。在笔者看来,这就是证据!可以证明在获取互斥锁的时候,似乎睡眠了,正常情况下应该是获取锁的时间消耗和释放锁的时间消耗是相同的。为此,继续做进一步验证,验证在一个半同步备机的情况下,获取锁的时间消耗和释放锁的时间消耗是否近似相等。理论上,只要两者相等,就可以证明,主要的性能问题在于互斥锁出现了争用,获取锁的时间消耗非线性增长。一个半同步从库重启复制时抓取的火焰图如下:

进一步分析如下:

从图可以发现,pthread_mutex_lock与pthread_mutex_unlock相当,都是1.8%左右。分析到此,足以说明,互斥锁争用,导致了耗时非线性增长。

结论:CPU时间主要消耗在了LOCK_plugin互斥锁,原因在于互斥锁争用,导致了耗时的非线性增长。

笔者猜测,在多核处理器的情况下,是否可以把互斥锁改为自旋锁性能更好?前提在于Binlog dump线程数一般少于CPU核数且普通线程不会去竞争LOCK_plugin互斥锁。代码分析

上一节,已经提到过,为了尽可能减少每次循环的时间,完全可以在Binlog Dump读取到真正需要发送给从库的event前,跳过半同步的逻辑。因为,在笔者与同事的讨论结果看来,Binlog Dump线程在读取event,对比从库GTID以确认event的发送起点position时,与半同步并无任何关系。合理的理由是,为了维持当前的半同步插件动态加载机制实现模式,否则将大大增加代码维护成本。因而,在此背景下,笔者开始分析代码,并进行优化。

分析方式与之前类似,首先去分析Binlog Dump线程的debug日志,一层一层地去剖析。从主干函数,一层一层剥开,再结合第二节已经提到的Binlog_sender::run()函数,很快就可以把大致逻辑梳理一遍。梳理第二节Binlog_sender::run()中的init()语句,即Binlog_sender的初始化函数

// 调用方式如下:// Binlog_sender sender(thd, log_ident, pos, slave_gtid_executed, flags);// sender.run();// 其中log_ident为master_log_file,pos为master_log_position,如果slave_gtid_executed不为空,m_using_gtid_protocol为true

Binlog_sender::init() // Binlog Dump线程的初始化函数>>check_start_file()

>>>>if (m_start_file[0] != '\0') // 如果master_log_file不为空>>>>>>mysql_bin_log.make_log_name(index_entry_name, m_start_file) // 根据master_log_file生成Binlog的绝对路径文件名>>>>>>name_ptr= index_entry_name // 内容地址赋值给name_ptr>>>>else if (m_using_gtid_protocol) // 如果slave_gtid_executed不为空>>>>>>Sid_map* slave_sid_map= m_exclude_gtid->get_sid_map()

// MySQL启动时,会把auto.cnf中server-uuid字符串转换成字节数组存放在rpl_sid中,删除该文件时会重新生成// rpl_sidno为一个从1开始累加的整数值,每重新生成一次UUID就会累加一次// 每个Gtid_set中都包含一个Sid_map,这个Sid_map是全局的,它保存着rpl_sidno与rpl_sid的对应关系// Sid_map保存着哈希查找方式,rpl_sid(UUID)为key,rpl_sidno(整数)为value// 如:{sidno, gno} = {1, 1},{sid, gno} = {caf36128-99a4-11e9-b1c9-0242ac110002, 1},第二种表示就是Binlog常见的方式// sid即UUID,必须保证主从唯一,但sidno不一定,所以对比GTID的时候,需要转换// 分析Binlog_sender的构造函数,可以知道m_exclude_gtid来自slave_gtid_executed,此处为获取其Sid_map,即从库的sidno与sid对应关系>>>>>>const rpl_sid &server_sid= gtid_state->get_server_sid() // 获取当前主库的rpl_sid(UUID),gtid_state为mysqld启动时初始化>>>>>>rpl_sidno subset_sidno= slave_sid_map->sid_to_sidno(server_sid) // 把主库的rpl_sid(UUID)转换成在从库的rpl_sidno(整数)>>>>>>Gtid_set gtid_executed_and_owned(gtid_state->get_executed_gtids()->get_sid_map())

// 定义一个包含主库rpl_sid(UUID)和rpl_sidno(整数)映射关系的GTID集合gtid_executed_and_owned>>>>>>gtid_executed_and_owned.add_gtid_set(gtid_state->get_executed_gtids())

// gtid_executed_and_owned加入gtid_executed表中保存的GTID集合>>>>>>gtid_state->get_owned_gtids()->get_gtids(gtid_executed_and_owned)

// gtid_executed_and_owned加入已经申请但仍在处理中的GTID集合gtid_owned>>>>>>m_exclude_gtid->is_subset_for_sid(&gtid_executed_and_owned, gtid_state->get_server_sidno(), subset_sidno)

// 如果subset_sidno为0,即在从库已经执行过的GTID集合中找不到主库UUID对应的记录,返回true// 分别根据主库和从库各自的rpl_sidno从各自的Gtid_set中找到对应的元素迭代,如:// A:{1:1-99},{1:150-300} B:{1:1-99},{1:150-300}// 假设A是主库,B是从库,// 依次遍历B中每个元素,然后与A中的元素比较,如果B元素的起始值大于A元素的结束值,则找A的下一个元素// 否则,如果B元素的起始值小于A元素的起始值或者B元素的结束值大于A元素的结束值,返回false,退出遍历// 综上,保证slave_gtid_executed中关于主库UUID的GTID集合是主库gtid_executed_and_owned的子集,如果不是,则报错退出>>>>>>gtid_state->get_lost_gtids()->is_subset(m_exclude_gtid)

// 逻辑与上者类似,但又有区别:// 分别找出purged_gtid中最大的rpl_sidno(即最大的UUID)与m_exclude_gtid最大的rpl_sidno// 从主库的rpl_sid=1开始遍历到最大的rpl_sid,即遍历所有UUID,如果主库的rpl_sidno大于从库的rpl_sidno,直接退出// 否则,保证主库purged_gtid中的每个UUID都是从库m_exclude_gtid中对应的UUID的子集// 综上,与前者的区别是,必须保证每个UUID对应的GTID都是从库m_exclude_gtid对应UUID的子集>>>>>>Gtid first_gtid= {0, 0}; // 设置查找初始值>>>>>>mysql_bin_log.find_first_log_not_in_gtid_set(index_entry_name, m_exclude_gtid, &first_gtid, &errmsg)

>>>>>>>>mysql_mutex_lock(&LOCK_index) // 获取Binlog的index文件互斥锁>>>>>>for (error= find_log_pos(&linfo, NULL, false);!error; error= find_next_log(&linfo, false))

// find_log_pos函数:从偏移位置为0的地方开始读取Binlog的index文件,一行行读取,NULL表示找到第一行,否则为文件名匹配的那行// find_next_log函数:从刚才的偏移位置一行行依次往下读取,每执行一次都会更新偏移位置>>>>>>>>filename_list.push_back(string(linfo.log_file_name)) // 把读取的文件名加入到filename_list链表中>>>>>>mysql_mutex_unlock(&LOCK_index) // 释放Binlog的index文件互斥锁>>>>>>rit= filename_list.rbegin() // 反向遍历filename_list链表的初始位置>>>>>>while (rit != filename_list.rend()) // 反向遍历filename_list链表>>>>>>>>switch (read_gtids_from_binlog(filename, NULL, &binlog_previous_gtid_set,

first_gtid,

binlog_previous_gtid_set.get_sid_map(),

opt_master_verify_checksum, is_relay_log))

// binlog_previous_gtid_set为通过m_exclude_gtid的Sid_map定义的一个空Gtid_set// 读取Binlog的event,读取到binary_log::PREVIOUS_GTIDS_LOG_EVENT时,把GTID加入到binlog_previous_gtid_set,退出读取// opt_master_verify_checksum参数,决定是否校验>>>>>>>>>>case GOT_GTIDS:

>>>>>>>>>>case GOT_PREVIOUS_GTIDS:

>>>>>>>>>>>>if (binlog_previous_gtid_set.is_subset(gtid_set)) // binlog_previous_gtid_set为m_exclude_gtid的子集>>>>>>>>>>>>>>strcpy(binlog_file_name, filename) // 拷贝下Binlog文件名,通过binlog_file_name指针传出去>>>>>>>>>>>>>>goto end // 寻找结束,文件名传给了index_entry_name指针>>>>>>name_ptr= index_entry_name // index_entry_name指针地址赋值给name_ptr>>>>mysql_bin_log.find_log_pos(&m_linfo, name_ptr, true) // 找到Binlog文件在Binlog的index文件中的偏移位置,存放在m_linfo类中>>RUN_HOOK(binlog_transmit, transmit_start,(thd, m_flag, m_start_file, m_start_pos,&m_observe_transmission)) // 调用钩子函数>>...ack_receiver.add_slave(current_thd) // 把从库信息加入到Ack_receiver类中,即通知ACK接收器接收该Binlog Dump线程的ACK>>...repl_semisync.add_slave() // 累加rpl_semi_sync_master_clients变量,但更新前会获取LOCK_binlog_互斥锁,更新完后立即释放>>...repl_semisync.handleAck(param->server_id, log_file, log_pos)

// 告诉主库server_id为param->server_id的从库到“(log_file, log_pos)”位置的Binlog已经收到从库的回复// 上报该位置信息,也会获取LOCK_binlog_互斥锁,更新完后立即释放

笔者重点关注点如下图梳理:关键点梳理Binlog Dump线程启动:半同步复制,启动Binlog Dump线程时,会通知ACK接收器接收该线程的ACK,并累加rpl_semi_sync_master_clients全局状态变量值,告诉主库去监听传输过程。如果change master时,带有position,则此时会认为该position之前的event都已经得到ACK回复。

Binlog Dump线程读取event:Binlog Dump线程会有一个死循环,不断地把读取到的event写入包内存,再发送给从库。半同步复制时,因为包头部需要3字节,故需要扩展包内存长度,由异步复制的1字节变成半同步的3字节。

event发送前:在半同步复制时,发送前需要设置包头的sync位,用于告知从库主库是否等待event回复。

event发送:如果该event已经在从库的slave_gtid_executed中,表示可以跳过,此时跳过发送event,但会定期给从库发送心跳;否则,需要给从库发送event。

event发送后:如果event可以被跳过,此时会假装已经收到从库的回复,直接上报Binlog位置;否则,需要读取从库的回复。优化

结合以上的关键点梳理,以及前面的火焰图分析:CPU时钟周期主要消耗在2、3以及5。结合之前的情景分析,在读取Binlog文件给从库寻找position时候,并没有必要去执行2、3以及5。最简单的验证方式,在第二节的Binlog_sender::run()函数中,把before_send_hook(log_file, log_pos)和after_send_hook(log_file, in_exclude_group ? log_pos : 0)放到判断if (m_exclude_gtid && (in_exclude_group= skip_event(event_ptr, event_len, in_exclude_group)))内部,即如果event可以跳过,跳过执行before_send_hook和after_send_hook。如下所示,结果

按照以上的优化方式,调整代码后,从库的挂载时间可以由原来的60s左右变成14s左右。之所以,还存在差距,在于还有第二步尚待优化,即reset_transmit_packet函数,在读取event前,会为包内存扩容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值