jjzjj

MySQL 主从模式采用 GTID 的实践

悟空聊架构 2023-03-28 原文
你好,我是悟空。

本文主要内容如下:

一、背景

为了保证高可用,之前在测试环境部署了一套 MySQL 双主模式,当一个主库服务出现异常,可以将流量切到另外一个主库,两个主库之间相互同步数据。

双主模式

双主模式的原理图如下:

但是经常出现数据冲突的问题,于是我们又把​​双主模式​​改为了​​主从读写分离模式​​。主库作为读写库,再加上一个从库用来做 I/O 密集型的任务(如大量的数据统计操作)。如下图所示:

另外从库复制的模式采用​​位点​​的方式:指定 binlog 文件和 binlog 位置,这样从库就知道了复制的起始位置。(下文会讲解这种方式)

虽然改为了主从模式,但依旧遇到了些问题:

  • 问题 1:从库 B 复制数据时,出现了主键冲突问题,导致同步失败,从库停止复制。猜测因主库配置的 binlog 日志的格式为 mixed,从库同步时出现不一致的情况。
  • 问题 2:从库 B 停止复制后,导致很多数据未同步到从库,出现主从大量数据不一致的情况。
  • 问题 3:从库 B 想要恢复复制,必须先解决同步失败的问题才能恢复。排查难度较大,耗时。
  • 问题 4:从库 B 恢复时,必须知道同步位点,也就是从哪个 binlog 文件和 binlog 位置断开复制的,且即使找到了位点,也不是精确的。
  • 问题 5:从库 B 因同步异常导致停止复制到恢复复制这段期间,主库 A 自动清理了几天前的 binlog 日志,而这些日志从库 B 还未来得及同步,进而导致再次同步失败。
  • 问题 6:主从存在同步延迟。
这篇我们来探讨下问题 4 和问题 6。

其中问题 4 是一个比较头疼的问题,我们一般是通过查看从库 B 当前的同步状态拿到同步位点,然后设置同步位点后。但是重新启动同步的时候又会出现同步异常,比如从库 B 可能会出现 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’ 错误,提示出现了主键冲突,然后停止同步。

为了减少位点同步引入的复杂度,我们切换成了 GTID 模式。

对于问题 6,本篇也仅限于探讨如何观察延迟,对于如何减少延迟不在本篇探讨范围之内。

接下来我们来展开看下位点同步的痛点。

二、位点同步的痛点

2.1 通过位点同步的原理图

为了更清晰地理解主从采用位点同步的原理,这里有一个原理图:

1、主库会生成多个 binlog 日志文件。

2、从库的I/O 线程请求指定文件和指定位置的 binlog 日志文件(位点)。

3、主库 dump 线程获取指定位点的 binlog 日志。

4、主库按照从库发送给来的位点信息读取 binlog,然后推送 binlog 给从库。

5、从库将得到的 binlog 写到本地的 relay log (中继日志) 文件中。

6、从库的 SQL 线程读取和解析 relay log 文件。

7、从库的 SQL 线程重放 relay log 中的命令。

当我们使用位点同步的方式时,两种场景下的操作步骤比较复杂。

2.2 痛点

痛点1:首次开启主从复制的步骤复杂

  • 第一次开启主从同步时,要求从库和主库是一致的。
  • 找到主库的 binlog 位点。
  • 设置从库的 binlog 位点。
  • 开启从库的复制线程。

痛点2:恢复主从复制的步骤复杂

  • 找到从库复制线程停止时的位点。
  • 解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。(1062 错误是插入数据时唯一键冲突;1032 错误是删除数据时找不到行)
不论是首次开启同步时需要找位点和设置位点,还是恢复主从复制时,设置位点和忽略错误,这些步骤都显得过于复杂,而且容易出错。所以 MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。

三、GTID 方案

3.1 GTID 是什么?

GTID 的全称是 Global Transaction Identifier,全局事务 ID,当一个事务提交时,就会生成一个 GTID,相当于事务的唯一标识。

GTID 长这样:

c5d74746-d7ec-11ec-bf8f-0242ac110002:1
结构:

GTID=server_uuid:gno
server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;

gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。

每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。

3.2 GTID 的优势

  • 更简单的实现 failover,不用以前那样在需要找位点(log_file 和 log_pos)。
  • 更简单的搭建主从复制。
  • 比传统的复制更加安全。
  • GTID是连续的没有空洞的,保证数据的一致性,零丢失。

3.3 如何启用 GTID

修改主库和从库的配置文件:

#GTID:
gtid_mode=on
enforce_gtid_cnotallow=on
从库配置同步的参数:

CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_positinotallow=1
其中 master_auto_position 标识主从关系使用的 GTID 协议。

相比之前的配置,MASTER_LOG_FILE 和 MASTER_LOG_POS 参数已经不需要了。

3.4 GTID 同步方案

GTID 同步的原理图。

GTID 方案:主库计算主库 GTID 集合和从库 GTID 的集合的差集,主库推送差集 binlog 给从库。

当从库设置完同步参数后,主库 A 的GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。从库同步的逻辑如下:

  • 从库 B 指定主库 A,基于主备协议简历连接。
  • 从库 B 把集合 y 发给主库 A。
  • 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
  • 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务 GTID,也就是找到了 91。
  • 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。
  • 从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。
GTID 同步方案和位点同步的方案区别是:

  • 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
  • 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。

四、如何判断主从库是否有延迟

上面提到的问题 6 是主从读写分离后,从库复制存在延迟,接下来我们来探讨下如何观察主从延迟多少的问题。

方案一:判断从库的同步状态参数 seconds_behind_master 是否为 0。(不准确)

方案二:对比位点确保主备无延迟。

方案三:对比 GTID 集合确保主备无延迟。

方案一:查看 seconds_behind_master

可以在从库上执行 slow slave status 命令来看执行结果里面的 ​​seconds_behind_master​​ 参数的值,如下图所示,Seconds_Behind_Master 等于 0

Seconds_Behind_Master 的单位是秒,所以精度不准确。

所以为了保证查询的数据是和主库一致的,就需要先判断 seconds_behind_master 是否已经等于 0,如果不等于 0,就必须等到这个参数变为 0 才能执行查询请求。

方案二:对比位点

可以通过查看从库当前的同步位点来确认从库同步是否有延迟。下图是在从库上执行 ​​show slave status \G​​命令后的结果:

Master_Log_File​ 和 Read_Master_Log_Pos 这两个参数合起来表示的是读到的主库的最新位点,第一参数是代表读取到了哪个文件,第二个是读取到的文件的位置。

Relay_Master_Log_File​ 和 Exec_Master_Log_Pos,这两个参数合起来表示的是从库执行的最新位点。

如果红色框起来的两个参数:Master_Log_File​ 和 Relay_Master_Log_File 相等,则说明从库读到的最新文件和主库上生成的文件相同,这里都是 mysql-bin.000934。

如果蓝色框起来的两个参数 Read_Master_Log_Pos​ 和 Exec_Master_Log_Pos 相等,则说明从库读到的日志文件的位置和从库上执行日志文件的位置相同,这里都是 59521082。

当上面两组参数都相等时,则说明没有延迟。

方案三:对比 GTID 集合

方案三是对比 GTID 集合。首先我们在从库上执行  show slave status \G来查看 GTID 集合。

如下图所示:

Master_UUID 表示当前连接的主库的 ID。

Auto_Position: 1 表示主备使用了 GTID 协议。

Retrieved_Gtid_Set 表示从库收到的所有日志的 GTID 集合。

Executed_Gtid_Set 表示从库已经执行完成的 GTID 集合。

如果 Executed_Gtid_Set 集合是包含 Retrieved_Gtid_Set,则表示从库接收到的日志已经同步完成。

比如上图中 Retrieved_Gtid_Set 值为

c5d74746-d7ec-11ec-bf8f-0242ac110002:1-87323
前面一段是主库 id,后面一段 1-87383 是 GTID 范围。而Executed_Gtid_Set 的值有两个集合

7083ae1f-d7ef-11ec-a329-0242ac110002:1-2,
c5d74746-d7ec-11ec-bf8f-0242ac110002:1-87323
Executed_Gtid_Set 的第二个集合和第一个集合完全一致,第一个集合 id 和 集合范围是上次同步另外一个主库的记录。这里说明从库已经和当前主库同步完成了。

方案二对比位点和方案三的 GTID 比对都要比方案一的seconds_behind_master 更准确。但是还是没有达到精确的程度,需要配合半同步复制(semi-sync replication)才能达到。

小结:本篇通过 GTID 的方式更好地实现了主从节点的同步,以及如何观察主从同步的延迟。

参考资料:

www.passjava.cn

https://time.geekbang.org/column/article/77636

高性能 MySQL 第四版

千金良方:MySQL性能优化金字塔法则

关于我

8 年互联网开发经验,擅长微服务、分布式、架构设计。目前在一家大型上市公司从事基础架构和性能优化工作。

InfoQ 签约作者、蓝桥签约作者、阿里云专家博主、51CTO 红人。

有关MySQL 主从模式采用 GTID 的实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  5. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  6. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  7. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  8. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  9. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  10. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

随机推荐