作为MySQL DBA,相信大家对参数 `Seconds_Behind_Master` 并不陌生,该字段的值可以通过 show slave status\G的输出,表示主从延迟的时间,单位为秒。监控主从延迟一般取这个值就足够了。0 表示无延迟,理想状态该值不要超过1。如果业务场景中有写后读,而且读的是从库,那么就会对主从延迟要求非常高。
理解了该字段的含义,大家有没有探究过该值是如何计算的?相信大家都可以说出来 是从库的时间 减去 SQL_thread应用到的主库binlog event的时间戳。
今天从官方文档 和 MySQL源码 探寻正确的计算方法
MySQL官方文档如何解释
原文
翻译
Seconds_Behind_Master
该字段表示复制实例相对于主实例的“延迟”:
-
当复制实例正在处理更新时,该字段显示复制实例上当前时间戳与正在处理的事件在主实例上原始时间戳之间的差异。
-
当复制实例上没有正在处理的事件时,该值为0。
基本上,该字段测量复制实例的SQL线程和复制实例的I/O线程之间的秒数差异。如果主实例和复制实例之间的网络连接很快,复制实例的I/O线程非常接近主实例,因此该字段是复制实例的SQL线程相对于主实例的延迟的良好近似值。如果网络速度较慢,这就不是一个良好的近似值;复制实例的SQL线程可能经常追赶读取速度较慢的复制实例的I/O线程,因此Seconds_Behind_Master经常显示为0,即使I/O线程相对于主实例是延迟的。换句话说,该列仅在快速网络中才有用。
即使主实例和复制实例的时钟时间不同,只要在复制实例的I/O线程启动时计算的差异保持不变,此时间差异计算就有效。任何更改,包括NTP更新,都可能导致时钟偏差,使Seconds_Behind_Master的计算变得不太可靠。
在MySQL 5.7中,如果复制实例的SQL线程未运行,或者SQL线程已经消耗完中继日志并且复制实例的I/O线程未运行,该字段为NULL(未定义或未知)。 (在MySQL的早期版本中,如果复制实例的SQL线程或复制实例的I/O线程未运行或未连接到主实例,则该字段为NULL。)如果I/O线程正在运行但中继日志用尽,Seconds_Behind_Master设置为0。
Seconds_Behind_Master的值基于存储在事件中的时间戳,这些事件通过复制进行保留。这意味着如果主实例M1本身是主实例M0的复制实例,M1二进制日志中源自M0二进制日志的任何事件都具有M0该事件的时间戳。这使得MySQL能够成功地复制时间戳。然而,对于Seconds_Behind_Master而言,问题在于如果M1还直接从客户端接收更新,那么Seconds_Behind_Master的值会随机波动,因为M1的最后一个事件有时源自M0,有时是M1上的直接更新的结果。
在使用多线程复制时,您应该记住该值基于Exec_Master_Log_Pos,因此可能不反映最近提交的事务的位置。
官方文档的翻译也有两种计算方法 :
方法一:该字段显示复制实例上当前时间戳与正在处理的事件在主实例上原始时间戳之间的差异
方法二:该字段测量复制实例的SQL线程和复制实例的I/O线程之间的秒数差异,前提主实例和复制实例之间的网络连接很快,复制实例的I/O线程非常接近主实例
源码中如何计算
在源码中全局搜索 Seconds_Behind_Master 关键字 ,排除测试文件,出现Seconds_Behind_Master关键字的文件也就几个。找到关于计算Seconds_Behind_Master 源码。
注释中也给出了 计算Seconds_Behind_Master 的伪代码
/*
The pseudo code to compute Seconds_Behind_Master:
if (SQL thread is running)
{
if (SQL thread processed all the available relay log)
{
if (IO thread is running)
print 0;
else
print NULL;
}
else
compute Seconds_Behind_Master;
}
else
print NULL;
*/
// 伪代码:计算 Seconds_Behind_Master
if (SQL 线程正在运行) {
if (SQL 线程已处理所有可用的中继日志) {
if (IO 线程正在运行) {
输出 0; // I/O 线程正在运行,但中继日志已用尽,因此 Seconds_Behind_Master 设置为 0。
} else {
输出 NULL; // I/O 线程不在运行,因此 Seconds_Behind_Master 为 NULL。
}
} else {
计算 Seconds_Behind_Master; // 计算复制实例相对于主实例的延迟。
}
} else {
输出 NULL; // SQL 线程不在运行,因此 Seconds_Behind_Master 为 NULL。
}
真实代码
if (mi->rli->slave_running)
{
/*
Check if SQL thread is at the end of relay log
Checking should be done using two conditions
condition1: compare the log positions and
condition2: compare the file names (to handle rotation case)
*/
if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) &&
(!strcmp(mi->get_master_log_name(), mi->rli->get_group_master_log_name())))
{
if (mi->slave_running == MYSQL_SLAVE_RUN_CONNECT)
protocol->store(0LL);
else
protocol->store_null();
}
else
{
long time_diff= ((long)(time(0) - mi->rli->last_master_timestamp)
- mi->clock_diff_with_master);
/*
Apparently on some systems time_diff can be <0. Here are possible
reasons related to MySQL:
- the master is itself a slave of another master whose time is ahead.
- somebody used an explicit SET TIMESTAMP on the master.
Possible reason related to granularity-to-second of time functions
(nothing to do with MySQL), which can explain a value of -1:
assume the master's and slave's time are perfectly synchronized, and
that at slave's connection time, when the master's timestamp is read,
it is at the very end of second 1, and (a very short time later) when
the slave's timestamp is read it is at the very beginning of second
2. Then the recorded value for master is 1 and the recorded value for
slave is 2. At SHOW SLAVE STATUS time, assume that the difference
between timestamp of slave and rli->last_master_timestamp is 0
(i.e. they are in the same second), then we get 0-(2-1)=-1 as a result.
This confuses users, so we don't go below 0: hence the max().
last_master_timestamp == 0 (an "impossible" timestamp 1970) is a
special marker to say "consider we have caught up".
*/
protocol->store((longlong)(mi->rli->last_master_timestamp ?
max(0L, time_diff) : 0));
}
}
else
{
protocol->store_null();
}
具体计算方式
long time_diff= ((long)(time(0) - mi->rli->last_master_timestamp)
- mi->clock_diff_with_master);
这行代码计算了一个名为 time_diff
的长整型变量,其值是当前时间(通过 time(0)
获取)与主实例上最后一个事件的时间戳(mi->rli->last_master_timestamp
)之间的差异减去主从实例之间的时钟差异(mi->clock_diff_with_master
)。这个 time_diff
的值表示当前时间与主实例最后一个事件的时间戳之间的秒数差异,减去主从实例之间的时钟差异。
clock_of_slave - last_timestamp_executed_by_SQL_thread - clock_diff_with_master
/*
The difference in seconds between the clock of the master and the clock of
the slave (second - first). It must be signed as it may be <0 or >0.
clock_diff_with_master is computed when the I/O thread starts; for this the
I/O thread does a SELECT UNIX_TIMESTAMP() on the master.
"how late the slave is compared to the master" is computed like this:
clock_of_slave - last_timestamp_executed_by_SQL_thread - clock_diff_with_master
*/
/*
主时钟和从属时钟之间的差异(second - first)。它必须是有符号的,因为它可能 <0 或 >0。
clock_diff_with_master 是在 I/O 线程启动时计算的;
为此,I/O 线程在主实例上执行 SELECT UNIX_TIMESTAMP()。
“从属相对于主实例的延迟”是这样计算的:
clock_of_slave - last_timestamp_executed_by_SQL_thread - clock_diff_with_master
*/
参考
官方文档 Seconds_Behind_Master 解释
MySQL :: MySQL 5.7 Reference Manual :: 13.7.5.34 SHOW SLAVE STATUS Statement
MySQL 复制延迟 Seconds_Behind_Master 究竟是如何计算的 - 知乎