暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 5.7 Slave I/O线程流程分析

数据库技术汇 2021-04-25
3061

主从复制是MySQL源代码中比较重要的一部分代码,一直以来比较经典的复制逻辑是:Master Dump线程 -> Slave I/O线程(Relay log) -> Slave SQL线程。随着MySQL版本的不断演进,在已经比较长的主从复制链路中又出现了下面一些显著的变化:

  • MySQL 5.6以后引入的MTS(Multi-Thread Slave)优化将传统的Slave SQL线程分解成了一个Dispatch线程和多个Worker线程;

  • MySQL 5.7半同步复制插件中又引入了一个异步ACK线程,这个线程和Dump线程有着密切的关联;

  • MySQL 5.6以后GTID的引入,又把主从复制的交互逻辑(传输循环以前)变得更为复杂,主要是GTID的解析和与file:pos复制模式的兼容;

以下是MySQL 5.7.10 Slave I/O线程的逻辑流程的关键点解析:

注意: 
HOOK是MySQL插件架构的基础,为保证总体流程的清晰,没有具体说明每一个HOOK的作用。


一、代码入口

在MySQL源代码sql/rpl_slave.cc文件中:

  1. /**

  2.  Slave IO thread entry point.


  3.  @param arg Pointer to Master_info struct that holds information for

  4.  the IO thread.


  5.  @return Always 0.

  6. */

  7. extern "C" void *handle_slave_io(void *arg)


二、关键逻辑

步骤01:(HOOK)thread_start

执行注册过的thread_start函数,在半同步插件中就实现了一个HOOK,做一些半同步插件的初始化工作:

  1. if (RUN_HOOK(binlog_relay_io, thread_start, (thd, mi)))

  2. {

  3.  mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,

  4.    ER(ER_SLAVE_FATAL_ERROR), "Failed to run 'thread_start' hook");

  5.    goto err;

  6. }


步骤02:mysql_init

其实MySQL Slave的I/O线程和其它mysql cli一样,也是一个MySQL Master的客户端,因此这里首先创建一个用于连接Master的MYSQL句柄:

  1. if (!(mi->mysql = mysql = mysql_init(NULL)))

  2. {

  3.  mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,

  4.               ER(ER_SLAVE_FATAL_ERROR), "error in mysql_init()");

  5.  goto err;

  6. }


步骤03:safe_connect

