主从延迟如何解决

2024年 2月 16日 87.9k 0

最近项目上线,遇到了主从问题。按理说公司基建不至于出现这种问题,但就是出现了。可能因为用的不是原生的MySQL吧。主从延迟会给前端和服务端带来很多问题,需要花费时间用工程手段来解决,我认为这是很不合理的。

举几个因为主从延迟会导致问题场景:

  • 创建了一个商品然后立即跳转到详情页
  • 在列表页更新了用户的权限,立即刷新
  • 凡是像这种操作后立即获取的,全会有问题。

    为什么要有主从

    MySQL数据库的主从(Master-Slave)架构主要是为了实现数据的高可用性(High Availability)和读写分离,具体的原因如下:

  • 数据备份:主从架构可以实现数据的实时备份,从库可以作为主库的一个镜像存在,当主库出现问题时,可以迅速切换到从库,保证数据的安全性。
  • 读写分离:在主从架构中,主库主要负责写操作,从库主要负责读操作,这样可以分担主库的压力,提高系统的处理能力。
  • 故障切换:当主库出现故障时,可以迅速切换到从库,保证服务的连续性,提高系统的可用性。
  • 负载均衡:通过主从架构,可以将读请求分散到多个从库,实现负载均衡,提高系统的性能。
  • 数据一致性:主从复制可以确保数据在主库和从库之间保持一致,提高数据的准确性。 因此,为了保证数据的安全性和系统的高可用性,MySQL通常会采用主从架构。
  • 主从如何同步

    下面是一个update 语句在主节点 A 执行,然后同步到从节点 B 的完整流程图

    image-20240216103317526.png

    从库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务从库 B的这个长连接。一个事务日志同步的完整过程是这样的:

  • 在从库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。

  • 在从库 B 上执行 start slave 命令,这时候从库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。

  • 主库 A 校验完用户名、密码后,开始按照从库 B 传过来的位置,从本地读取 binlog,发给 B。

  • 从库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。

  • sql_thread 读取中转日志,解析出日志里的命令,并执行。

  • 为什么会主从延迟

    什么是主从延迟?

  • 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;

  • 之后传给从库 B,我们把从库 B 接收完这个 binlog 的时刻记为 T2;

  • 从库 B 执行完成这个事务,我们把这个时刻记为 T3。

  • 所谓主从延迟,就是同一个事务,在从库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。

    你可以在从库上执行 show slave status 命令,它的返回结果里面会显示seconds_behind_master,用于表示当前从库延迟了多少秒。

    在网络正常的时候,日志从主库传给从库所需的时间是很短的,即 T2-T1的值是非常小的。也就是说,网络正常情况下,主从延迟的主要来源是从库接收完 binlog和执行完这个事务之间的时间差。

    主从延迟来源

  • 有些部署条件下,从库所在机器的性能要比主库所在的机器性能差。
  • 从库的压力大。如从库上的查询耗费了大量的 CPU 资源,影响了同步速度,造成主从延迟。
  • 大事务。因为主库上必须等事务执行完成才会写入 binlog,再传给从库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10分钟。
  • 大表 DDL,也是典型的大事务场景。
  • 从库的并行复制能力。查看软件版本,在官方的 5.6 版本之前,MySQL (sql_thread)只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主从延迟问题。
  • 如何解决

    一般的主从结构如下:

    image-20240216110115222.png

    一旦出现主从延迟问题,有如下解决方案

    强制走主库方案 - 有点可行

  • 将查询请求做分类,必须拿到最新结果的,强制请求到主库
    • 优点:能够区分场景,压力可控
    • 缺点:
      • 前后端都得改动。前端判断是否走主库,服务端判断指定场景走查主库
      • 后续维护也比较麻烦
      • 有时前端无法判断出场景,如进详情页,前端无法判断是刚创建完跳转的还是打开的是早已创建的
  • 全部走主库
    • 优点:简单便捷
    • 缺点:不区分具体场景,主库压力大
  • 先读从库,从库没有读主库
    • 优点:相对简单
    • 缺点:无法处理所有场景,如list的场景,因为必然有数据,但并不知道是否准确

    sleep 方案 - 有点可行

  • 前端延迟请求
    • 优点:简单便捷
    • 缺点:用户体验不好
  • 写相关接口,服务端返回结果详情,前端展示详情。如创建商品接口,服务端返回商品的详细信息,前端直接展示详细信息,不请求接口
    • 优点:逻辑比较通顺、清晰
    • 缺点:
      • 前端需要实现多套逻辑。如虽然是详情,但至少可能来自创建和详情接口;在成员列表中删除member成功后就直接不显示,不再调用list接口
      • 维护成本高,如对于详情页,如果后续详情也变更,创建和详情接口如何保持一致
      • 无法处理所有场景,如创建空间后获取成员列表(至少有自己)

    判断主从延迟方案 - 有的可行

  • 写操作写redis(过期时间1~2s),读的时候判断是否有redis,有则读主库。如创建商品,则在redis里记录商品id,获取详情的时候先判断该商品id是否在redis存在,如果存在则读主库。
    • 优点:
      • 前端无需改动,服务端改动相对可控,而且设置为弱依赖,所以问题应该不大
      • 能解决大部分问题
    • 缺点:
      • 服务端需要根据场景记录、读取redis
      • 有一定概率增加主库压力,但总体可控
  • 判断主从无延迟方案 - 不可行
    • 每次从库执行查询请求前,先判断seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为0 才能执行查询请求。可以使用 show slave status。
    • 对比位点确保主从无延迟
    • 对比 GTID 集合确保主从无延迟

    这种方式感觉不太现实

    • 实现上成本高
    • 仍然有过期读问题。因为上面判断主从无延迟的逻辑,是“从库收到的日志都执行完成了”。但是,从 binlog在主从之间状态的分析中,不难看出还有一部分日志,处于客户端已经收到提交确认,而从库还没收到日志的状态。
    • 如果在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况
  • 配合 semi-sync(半同步复制) 方案 - 不可行
  • 主从无延迟方案 + semi-sync 方案 能解决过期读问题。

    semi-sync 做了这样的设计:

    • 事务提交的时候,主库把 binlog 发给从库;

    • 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;

    • 主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。

    这样,semi-sync 配合前面关于位点的判断,就能够确定在从库上执行的查询请求,可以避免过期读。

    但这种方案也不太现实,而且没有完全解决问题

    • semi-sync+ 位点判断的方案,只对一主一从的场景是成立的。如果落到其它从库,还是会出现过期读
  • 等主库位点方案 - 不可行
  • select master_pos_wait(file, pos[, timeout]);
    

    这条命令的逻辑如下:

    • 它是在从库执行的;

    • 参数 file 和 pos 指的是主库上的文件名和位置;

    • timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。

    返回结果如下:

    • 如果执行期间,从库同步线程发生异常,则返回 NULL;

    • 如果等待超过 N 秒,就返回 -1;

    • 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。

    • 正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。

    使用方法如下

    • trx1 事务在主库更新完成后,马上执行 show master status 得到当前主库执行到的 File 和Position;

    • 选定一个从库执行查询语句;

    • 在从库上执行 select master_pos_wait(File, Position, 1);

    • 如果返回值是 >=0 的正整数,则在这个从库执行查询语句

    这种也不太现实,想想实现复杂度有多高。

  • 等 GTID 方案 - 不可行
  •  select wait_for_executed_gtid_set(gtid_set, 1);
    

    这条命令的逻辑是:

    • 等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;

    • 超时返回 1。

    等 GTID 的执行流程为:

    • trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;

    • 选定一个从库执行查询语句;

    • 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);

    • 如果返回值是 0,则在这个从库执行查询语句;

    • 否则,到主库执行查询语句。

    总结

    真的是基建要做好,基建好了大家能把精力放到更重要的事情上。如果真出现主从延迟问题,能选择的方案其实比较少。要么就分场景走主库、要么前端sleep(偏临时方案)、要目服务端自己判断一下主从延迟情况。

    这次我们选择redis打点记录,看看效果怎么样吧。

    最后

    大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

    我的个人博客为:shidawuhen.github.io/

    往期文章回顾:

  • 设计模式
  • 招聘
  • 思考
  • 存储
  • 算法系列
  • 读书笔记
  • 小工具
  • 架构
  • 网络
  • Go语言
  • 相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论