2394 字
12 分钟

MariaDB 复制正常,备库却没有只读保护

同一晚排查完 PostgreSQL 的主备复制问题,MariaDB 还有两处异常需要处理。

它们的表现没有那么显眼:五套 MariaDB 全部 Ready,备库的 IO、SQL 复制线程正常,Seconds_Behind_Master 也都是 0。但 Miho 和 Uptime Kuma 的两个备库,实际运行的 read_only 却是 0。

2026-09-27 01:35,确认主备角色和活动事务后,我分别在线把这两个备库改成 read_only=ON。到 01:39 再次复核,五套数据库均为主库可写、备库只读,复制状态正常。

这次恢复了当前运行状态,没有完成重启后的防复发。原因在后面:当前 Operator 会根据复制关系认定实例是备库,却没有持续纠正这个实例的只读参数。

环境为 MariaDB 11.8.5、MariaDB Operator/agent 26.3.0。下文时间均为北京时间,省略生产地址、凭据和现场资料路径。

复制线程正常,不等于备库禁止写入#

最初的检查结果如下:

应用主库 read_only备库 read_only备库复制线程采样延迟
Cyxc API01IO/SQL 均正常0 秒
Miho00IO/SQL 均正常0 秒
PushDeer01IO/SQL 均正常0 秒
RustDesk01IO/SQL 均正常0 秒
Uptime Kuma00IO/SQL 均正常0 秒

复制线程负责把主库变化应用到备库;read_only 则用于限制其他客户端直接修改备库。这是两个不同维度的状态。备库复制正常时,仍可能因为缺少只读保护而被误写,随后产生数据分歧。

这里需要准确限定“可写”:read_only=0 表示数据库没有启用这层只读限制,并不表示任意账号都绕过了数据库权限。客户端仍需拥有相应写权限。

对于本次的 MariaDB 11.8,read_only=1 也不是对所有操作一律禁止:复制线程可以继续应用数据,拥有 READ ONLY ADMIN 权限的账号可以绕过限制,临时表等还有例外。因此,不能用一个高权限管理账号的写入结果,简单判断业务账号是否受到保护。MariaDB read_only 说明

这次没有向备库写测试业务数据,也没有仅凭参数异常断言已经发生数据分歧。

先把声明角色和实际连接对上#

不能看到 Pod 名字带 -0 或 -1 就决定它应不应该只读。两套数据库的当前主库恰好不同:Miho 主库是 -1,Uptime Kuma 主库是 -0。

我先检查 MariaDB CR 的 currentPrimary、复制角色,以及 Pod 的角色标签,再在数据库内部查询:

SELECT @@global.hostname AS host,
@@global.server_id AS server_id,
@@global.read_only AS read_only,
@@global.gtid_binlog_pos AS gtid_binlog_pos,
@@global.gtid_slave_pos AS gtid_slave_pos;
SHOW ALL SLAVES STATUS;

重点核对这些字段:

  • Master_Host 确实指向本组现任主库。
  • Slave_IO_Running、Slave_SQL_Running 都为 Yes。
  • Seconds_Behind_Master=0,IO/SQL 错误编号均为 0。
  • 主、备的身份与 CR、Pod 标签及访问入口一致。

两个异常备库的 gtid_binlog_pos 都为空,gtid_slave_pos 有值。操作前还检查了 InnoDB 活动事务与连接类型:采样时没有活跃 InnoDB 事务,修改行数为 0,连接列表只看到本次管理查询和复制线程。

这些检查用于确认本次调整的目标和当时状态,不能替代全量数据一致性校验。

参数来源指向了重启后的默认值#

随后查询变量的实际来源:

SELECT VARIABLE_NAME,
GLOBAL_VALUE,
GLOBAL_VALUE_ORIGIN,
GLOBAL_VALUE_PATH
FROM information_schema.SYSTEM_VARIABLES
WHERE VARIABLE_NAME = 'READ_ONLY';

两个异常备库都返回:

READ_ONLY = OFF
GLOBAL_VALUE_ORIGIN = COMPILE-TIME
GLOBAL_VALUE_PATH = NULL

同时,Cluster 的 myCnf 里没有 read_only,当前 Operator 生成的复制启动配置也没有这个参数。

这说明当时的 OFF 来自启动默认值。此前动态设置过的只读状态,没有成为数据库重启后的配置来源。MariaDB 的动态 SET GLOBAL 设置本来就不会自动跨重启保留。系统变量设置方式

接下来的问题是:既然 Operator 还知道它是备库,为什么没有重新把只读状态设回来?

Operator 已认定它是 Replica,于是跳过配置#

我按线上使用的 v26.3.0 检查了官方源码,对应提交为:

40fc01c4f2b08e1b9e200573ce90510596b4b6d8

这条处理路径可以分成三段:

位置该版本的行为对本次故障的影响
getReplicationRoles按复制关系判断 Primary/Replica只读参数没有参与角色判定
ReconcileReplicationInPod已有 Replica 角色时,普通调谐直接返回没有再次检查和恢复 read_only
ConfigureReplica真正配置备库时才开启只读不是每轮都执行的轻量参数纠偏

对应源码见角色识别、复制调谐和备库配置。

于是就形成了这次观察到的状态:数据库重启后,复制关系还在,线程也恢复正常;Operator 继续把它认作 Replica,但动态的 read_only 已回到默认 OFF。两边的状态并不矛盾,只是控制器没有覆盖这个参数漂移。

这也解释了为什么只看 CR 的 Replica 标签和复制延迟,会漏掉问题。

在线恢复,只改两个备库的运行参数#

确认目标后,实际执行的 SQL 很短:

-- 仅在已重新确认身份的目标备库执行
SET SESSION lock_wait_timeout = 5;
SET GLOBAL read_only = ON;
SELECT @@global.hostname, @@global.read_only;

lock_wait_timeout 只对本次会话生效,用于限制相关元数据锁等待。它不是整条命令的总执行时间上限;本次客户端调用也设置了超时,没有无限等待。

执行前又核对了一次 CR 的现任主库和目标 Pod UID,避免检查与操作之间角色发生变化。两个目标分别在 01:35:34 和 01:35:37 返回 read_only=1,对应主库仍为 0。

本次没有重启 Pod、切换主库、重置 GTID、清理 binlog,也没有停止或重新配置复制线程。调整只涉及两个运行时全局参数,数据库内容、Cluster spec 和主备 Pod 身份保持不变。

这次操作范围较小,没有因此再创建一轮 etcd 快照或数据库备份。参数是可逆的,但回退仍必须重新确认角色,不能因为旧值是 OFF,就在备库上机械恢复那个不安全状态。

为什么没有把 read_only 直接写进公共配置#

一个看起来更彻底的做法,是在主备共用的 myCnf 中加上:

[mariadb]
read_only=ON

这次没有采用。公共配置也会作用于主库,主库重启时同样可能以只读状态启动;当前控制器又可能因为角色已经是 Primary 而跳过重新配置。修补备库问题的同时,就会引入新的主库写入风险。

按固定 Pod ordinal 写脚本也不合适。主备身份会在切换时变化,长期把某个编号当成备库,会把后续维护变成隐患。

还有一种做法是强制让 Operator 重新运行 ConfigureReplica。但 26.3.0 的这条路径还包含 RESET MASTER、停止复制和重新设置上游等操作。为了修正一个只读参数去触发整套复制配置,变更范围明显大于当前需要。

这次先恢复正确运行状态,把持久的角色纠偏留给明确具备该能力的控制器机制。

验证五套数据库,而不只看两个返回值#

设置完成后,重新检查了全部五套 MariaDB、十个实例:

检查项本次结果
主库只读状态五个主库均 read_only=0
备库只读状态五个备库均 read_only=1
复制线程IO/SQL 均为 Yes
采样复制延迟全部为 0 秒
复制错误IO/SQL 错误编号均为 0
Pod 与声明配置全部 Ready,两套目标主备 Pod UID 和 Cluster spec 摘要未变
访问入口primary/secondary Service 分别指向正确的主、备实例

到 01:39 再次查询,两个目标仍保持 read_only=1。Miho 与 Uptime Kuma 的应用副本满足声明值,API 和 etcd 正常,Ceph 保持原有状态,没有因本次参数调整出现新的主备切换。

这里没有进行重启验证、故障注入或登录后的完整业务测试,也没有用高权限账号向备库写业务数据。验收结论限定为当前角色、参数、复制和入口状态。

防复发有上游路径,但这次尚未升级#

调查过程中,已经找到官方 26.10.1 中的持续只读调谐机制,对应提交 e8ece7a8076954674e10e0381571bd80278ac35f。

新的只读调谐逻辑会根据 status.currentPrimaryPodIndex 计算每个实例应有的只读状态,再读取实际值并纠偏。这段逻辑在普通运行模式下也会执行,不要求人工开启维护模式。

26.10.0 发布说明记录了只读漂移修正,并明确建议跳过 26.10.0、使用补丁版 26.10.1。同一轮版本还涉及半同步复制角色等行为变化,因此不能把共享 Operator 升级当成只修改两个 SQL 参数。

真正安排升级前,还要核对 CRD、Operator、agent 的兼容关系,对五套数据库的影响,固定现有数据库镜像,以及备份和回退方法。升级后也需要验证重启后的纠偏过程,才能把防复发工作闭环。

这篇记录的终点是:两个备库已经恢复只读,五套数据库当前复制正常;共享 Operator 未升级,重启后再次丢失只读保护的风险仍在。

以后检查 MariaDB 主备状态,除了确认复制源、线程和延迟,还需要直接查询 @@global.read_only。声明角色和实际写入保护,值得各查一次。