用 MySQL HeatWave 高可用与读副本构建抗脆弱 API

2026-05-21 34 预计阅读时间:1 分钟
来源:blogs.oracle.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

云上应用对可用性的要求已经从"尽量不宕机"演变为"宕机是常态,代码要扛住"。数据库主节点切换、副本复制延迟、网络瞬断——这些在分布式环境中不是意外,而是日常。MySQL HeatWave 提供了内置的高可用(HA)和读副本(Read Replica)机制,但光靠数据库本身不够,API 层必须主动配合,才能把"基础设施抖动"对用户的影响降到最低。

瞬断不是 Bug,是生产常态

HeatWave HA 的底层逻辑是:主节点不可用时,服务自动在另一个实例上恢复,通常在几十秒内完成。读副本则把读流量分散到不同节点,同时提供额外的容灾能力。

问题在于——切换期间,应用会遭遇连接拒绝、DNS 解析滞后、事务中断等瞬态错误。如果 API 层把这类错误当作"致命异常"直接抛给用户,那 HA 再快也救不了体验。

正确的姿势是:把瞬断归类为可恢复错误,在代码层自动重试或降级

读副本的分流与风险

HeatWave 读副本的核心价值:

  • 读查询不争主节点资源,写操作不受读流量拖慢。
  • 主节点故障时,读副本仍可返回数据(可能是稍旧的数据)。
  • 区域级故障时,跨区域读副本提供就近读取能力。

但读副本有一个必须正视的边界:复制延迟。主节点写入后,副本同步存在时间差,通常在毫秒到秒级,但在高写入负载或网络抖动时可能拉长。如果 API 要求"写后立即读到最新数据",直接走读副本就会返回旧值——这不是 Bug,是异步复制的物理约束。

应对策略很直接:写后读走主节点,纯读走副本,并在业务上容忍副本数据的秒级滞后。

API 层的三个韧性设计

1. 连接层:自动重试 + 读写分离

用连接池配合读写路由,是最低成本的韧性改造。以下是一个基于 Python 的实践示例,展示如何在 HeatWave 场景下做连接重试和读写分流:

import mysql.connector
from mysql.connector import errorcode
from mysql.connector.pooling import MySQLConnectionPool
import time
import random

# 主节点与读副本配置
DB_CONFIG_PRIMARY = {
    "host": "heatwave-primary.example.com",
    "port": 3306,
    "user": "app_user",
    "password": "app_secret",
    "database": "orders_db",
}

DB_CONFIG_REPLICA = {
    "host": "heatwave-replica.example.com",
    "port": 3306,
    "user": "app_readonly",
    "password": "app_readonly_secret",
    "database": "orders_db",
}

# 连接池:主节点和读副本各一个
primary_pool = MySQLConnectionPool(pool_name="primary", pool_size=5, **DB_CONFIG_PRIMARY)
replica_pool = MySQLConnectionPool(pool_name="replica", pool_size=10, **DB_CONFIG_REPLICA)

# 瞬态错误码列表——这些是 HA 切换或网络瞬断时常见的错误
TRANSIENT_ERROR_CODES = {
    errorcode.CR_CONN_HOST_ERROR,
    errorcode.CR_CONNECTION_ERROR,
    errorcode.CR_SERVER_GONE_ERROR,
    errorcode.CR_SERVER_LOST,
    errorcode.ER_LOCK_DEADLOCK,
}

def execute_query(sql, params=None, is_read=True, max_retries=3, backoff_base=0.2):
    """
    执行查询,自动选择主节点或读副本,并对瞬态错误做重试。
    is_read=True  → 读副本优先,失败后回退主节点
    is_read=False → 强制走主节点(写操作或写后一致性读)
    """
    pools = [replica_pool, primary_pool] if is_read else [primary_pool]

    for attempt in range(max_retries):
        for pool in pools:
            try:
                conn = pool.get_connection()
                cursor = conn.cursor(dictionary=True)
                cursor.execute(sql, params)
                if sql.strip().upper().startswith("SELECT"):
                    result = cursor.fetchall()
                else:
                    result = {"affected_rows": cursor.rowcount}
                conn.commit()
                cursor.close()
                conn.close()
                return result
            except mysql.connector.Error as err:
                # 瞬态错误:重试
                if err.errno in TRANSIENT_ERROR_CODES:
                    wait = backoff_base * (2 ** attempt) + random.uniform(0, 0.1)
                    print(f"[Retry {attempt+1}] 瞬态错误 {err.errno}: {err.msg}, 等待 {wait:.2f}s")
                    time.sleep(wait)
                    continue
                # 非瞬态错误:直接抛出
                raise
    raise RuntimeError(f"查询在 {max_retries} 次重试后仍失败: {sql}")

# ---- 使用示例 ----

# 纯读:走读副本
orders = execute_query(
    "SELECT order_id, status FROM orders WHERE user_id = :uid",
    params={"uid": 42},
    is_read=True,
)