建立到Master的连接,其内部包含首先创建到Master TCP监听的TCP连接,然后进行用户权限验证,非法用户的连接会以失败告终:

  1. // we can get killed during safe_connect

  2. if (!safe_connect(thd, mysql, mi))

  3. {

  4.  sql_print_information("Slave I/O thread%s: connected to master '%s@%s:%d',"

  5.  ...


步骤04:与Master交互

首先是根据Master的版本来创建Format_Description_Event、和Master对比时间戳、设置默认字符集等等;其次是查询对比Master的UUID,这样当故障转移时Master变化,Slave是可以感知到是从不同Master复制数据;最后,将当前Slave的UUID上报给Master, UUID是全局唯一的,Master也可以根据这个特性来做一些检验和错误预防。

注意: 
Format_Description_Event是binlog中非常重要的一种事件类型,主要的作用就是描述当前binlog文件的格式信息,通过在每一个binlog文件中包含这个事件,来兼容不同版本的binlog格式;同时允许用户在线修改一些和binlog相关的参数。解析一个binlog文件,比如mysqlbinlog等,都依赖这个格式描述符。

  1. ret= get_master_version_and_clock(mysql, mi);

  2. if (!ret)

  3.  ret= get_master_uuid(mysql, mi);

  4. if (!ret)

  5.  ret= io_thread_init_commands(mysql, mi);


步骤05:注册Slave到Master

Slave上报自身用户复制的用户名、密码等信息,Master验证其是否具有进行复制的权限,COM_REGISTER_SLAVE命令的协议格式如下:

长度(字节)含义备注
1COM_REGISTER_SLAVE命令类型
4server-id
1slaves hostname length
string[len]slaves hostname
1slaves user len
string[len]slaves user
1slaves password len
string[len]slaves password
2slaves mysql-port
4replication rank
4master-id
  1. /*

  2. Register ourselves with the master.

  3. */

  4. THD_STAGE_INFO(thd, stage_registering_slave_on_master);

  5. if (register_slave_on_master(mysql, mi, &suppress_warnings))


步骤06:发起Dump请求

根据基于file:pos还是GTID复制模式的不同,Slave在Dump请求中附带位置信息, COM_BINLOG_DUMP_GTID命令的协议格式如下:

长度(字节)含义备注
1COM_BINLOG_DUMP_GTID命令类型
2flags
4server-id
4binlog file name length (N)
Nbinlog file name
8offset, always 4*
4gtid string length (M)二进制
Mgtid string
  1. THD_STAGE_INFO(thd, stage_requesting_binlog_dump);

  2. if (request_dump(thd, mysql, mi, &suppress_warnings))


步骤07:接收循环

上述步骤完成后,Slave I/O线程进入一个循环,不断接收TCP Socket的数据并做相应的处理,关键步骤分解如下:


1. 读取一个Binlog事件

  1. THD_STAGE_INFO(thd, stage_waiting_for_master_to_send_event);

  2. event_len= read_event(mysql, mi, &suppress_warnings);


2. (HOOK)after_read_event

  1. if (RUN_HOOK(binlog_relay_io, after_read_event,

  2.  (thd, mi,(const char*)mysql->net.read_pos + 1,

  3.  event_len, &event_buf, &event_len)))


3. 写入到relay文件

  1. bool synced= 0;

  2. if (queue_event(mi, event_buf, event_len))

  3. {

  4.  mi->report(ERROR_LEVEL, ER_SLAVE_RELAY_LOG_WRITE_FAILURE,

  5.    ER(ER_SLAVE_RELAY_LOG_WRITE_FAILURE),

  6.    "could not queue event from master");

  7.    goto err;

  8. }

其实queue_event是一个非常复杂冗长的处理逻辑,简单归纳为下面几个主要的子步骤:

  • 获取relay文件锁并加锁

  • 根据Format_Description_Event判断是否开启了binlog校验

  • 读取每一个事件,做必要的验证,如果开启校验则执行校验

  • 如果是GTID事件,则解析出对应的GTID

  • 写relay文件,同样根据设置判断是否是要落盘(fsync)

  • 是GTID事件,将GTID事件加入到queued_gtid集合中

  • 通知SQL线程有新的binlog事件到达


4. (HOOK)after_queue_event

  1. if (RUN_HOOK(binlog_relay_io, after_queue_event,

  2.   (thd, mi, event_buf, event_len, synced)))

  3. {

  4.  mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,

  5.    ER(ER_SLAVE_FATAL_ERROR),

  6.    "Failed to run 'after_queue_event' hook");

  7.    goto err;

  8. }


5. 写Master Info文件

注意: 
这里的写Master Info文件是在保护下进行的,另外,即便MySQL默认把这个文件的落盘设置为10000个Log事件fsync一次,但是还是会频繁的发起write 系统调用

思考: 
如果调整一下binlog文件的格式,或者加入一种新的Log事件类型,这样是不是可以和其它事件合并在一起?

  1. mysql_mutex_lock(&mi->data_lock);

  2. if (flush_master_info(mi, FALSE))

  3. {

  4.  mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,

  5.    ER(ER_SLAVE_FATAL_ERROR),

  6.    "Failed to flush master info.");

  7.    mysql_mutex_unlock(&mi->data_lock);

  8.  goto err;

  9. }

  10. mysql_mutex_unlock(&mi->data_lock);


6. relay空间占用检查

如果设置了relay日志的空间占用限制,则进行检查,剩余磁盘空间不足时等待。

  1. if (rli->log_space_limit && rli->log_space_limit <

  2.    rli->log_space_total &&

  3.    !rli->ignore_log_space_limit)

  4.  if (wait_for_relay_log_space(rli))

  5.  {

  6.    sql_print_error("Slave I/O thread aborted while waiting for relay"

  7.    " log space");

  8.    goto err;

  9.  }


7. 清理内存

清理内存,防止OOM!

思考: 
如何优化,复用内存是可行的,因为大多数复制架构中Slave I/O线程只有一条,预分配+复用可以优化绝大多数场景中的性能开销;唯一不足的是如果偶尔遇见一个特别大的binlog事件,是放大预分配的内存(采用什么策略缩放回去),还是分片读取?

  1. /*

  2. After event is flushed to relay log file, memory used

  3. by thread's mem_root is not required any more.

  4. Hence adding free_root(thd->mem_root,...) to do the

  5. cleanup, otherwise a long running IO thread can

  6. cause OOM error.

  7. */

  8. free_root(thd->mem_root, MYF(MY_KEEP_PREALLOC));


三、后记

从上面的分解可以看出:

  • queue_event的逻辑有优化的空间,尤其是写relay日志的部分

  • 写master info, 无论是写文件还是innodb表,都还可以优化


文章转载自数据库技术汇,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论