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 API | 0 | 1 | IO/SQL 均正常 | 0 秒 |
| Miho | 0 | 0 | IO/SQL 均正常 | 0 秒 |
| PushDeer | 0 | 1 | IO/SQL 均正常 | 0 秒 |
| RustDesk | 0 | 1 | IO/SQL 均正常 | 0 秒 |
| Uptime Kuma | 0 | 0 | IO/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_PATHFROM information_schema.SYSTEM_VARIABLESWHERE VARIABLE_NAME = 'READ_ONLY';两个异常备库都返回:
READ_ONLY = OFFGLOBAL_VALUE_ORIGIN = COMPILE-TIMEGLOBAL_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。声明角色和实际写入保护,值得各查一次。