Atlas 中间件自动故障转移

解读

在国内互联网公司的 PHP 面试里,数据库高可用是“必答题”。Atlas 是 360 开源的 MySQL 代理中间件,很多中小型公司用它做读写分离 + 故障转移,面试官问“自动故障转移”并不是让你背文档,而是想确认三件事:

  1. 你是否真的在生产环境踩过主库宕机的坑;
  2. 是否理解 Atlas 的“无脑切换”与业务幂等、事务补偿之间的冲突;
  3. 能否用 PHP 代码把“感知切换 → 重试 → 降级”这三步写利索。 答得太浅(“改配置就行”)会被认为没实战;答得太深(“改 Atlas 源码”)又容易超时。把“机制 + 配置 + PHP 兜底”三段讲清,最能体现后端工程师的“靠谱度”。

知识点

  1. Atlas 故障转移触发条件
    • 后端 MySQL 连续心跳失败次数 ≥ backend-max-failure(默认 3)
    • 心跳间隔 backend-idle-timeout(默认 30 s)
    • 满足条件后 Atlas 把 master 标记为 OFFLINE,把第一台 slave 提升为 new master,并改写内部 ip_pool 映射;对客户端连接池完全透明。
  2. 切换粒度与一致性
    • Atlas 只做“实例级”切换,不做数据补偿,可能出现 1~2 秒写丢失(RPO>0)。
    • 原主库如果“假死”又恢复,会形成双主,必须借助 kill 旧连接 + 设置 read_only=1 手工隔离。
  3. PHP 侧必须做的三件事
    • 连接层:PDO::ATTR_ERRMODE 设 EXCEPTION,捕获 2003/2013/2006 三类错误码。
    • 重试层:指数退避 3 次,间隔 100 ms、200 ms、400 ms,防止 Atlas 正在重启旧连接。
    • 业务层:事务型写操作加唯一索引 + 幂等令牌(如订单号),切换后重试不重复落库。
  4. 国内云环境差异
    • 阿里云 RDS 自带高可用,Atlas 只放在本地 IDC 做“多云混合”场景;腾讯云 CDB 的 GTID 与 Atlas 心跳端口冲突,需要把 heartbeat-user 设成 ‘repl’ 账号并开 3307 端口。
  5. 监控与告警
    • 在 Atlas 的 stats 接口(默认 1234)拉取 backend_status=OFFLINE 次数,通过 Falcon 或夜莺推送钉钉告警。
    • PHP 端把重试次数写入 Redis 队列,方便 SRE 做故障复盘。

答案

“Atlas 的自动故障转移分三层:中间件层、驱动层、业务层,我分别做过配置和代码兜底。

  1. 中间件层 在 instance.cnf 把 backend-max-failure 调成 2 次、backend-idle-timeout 调成 15 秒,缩短探测周期;同时把 slave 节点按优先级 1、2、3 排列,确保第一从库延迟最低。为了防止‘脑裂’,我把旧主库通过 cron 每 10 秒执行 mysql -e "set global read_only=1",一旦 Atlas 把它踢掉就自动只读。

  2. 驱动层 PHP 用 PDO 长连接,捕获异常码:

    try {
        $pdo->exec($sql);
    } catch (PDOException $e) {
        $code = $e->getCode();
        if (in_array($code, ['2003', '2013', '2006'])) {
            usleep(100000);          // 第一次 100 ms
            $pdo->exec($sql);        // 重试
        } else {
            throw $e;
        }
    }
    

    生产环境我封装成 RetryablePDO,对业务透明。

  3. 业务层 订单写库带唯一索引 order_sn,重试时如果主键冲突直接返回上次写入的 id,保证幂等;同时把重试事件打到 Kafka,供 BI 校正金额。

上线半年,Atlas 做过 3 次主库宕机演练,RPO 最大 1.8 秒,PHP 端无 500 错误,符合公司 SLA。”

拓展思考

如果公司从 Atlas 迁到 ShardingSphere-Proxy,故障转移模型从“代理层选主”变成“ZK 协调选主”,PHP 侧代码几乎不用改,但要把重试异常码扩展到 8001(ShardingSphere 的“Circuit Break”)。此外,Atlas 不支持 GTID,无法做“防回环”校验,未来想做到 RPO=0,需要下沉到 MySQL MGR + 半同步复制,PHP 端再引入 Saga 事务模式补偿,高可用才算“毕业”。