# 写操作:走主节点
execute_query(
    "UPDATE orders SET status = 'shipped' WHERE order_id = :oid",
    params={"oid": 1001},
    is_read=False,
)

# 写后一致性读:强制走主节点
updated = execute_query(
    "SELECT status FROM orders WHERE order_id = :oid",
    params={"oid": 1001},
    is_read=False,  # 写后读必须走主节点
)

关键设计点:

  • TRANSIENT_ERROR_CODES 覆盖了连接断开、服务消失、死锁等常见瞬态错误,只对这些做重试,非瞬态错误(如语法错误)直接抛出。
  • 读操作先尝试副本,副本失败后回退主节点——副本瞬断不影响读可用性。
  • 写操作和写后一致性读只走主节点,避免复制延迟导致数据不一致。
  • 重试间隔用指数退避 + 随机抖动,避免 HA 切换期间大量连接同时冲击新主节点。

2. 降级层:读副本延迟容忍策略

当复制延迟拉长时,API 可以选择降级而非报错。一个实用做法是在读副本查询前检查延迟阈值:

def read_with_staleness_tolerance(sql, params=None, max_staleness_seconds=5):
    """
    读副本查询,容忍指定秒数的复制延迟。
    超过阈值则回退主节点。
    """
    try:
        conn = replica_pool.get_connection()
        cursor = conn.cursor(dictionary=True)

        # 检查复制延迟(HeatWave/MySQL 通过 SHOW REPLICA STATUS 获取 Seconds_Behind_Source)
        cursor.execute("SHOW REPLICA STATUS")
        replica_status = cursor.fetchone()
        lag = float(replica_status.get("Seconds_Behind_Source", 0))

        if lag > max_staleness_seconds:
            print(f"[Fallback] 副本延迟 {lag}s 超过阈值 {max_staleness_seconds}s,回退主节点")
            cursor.close()
            conn.close()
            return execute_query(sql, params, is_read=False, max_retries=1)

        cursor.execute(sql, params)
        result = cursor.fetchall()
        cursor.close()
        conn.close()
        return result
    except mysql.connector.Error as err:
        # 检查失败时安全回退主节点
        print(f"[Fallback] 副本查询异常: {err}, 回退主节点")
        return execute_query(sql, params, is_read=False, max_retries=1)

# 用户查看历史订单——容忍 5 秒延迟完全够用
history = read_with_staleness_tolerance(
    "SELECT order_id, created_at FROM orders WHERE user_id = :uid ORDER BY created_at DESC LIMIT 20",
    params={"uid": 42},
    max_staleness_seconds=5,
)

业务场景决定阈值:订单历史列表容忍 5 秒没问题,但支付状态查询可能需要阈值设为 0(强制走主节点)。

3. 流量层:健康检查与优雅降级

在 Kubernetes 或类似编排环境中,API 服务自身也要对数据库做健康探测,避免在数据库切换期涌入无效请求:

# Kubernetes readinessProbe——检测数据库连接可用性
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-api
spec:
  template:
    spec:
      containers:
        - name: order-api
          image: order-api:1.4.0
          readinessProbe:
            exec:
              command:
                - python
                - -c
                - |
                  import mysql.connector, sys
                  try:
                      conn = mysql.connector.connect(
                          host="heatwave-primary.example.com",
                          user="probe_user",
                          password="probe_secret",
                          connect_timeout=3
                      )
                      conn.ping(reconnect=True, attempts=2)
                      conn.close()
                  except Exception as e:
                      print(f"DB not ready: {e}")
                      sys.exit(1)
            initialDelaySeconds: 10
            periodSeconds: 15
            failureThreshold: 3
          env:
            - name: DB_PRIMARY_HOST
              value: "heatwave-primary.example.com"
            - name: DB_REPLICA_HOST
              value: "heatwave-replica.example.com"

当主节点切换时,readinessProbe 连续失败 3 次(约 45 秒),Pod 从 Service Endpoints 中摘除,流量不再进入,直到数据库恢复探测成功后自动加回。这比在代码里硬等更优雅——编排层自动完成流量摘除和恢复。

部署与运维清单

上线前逐项确认:

检查项 说明
读副本路由策略 纯读走副本,写后一致性读走主节点,代码中显式标注
瞬态错误重试 连接断开、死锁等做有限次指数退避重试,非瞬态错误不重试
复制延迟阈值 每类 API 根据业务容忍度设定不同 staleness 上限
健康探测 K8s readinessProbe 覆盖数据库连接,failureThreshold ≥ 3
DNS 缓存时间 HeatWave HA 切换依赖 DNS 更新,客户端 DNS TTL 应 ≤ 30 秒
连接池参数 connect_timeout 设 3-5 秒,pool_recycle 避免长连接在切换后僵死
监控面板 副本延迟、重试次数、主节点切换事件三项必看

核心思路一句话:数据库 HA 是基础设施的能力,API 韧性是应用层的责任。HeatWave 保证数据库在几十秒内恢复,API 层的重试、分流和降级保证用户在这几十秒内要么无感,要么只看到轻微延迟而非报错页面。


相关推荐