共计 14159 个字符,预计需要花费 36 分钟才能阅读完成。
前言
常常听到他人说 Mysql 的 SBR、RBR、MBR,如果不分明,那么能够跟着文章一起来学习。因为波及到主从的内容比拟多,须要拆分成多篇内容来概述,这章先从基础知识和主从关系建设开始讲起。还会出一篇文章具体解说从主同步。
1. 理解什么是 SBR、RBR、MBR?
2. 理解下主从配置该如何配置?
3. 理解主从关系如何建设?
1. 配置 Mysql 主从
在本文中,分为一主一从。主监听的端口为 3306,从监听的端口为 3309。
1.1 主服务配置
master 配置,配置文件 my.cnf:
[mysqld]
port=3306
basedir=/usr/local/mysql8.0.20
datadir=/usr/local/mysql8.0.20/data
socket=/tmp/mysql.sock
#explicit_defaults_for_timestamp=true
lower_case_table_names=2 #表名存储为给定的大小写然而比拟的时候是小写的
log_bin=mysql-bin
server_id =10
主服务启动 Mysql 命令:
# sudo bin/mysqld --defaults-file=/usr/local/mysql8.0.20/etc/my.cnf --user=root
客户端连贯 master 端
# mysql --socket=/tmp/mysql.sock -u root
启动 master 后,须要创立一个用户,用于主从同步:
mysql> GRANT REPLICATION SLAVE ON *.* TO repl@'localhost' IDENTIFIED BY '123456';
查看主服务状态
mysql> show master status \G;
1.2 从服务配置
slave 配置, 配置文件 salve.conf:
[mysqld]
port=3309
basedir=/usr/local/mysql8.0.20
datadir=/usr/local/mysql8.0.20/data1
socket=/tmp/mysqlslave.sock
#explicit_defaults_for_timestamp=true
lower_case_table_names=2 #表名存储为给定的大小写然而比拟的时候是小写的
log_bin=mysql-bin
server_id=2
relay_log=/usr/local/mysql8.0.20/mysql-relay-bin
read_only=1 #执行模式
从服务启动 Mysql 命令:
# sudo bin/mysqld --defaults-file=/usr/local/mysql8.0.20/etc/salve.conf --user=root
连贯 Slave 端:
# mysql --socket=/tmp/mysqlslave.sock -u root
Slave 端主从同步配置:
mysql>
change master to master_host='localhost',
master_user='repl',
master_password='123456',
master_log_file='mysql-bin.000001',
master_log_pos=0;
Slave/Master
# 查看 binlog 事件
mysql> SHOW BINLOG EVENTS;
如果发现主从没有同步,能够应用如下命令查看相应的状态:
# 查看 slave 状态
mysql> show slave status;
如果遇到从库报这个谬误:Got fatal error 1236 from master when reading data from binary log: ‘Could not find first log file name in binary log index file’
Got fatal error 1236 from master when reading data from binary log: ‘could not find next log’
此时能够操作如下三个命令进行解决:
# 敞开 slave
mysql> stop slave;
#重置 slave
mysql> reset slave;
#启动 slave
mysql> start slave;
2.MYSQL 中 BINLOG_FORMAT 的三种模式
mysql 三种复制形式:基于 SQL 语句的复制(statement-based replication, SBR),基于行的复制(row-based replication, RBR),混合模式复制(mixed-based replication, MBR)。对应的,binlog 的格局也有三种:STATEMENT,ROW,MIXED。
2.1 STATEMENT 模式(SBR)
每一条会批改数据的 sql 语句会记录到 binlog 中。长处是并不需要记录每一条 sql 语句和每一行的数据变动,缩小了 binlog 日志量,节约 IO,进步性能。毛病是在某些状况下会导致 master-slave 中的数据不统一 (如 sleep() 函数,last_insert_id(),以及 user-defined functions(udf)等会呈现问题)
2.2 ROW 模式(RBR)
不记录每条 sql 语句的上下文信息,仅需记录哪条数据被批改了,批改成什么样了。而且不会呈现某些特定状况下的存储过程、或 function、或 trigger 的调用和触发无奈被正确复制的问题。毛病是会产生大量的日志,尤其是 alter table 的时候会让日志暴涨。
2.3 MIXED 模式(MBR)
以上两种模式的混合应用,个别的复制应用 STATEMENT 模式保留 binlog,对于 STATEMENT 模式无奈复制的操作应用 ROW 模式保留 binlog,MySQL 会依据执行的 SQL 语句抉择日志保留形式。
binlog 复制配置在 mysql 的配置文件 my.cnf 中,能够通过一下选项配置 binglog 相干, 配置如下:
#binlog 日志格局,mysql 默认采纳 statement,倡议应用 mixed
binlog_format = MIXED
#binlog 日志文件
log-bin = /data/mysql/mysql-bin.log
#binlog 过期清理工夫
expire_logs_days = 7
#binlog 每个日志文件大小
max_binlog_size = 100m
#binlog 缓存大小
binlog_cache_size = 4m
#最大 binlog 缓存大小
max_binlog_cache_size = 512m
2.4 优缺点比照
对于执行的 SQL 语句中蕴含 now()这样的工夫函数,会在日志中产生对应的 unix_timestamp()*1000 的工夫字符串,slave 在实现同步时,取用的是 sqlEvent 产生的工夫来保证数据的准确性。另外对于一些功能性函数 slave 能实现相应的数据同步,而对于下面指定的一些相似于 UDF 函数,导致 Slave 无奈通晓的状况,则会采纳 ROW 格局存储这些 Binlog,以保障产生的 Binlog 能够供 Slave 实现数据同步。
当初来比拟以下 SBR 和 RBR 这 2 种模式各自的优缺点:
SBR 的长处:
- 技术比拟成熟
- binlog 文件较小
- binlog 中蕴含了所有数据库更改信息,能够据此来审核数据库的平安等状况
- binlog 能够用于实时的还原,而不仅仅用于复制
- 主从版本能够不一样,从服务器版本能够比主服务器版本高
SBR 的毛病:
- 不是所有的 UPDATE 语句都能被复制,尤其是蕴含不确定操作的时候。
- 调用具备不确定因素的 UDF 时复制也可能出问题
-
应用以下函数的语句也无奈被复制:
- LOAD_FILE()
- UUID()
- USER()
- FOUND_ROWS()
- SYSDATE() (除非启动时启用了 –sysdate-is-now 选项)
INSERT … SELECT 会产生比 RBR 更多的行级锁
复制须要进行全表扫描 (WHERE 语句中没有应用到索引) 的 UPDATE 时,须要比 RBR 申请更多的行级锁
对于有 AUTO_INCREMENT 字段的 InnoDB 表而言,INSERT 语句会阻塞其余 INSERT 语句
对于一些简单的语句,在从服务器上的耗资源状况会更重大,而 RBR 模式下,只会对那个发生变化的记录产生影响
存储函数 (不是存储过程) 在被调用的同时也会执行一次 NOW() 函数,这个能够说是好事也可能是坏事
确定了的 UDF 也须要在从服务器上执行
数据表必须简直和主服务器保持一致才行,否则可能会导致复制出错
执行简单语句如果出错的话,会耗费更多资源
RBR 的长处:
任何状况都能够被复制,这对复制来说是最安全可靠的和其余大多数数据库系统的复制技术一样少数状况下,从服务器上的表如果有主键的话,复制就会快了很多。复制以下几种语句时的行锁更少:
- INSERT … SELECT
- 蕴含 AUTO_INCREMENT 字段的 INSERT
- 没有附带条件或者并没有批改很多记录的 UPDATE 或 DELETE 语句执行 INSERT,UPDATE,DELETE 语句时锁更少,从服务器上采纳多线程来执行复制成为可能。
RBR 的毛病:
- binlog 大了很多
- 简单的回滚时 binlog 中会蕴含大量的数据
主服务器上执行 UPDATE 语句时,所有发生变化的记录都会写到 binlog 中,而 SBR 只会写一次,这会导致频繁产生 binlog 的并发写问题
- UDF 产生的大 BLOB 值会导致复制变慢
无奈从 binlog 中看到都复制了写什么语句
当在非事务表上执行一段沉积的 SQL 语句时,最好采纳 SBR 模式,否则很容易导致主从服务器的数据不统一状况产生。
另外,针对零碎库 mysql 外面的表发生变化时的解决规定如下:
如果是采纳 INSERT,UPDATE,DELETE 间接操作表的状况,则日志格局依据 binlog_format 的设定而记录
如果是采纳 GRANT,REVOKE,SET PASSWORD 等治理语句来做的话,那么无论如何都采纳 SBR 模式记录
注:采纳 RBR 模式后,能解决很多原先呈现的主键反复问题。
2.5 如何查看复制格局
通过如下命令即可查看复制格局,如图 2 -4-1:
mysql> show variables like 'binlog_format';
<center> 图 2 -4-1 查看复制格局 </center>
默认 binlog_format 参数为行复制,在源码 mysql-8.0.20/sql/sys_vars.cc 中
static Sys_var_enum Sys_binlog_format(
"binlog_format",
"What form of binary logging the master will"
"use: either ROW for row-based binary logging, STATEMENT"
"for statement-based binary logging, or MIXED. MIXED is statement-"
"based binary logging except for those statements where only row-"
"based is correct: those which involve user-defined functions (i.e."
"UDFs) or the UUID() function; for those, row-based binary logging is"
"automatically used. If NDBCLUSTER is enabled and binlog-format is"
"MIXED, the format switches to row-based and back implicitly per each"
"query accessing an NDBCLUSTER table",
SESSION_VAR(binlog_format), CMD_LINE(REQUIRED_ARG, OPT_BINLOG_FORMAT),
binlog_format_names,
DEFAULT(BINLOG_FORMAT_ROW), // 默认 binlog 的同步为行复制
NO_MUTEX_GUARD,
NOT_IN_BINLOG, ON_CHECK(binlog_format_check),
ON_UPDATE(fix_binlog_format_after_update));
通过下面代码能够看出 mysql-8.0.20 默认为行复制。
那么持续来看一下 binlog_format 都有那些模式,能够看 mysql-8.0.20/sql/system_variables.h 文件
// Values for binlog_format sysvar
enum enum_binlog_format {
BINLOG_FORMAT_MIXED = 0, ///< 混合模式 statement if safe, otherwise row - autodetected
BINLOG_FORMAT_STMT = 1, ///<SQL 复制 statement-based
BINLOG_FORMAT_ROW = 2, ///< 行复制 row-based
BINLOG_FORMAT_UNSPEC =
3 ///< thd_binlog_format() returns it when binlog is closed};
基于如下代码能够得悉,binlog_format 蕴含 BINLOG_FORMAT_MIXED、BINLOG_FORMAT_STMT、BINLOG_FORMAT_ROW 三种模式,也就是对应:STATEMENT 模式(SBR)、ROW 模式(RBR)、MIXED 模式(MBR)
3. 建设主从关系
通过张图来看一下主从关系建设的相干流程, 如图 3 -1-1:
<center> 图 3 -1-1 主从同步建设 </center>
3.1 slave 端 start_salve 办法
在 mysql 主从建设中,是由 slave 端先发动,当执行“start slave;”语句时,会调用 rpl_slave.cc 中的 start_slave 办法,其中实现如下:
bool start_slave(THD *thd, LEX_SLAVE_CONNECTION *connection_param,
LEX_MASTER_INFO *master_param, int thread_mask_input,
Master_info *mi, bool set_mts_settings) {
bool is_error = false;
int thread_mask;
DBUG_TRACE;
lock_slave_threads(mi); // 进行运行线程
// 获取已进行线程的掩码
init_thread_mask(&thread_mask, mi, true /* inverse */);
/*
咱们将进行上面的所有线程。但如果用户想只启动一个线程,就如同另一个线程正在运行一样(就像咱们不要想碰另一根线),所以将位设置为 0 其余线程
*/
if (thread_mask_input) {thread_mask &= thread_mask_input;}
if (thread_mask) // 一些线程进行,启动它们
{if (load_mi_and_rli_from_repositories(mi, false, thread_mask)) {
is_error = true;
my_error(ER_MASTER_INFO, MYF(0));
} else if (*mi->host || !(thread_mask & SLAVE_IO)) {
/*
如果咱们要启动 IO 线程,咱们须要思考通过启动从机提供的选项。*/
if (thread_mask & SLAVE_IO) {if (connection_param->user) { // 设置用户
mi->set_start_user_configured(true);
mi->set_user(connection_param->user);
}
if (connection_param->password) { // 设置明码
mi->set_start_user_configured(true);
mi->set_password(connection_param->password);
}
if (connection_param->plugin_auth) // 设置受权插件
mi->set_plugin_auth(connection_param->plugin_auth);
if (connection_param->plugin_dir) // 插件目录
mi->set_plugin_dir(connection_param->plugin_dir);
}
//...
// 初始化设置
int slave_errno = mi->rli->init_until_option(thd, master_param);
if (slave_errno) {my_error(slave_errno, MYF(0));
is_error = true;
}
if (!is_error) is_error = check_slave_sql_config_conflict(mi->rli);
} else if (master_param->pos || master_param->relay_log_pos ||
master_param->gtid)
push_warning(thd, Sql_condition::SL_NOTE, ER_UNTIL_COND_IGNORED,
ER_THD(thd, ER_UNTIL_COND_IGNORED));
if (!is_error)
// 启动 slave 线程
is_error =
start_slave_threads(false /*need_lock_slave=false*/,
true /*wait_for_start=true*/, mi, thread_mask);
} else {
is_error = true;
my_error(ER_BAD_SLAVE, MYF(0));
}
} else {
/* 如果所有线程都已启动,则没有谬误,只有一个正告 */
push_warning_printf(
thd, Sql_condition::SL_NOTE, ER_SLAVE_CHANNEL_WAS_RUNNING,
ER_THD(thd, ER_SLAVE_CHANNEL_WAS_RUNNING), mi->get_channel());
}
/*
如果有人试图启动,请革除启动信息,IO 线程以防止任何平安问题。*/
if (is_error && (thread_mask & SLAVE_IO) == SLAVE_IO) mi->reset_start_info();
unlock_slave_threads(mi);
mi->channel_unlock();
return is_error;
}
从如上代码能够得悉调用“start slave;”时,会对线程做一些进行操作。而后进行一些设置后,调用 start_slave_threads 办法启动 slave 线程。而后 start_slave_threads 是一个比拟要害的办法。
那么接下来看一下 start_slave_threads 办法,实现如下:
bool start_slave_threads(bool need_lock_slave, bool wait_for_start,
Master_info *mi, int thread_mask) {
mysql_mutex_t *lock_io = nullptr, *lock_sql = nullptr,
*lock_cond_io = nullptr, *lock_cond_sql = nullptr;
mysql_cond_t *cond_io = nullptr, *cond_sql = nullptr;
bool is_error = false;
DBUG_TRACE;
DBUG_EXECUTE_IF("uninitialized_master-info_structure", mi->inited = false;);
//...
if (thread_mask & SLAVE_IO) // 判断是否反对 SLAVE_IO
is_error = start_slave_thread(
#ifdef HAVE_PSI_THREAD_INTERFACE
key_thread_slave_io,
#endif
handle_slave_io, lock_io, lock_cond_io, cond_io, &mi->slave_running,
&mi->slave_run_id, mi); // 调用 handle_slave_io 办法
if (!is_error && (thread_mask & SLAVE_SQL)) { // 判断是否反对 SLAVE_SQL
//...
if (!is_error)
is_error = start_slave_thread(
#ifdef HAVE_PSI_THREAD_INTERFACE
key_thread_slave_sql,
#endif
handle_slave_sql, lock_sql, lock_cond_sql, cond_sql,
&mi->rli->slave_running, &mi->rli->slave_run_id, mi); // 调用 handle_slave_sql 办法
if (is_error)
terminate_slave_threads(mi, thread_mask & SLAVE_IO,
rpl_stop_slave_timeout, need_lock_slave);
}
return is_error;
}
通过如上办法能够得悉 thread_mask 掩码是用于判断是否反对 SLAVE_IO 与反对 SLAVE_SQL。如果反对则线程调用对应的办法。因为思考到主题为主从关系建设, 这里次要关注一下 handle_slave_io 办法。
<center> 图 3 -1-2 主从同步协定 </center>
如图 3 -1- 2 中 IO Thread 与 SQL Thread 其实就是对应 handle_slave_io 办法与 handle_slave_sql 办法。
3.2 slave 端 handle_slave_io 办法
handle_slave_io 办法为建设主从的次要办法,其中蕴含了初始化 slave 线程、发动连贯 master、注册 slave 到 master,发动 COM_BINLOG_DUMP 或 COM_BINLOG_DUMP_GTID 操作等,实现如下:
extern "C" void *handle_slave_io(void *arg) {
//...
my_thread_init();
{
// 初始化 slave 线程
if (init_slave_thread(thd, SLAVE_THD_IO)) {mysql_cond_broadcast(&mi->start_cond);
mysql_mutex_unlock(&mi->run_lock);
mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,
ER_THD(thd, ER_SLAVE_FATAL_ERROR),
"Failed during slave I/O thread initialization");
goto err;
}
//...
mysql_cond_broadcast(&mi->start_cond); // 调用做唤醒操作
//...
// 发动登陆操作
successfully_connected = !safe_connect(thd, mysql, mi);
// we can get killed during safe_connect
#ifdef HAVE_SETNS
if (mi->is_set_network_namespace()) {
// Restore original network namespace used to be before connection has
// been created
successfully_connected =
restore_original_network_namespace() | successfully_connected;}
#endif
//...
/*
注册 slave 到 master
*/
THD_STAGE_INFO(thd, stage_registering_slave_on_master);
if (register_slave_on_master(mysql, mi, &suppress_warnings)) {
if (!check_io_slave_killed(thd, mi,
"Slave I/O thread killed"
"while registering slave on master")) {LogErr(ERROR_LEVEL, ER_RPL_SLAVE_IO_THREAD_CANT_REGISTER_ON_MASTER);
if (try_to_reconnect(thd, mysql, mi, &retry_count, suppress_warnings,
reconnect_messages[SLAVE_RECON_ACT_REG]))
goto err;
} else
goto err;
goto connected;
}
//...
while (!io_slave_killed(thd, mi)) {
MYSQL_RPL rpl;
THD_STAGE_INFO(thd, stage_requesting_binlog_dump);
if (request_dump(thd, mysql, &rpl, mi, &suppress_warnings)) { // 发动 dump 指令
LogErr(ERROR_LEVEL, ER_RPL_SLAVE_ERROR_REQUESTING_BINLOG_DUMP,
mi->get_for_channel_str());
if (check_io_slave_killed(thd, mi,
"Slave I/O thread killed while \
requesting master dump") ||
try_to_reconnect(thd, mysql, mi, &retry_count, suppress_warnings,
reconnect_messages[SLAVE_RECON_ACT_DUMP]))
goto err;
goto connected;
}
//...
}
//...
my_thread_end(); // 线程完结
#if OPENSSL_VERSION_NUMBER < 0x10100000L
ERR_remove_thread_state(0);
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
my_thread_exit(nullptr); // 退出线程
return (nullptr); // Avoid compiler warnings
}
从代码中能够得悉调用 safe_connect 进行 slave 对 master 的登陆,具体登陆能够协定能够看一下之前写的文章:https://blog.csdn.net/byxiaoyuonly/article/details/108212013。
而后又调用 register_slave_on_master 会发送 COM_REGISTER_SLAVE 指令进行把 slave 注册到 master,再调用 request_dump 发动 binlog_dump 指令。
<center> 图 3 -2-1 发送 COM_BINLOG_DUMP</center>
在 request_dump 发送指令其实反对 COM_BINLOG_DUMP 与 COM_BINLOG_DUMP_GTID 两种,然而具体发什么取决于是否开启 gtid 设置,如图 3 -2- 1 所示。
3.4 master 建设实现
通过对下面内容的理解, 咱们得悉 register_slave_on_master 会发动发动 COM_REGISTER_SLAVE 对把 slave 注册到 master,而后调用 request_dump 发动 binlog_dump 指令。master 指令解决如下:
bool dispatch_command(THD *thd, const COM_DATA *com_data,
enum enum_server_command command) {
//...
switch (command) {
case COM_REGISTER_SLAVE: { // 注册 slave 到 master
// TODO: access of protocol_classic should be removed
if (!register_slave(thd, thd->get_protocol_classic()->get_raw_packet(),
thd->get_protocol_classic()->get_packet_length()))
my_ok(thd);
break;
}
//...
case COM_BINLOG_DUMP_GTID: //binlog_dump_gtid
// TODO: access of protocol_classic should be removed
error = com_binlog_dump_gtid(thd, (char *)thd->get_protocol_classic()->get_raw_packet(),
thd->get_protocol_classic()->get_packet_length());
break;
case COM_BINLOG_DUMP: //binlog_dump
// TODO: access of protocol_classic should be removed
error = com_binlog_dump(thd, (char *)thd->get_protocol_classic()->get_raw_packet(),
thd->get_protocol_classic()->get_packet_length());
break;
}
return error;
}
注册 slave 到 master 过程能够看后续的数据包,这边能够接着看一下对应的 com_binlog_dump 办法实现:
bool com_binlog_dump(THD *thd, char *packet, size_t packet_length) {
DBUG_TRACE;
ulong pos;
ushort flags = 0;
const uchar *packet_position = (uchar *)packet;
size_t packet_bytes_todo = packet_length;
DBUG_ASSERT(!thd->status_var_aggregated);
thd->status_var.com_other++;
thd->enable_slow_log = opt_log_slow_admin_statements;
if (check_global_access(thd, REPL_SLAVE_ACL)) return false;
/*
4 bytes is too little, but changing the protocol would break
compatibility. This has been fixed in the new protocol. @see
com_binlog_dump_gtid().
*/
READ_INT(pos, 4);
READ_INT(flags, 2);
READ_INT(thd->server_id, 4);
DBUG_PRINT("info",
("pos=%lu flags=%d server_id=%d", pos, flags, thd->server_id));
kill_zombie_dump_threads(thd);
query_logger.general_log_print(thd, thd->get_command(), "Log:'%s'Pos: %ld",
packet + 10, (long)pos);
mysql_binlog_send(thd, thd->mem_strdup(packet + 10), (my_off_t)pos, nullptr,
flags); // 发送 binlog
unregister_slave(thd, true, true /*need_lock_slave_list=true*/);
/* 如果咱们到了这里,线程须要终止 */
return true;
error_malformed_packet:
my_error(ER_MALFORMED_PACKET, MYF(0));
return true;
}
在 mysql_binlog_send 中其实调用 Binlog_sender 的 run 办法,sender.run()办法中又调用 Binlog_sender::init 初始化检测、Binlog_sender::check_start_file() 查看文件等。最终调用 Binlog_sender::send_binlog 对从服务发送 binlog,如图 3 -4- 1 所示。
<center> 图 3 -4-1 send binlog</center>
4. 主从建设过程中数据包
<center> 图 4 -1 主从建设过程数据包 </center>
通过图 4 - 1 能够得悉在主从关系建设会发动如下操作:
# 查问以后工夫戳
SELECT UNIX_TIMESTAMP()
#查问 master 的 serverid
SELECT @@GLOBAL.SERVER_ID
#设置心跳周期, 单位为纳秒,其实只有 30s。初始化心跳如图 4 -2
SET @master_heartbeat_period= 30000001024
#设置 master_binlog_checksum
SET @master_binlog_checksum= @@global.binlog_checksum
#查问 master_binlog_checksum
SELECT @master_binlog_checksum
#取得是否反对 gtid
SELECT @@GLOBAL.GTID_MODE
#查问 server uuid
SELECT @@GLOBAL.SERVER_UUID
#设置 slave uuid
SET @slave_uuid= '2dc27df4-e143-11ea-b396-cc679ee1902b'
<center> 图 4 -2 初始化心跳周期 </center>
总结
- mysql 复制模式分为三种:STATEMENT 模式(SBR)、ROW 模式(RBR)、MIXED 模式(MBR)。
- mysql8.0.20 默认 ROW 模式(RBR)。
- 发送 binlog 反对两种模式:COM_BINLOG_DUMP 与 COM_BINLOG_DUMP_GTID。
- 默认状况 master_heartbeat_period 为 30 秒,单位为纳秒。
- IO Thread 与 SQL Thread 其实就是对应 handle_slave_io 办法与 handle_slave_sql 办法。