如果函数执行失败,如何配置指数退避重试?

解读

面试官真正想考察的是:

  1. 你是否理解 Cloud SQL 本身没有原生“函数”概念,失败重试通常发生在调用 Cloud SQL 的客户端代码或 Cloud Functions/Cloud Run 等无服务器计算层
  2. 你是否能把“指数退避”与 Google Cloud 的配额、限流、连接断闪场景联系起来,并给出可落地的代码级配置
  3. 你是否知道国内网络环境下,北京/上海/深圳多 VPC 跨可用区时,TCP 重传与 IAM 认证超时对退避参数的影响;
  4. 你是否能区分客户端重试服务端事务重试的边界,避免幂等性灾难。

知识点

  1. Cloud SQL 连接失败根因:

    • IAM 认证缓存刷新(~60 s TTL)
    • 连接数打满(默认 100 核 4000 连接)
    • 故障转移(~30 s 只读中断)
    • 国内 BGP 线路抖动(丢包 1% 即可触发重试)
  2. 指数退避算法:
    延迟 = baseDelay × 2^attempt × (1 + jitter),上限 maxDelay;
    Google 官方推荐:baseDelay=1 s,maxDelay=64 s,jitter∈[0,0.3],总重试 7 次≈127 s 窗口。

  3. Cloud 客户端库内置策略:

    • Cloud SQL Auth Proxy v2 自带 –max-connections、–auto-iam-authn,无退避,需在上层封装;
    • Java Spring Cloud GCP 提供 RetryTemplate,可注入 ExponentialBackOffPolicy;
    • Go cloud.google.com/go/sqlproxy 需手动实现 gax.CallOption 的 gax.WithRetry;
    • Python sqlalchemy-google-cloud-connector 0.3+ 支持 retry=retry_exponential(increment=2, max=64)。
  4. 无服务器场景:

    • Cloud Functions(2nd gen) 在代码层捕获异常,利用 google.api_core.retry.Retry;
    • Cloud Run 需把重试策略写在容器启动入口,不可依赖 K8s 层重试,因为国内私有集群版本默认关闭 http-grpc-retry。
  5. 幂等性保障:

    • 使用 UUID 主键INSERT … ON CONFLICT DO NOTHING
    • 事务内先 SELECT FOR UPDATE,再 UPDATE,避免 幻读导致重复扣款。
  6. 观测与告警:

    • Cloud Monitoring 自定义指标 cloudsql.googleapis.com/database/mysql/connection_errors;
    • 国内短信告警需通过 Google Cloud 中国合作伙伴(如神州数码)开通 Cloud Alert 通道,否则 429 Webhook 被墙。

答案

国内 VPC 内 Cloud Functions 访问 Cloud SQL(MySQL 8.0)为例,给出生产级 Python 代码

import os, uuid, sqlalchemy, google.cloud.sql.connector
from google.api_core.retry import Retry, exponential_sleep_generator
import functions_framework

# 1. 配置指数退避:base=1s,max=64s,jitter=0.3,最多 7 次
def custom_retry_predicate(exc):
    # 只重试连接类错误,业务异常直接抛
    return isinstance(exc, (sqlalchemy.exc.OperationalError,
                            sqlalchemy.exc.DBAPIError))

retry_obj = Retry(
    predicate=custom_retry_predicate,
    initial=1.0,
    maximum=64.0,
    multiplier=2.0,
    deadline=127.0,
    jitter=lambda x: x * (1 + 0.3 * (uuid.uuid4().int % 1000) / 1000)
)

# 2. 创建带重试的连接器
connector = google.cloud.sql.connector.Connector(
    ip_type="PRIVATE",  # 国内降低公网抖动
    enable_iam_auth=True,
    refresh_strategy="LAZY"  # 避免 IAM 缓存刷新风暴
)

# 3. 获取连接并执行,重试逻辑在 connector.connect 内部
@functions_framework.http
def entry(request):
    try:
        conn = connector.connect(
            os.environ["INSTANCE_CONNECTION_NAME"],
            "pymysql",
            db="orders",
            user="sa"
        )
        with conn.begin():
            order_id = str(uuid.uuid4())
            conn.exec_driver_sql(
                "INSERT INTO orders(id,status) VALUES(%s,'PAID') ON DUPLICATE KEY UPDATE status=status",
                (order_id,)
            )
        return "ok", 200
    except Exception as e:
        # 4. 如果仍失败,返回 500 触发 Cloud Functions 默认退避(3 次)
        # 但因为我们已经做了 7 次指数退避,这里直接抛
        raise

关键点

  • 把重试收敛在数据库连接层,业务层不再感知;
  • 使用 ON DUPLICATE KEY 保证幂等;
  • PRIVATE IP 规避国内公网丢包;
  • 通过 Retry.deadline=127 s 把总耗时控制在 Cloud Functions 540 s 超时以内;
  • 日志打印 attempt 次数与真实 sleep 值,方便后续在 Cloud Logging 按 trace_id 串联。

拓展思考

  1. 如果业务要求零丢消息,可把失败事件写入 Pub/Sub 死信主题,再启动 Cloud Run Job指数退避 + 业务补偿,实现跨地域双写
  2. 国内金融客户常要求RPO=0,可开启 Cloud SQL 高可用(区域级) 并设置 maintenance_window=cn-peak-off,但故障切换时仍会出现 30 s 只读,此时需把退避初始值调到 5 s,避免前 3 次重试全部撞墙;
  3. 并发量 > 5000 QPS 时,建议把退避参数外置到 FirestoreConsul,实现热调参,避免重启函数;
  4. 最后,所有重试必须配合告警:在 Cloud Alerting Policy 里设置“连续 5 min 内重试次数 > 100”即电话告警,否则指数退避可能掩盖雪崩级故障