site logo

Marico's space

从零到多区域:基于 Cloud Run 的高可用无服务器架构与跨区域故障转移及恢复

编程技术 2026-07-24 14:50:03 4

最近折腾了 Google Cloud Run 的多区域高可用方案,踩了几个坑,这篇把问题说清楚。

大多数团队意识到需要多区域架构的时候,往往都是在发生故障之后。不管你是做全球电商平台、实时游戏 API,还是金融服务应用,用户都期望服务随时可用。每个工程团队基本都会经历这样的对话——通常始于一次故障复盘。某个地区阿里云节点宕机,或者 Cloud Run 服务遭遇冷启动峰值,又或者单区域部署无法同时满足北京、上海和广州用户的延迟需求,这些痛苦终于让某人发问:为什么我们只部署在一个区域?

答案通常就三件事:觉得太复杂、觉得太贵、或者没人排期做。

2026 年 7 月,Google 将 Cloud Run Service Health(服务健康状态)推向正式可用,时机耐人寻味。六天前,Google 荷兰数据中心因断电导致三个服务下线。正式版带来了 Cloud Run 跨区域自动故障转移功能,Google 称之为两步配置:添加就绪探针、将最小实例数设为至少 1。负载均衡器自动完成其余工作。

本文覆盖完整架构、Service Health 是什么、就绪探针如何支撑它、如何正确配置全局负载均衡器、以及如何验证故障转移实际生效。同时也会讨论生产环境的细节。

发生了什么变化:Service Health 和就绪探针

在 Service Health 出现之前,多区域 Cloud Run 需要你在应用中实现 /health 端点,并在负载均衡器层面配置单独的 HTTPS 健康检查。这能工作,但存在明显缺陷。负载均衡器的健康检查只知道 Cloud Run 服务端点是否响应,不知道背后各个容器实例是否真正准备好处理流量。

Service Health 引入了两个新能力来填补这个空白:

就绪探针(Readiness probes)在容器实例级别运行。Cloud Run 定期向每个运行中容器实例上的指定路径发送 HTTP 请求。如果探针失败,Cloud Run 停止向该实例路由请求,直到探针再次成功。关键点:就绪探针失败不会杀死实例(那是存活探针的工作),只是将该实例标记为不ready接收流量。

Service Health 将区域内所有容器实例的就绪状态聚合为单个区域健康信号。这个聚合后的健康状态通过该区域的 Serverless NEG(网络端点组)暴露。当全局负载均衡器读取 NEG 的健康状态,发现某个区域不健康——因为足够多的实例就绪探针失败——它自动将流量重新路由到健康区域。当故障区域恢复后,流量无需任何运维操作就会逐渐恢复。

结果:故障转移和故障恢复能力现已完全自动化,由真实的实例级健康状态触发,而非合成端点检查。

Container instance (readiness probe fails) │ ▼
Cloud Run aggregates probe results across all instances in the region to determine the overall health status of each regional service │ ▼
Service Health: region marked UNHEALTHY │ ▼
Serverless NEG reports unhealthy status to Global Load Balancer │ ▼
Load Balancer stops routing to this region → shifts traffic to healthy region │ ▼
Region recovers → Load Balancer gradually restores traffic

所有 Cloud Run 区域都可使用此功能,不额外收费(仅支付就绪探针运行期间的 CPU 和内存消耗)。

架构概览

 ┌──────────────────────────────────┐ │ Global Anycast IP (single IP) │ │ + SSL Certificate (managed) │ └───────────────┬──────────────────┘ │ ┌───────────────▼──────────────────┐ │ Global External HTTP(S) LB │ │ (URL map + forwarding rules) │ └──────┬──────────────────┬────────┘ │ │ ┌──────────────────▼──┐ ┌─────▼──────────────────┐ │ Serverless NEG │ │ Serverless NEG │ │ africa-south1 │ │ us-central1 │ │ (Service Health │ │ (Service Health │ │ status: healthy) │ │ status: healthy) │ └──────────┬──────────┘ └───────────┬────────────┘ │ │ ┌───────────────▼───────────┐ ┌──────────────▼──────────────┐ │ Cloud Run Service │ │ Cloud Run Service │ │ africa-south1 │ │ us-central1 │ │ Readiness probe: /health │ │ Readiness probe: /health │ │ min-instances: 1+ │ │ min-instances: 1+ │ │ (auto-scales 0–N) │ │ (auto-scales 0–N) │ └───────────────────────────┘ └─────────────────────────────┘

前置条件和设置

export PROJECT_ID="your-project-id"
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \ --format="value(projectNumber)")
export SERVICE="my-api"
export REGION_A="africa-south1"
export REGION_B="us-central1"
export DOMAIN="api.yourdomain.com"
export IMAGE="gcr.io/${PROJECT_ID}/${SERVICE}:latest" gcloud config set project $PROJECT_ID # Enable required APIs
gcloud services enable \ run.googleapis.com \ compute.googleapis.com \ artifactregistry.googleapis.com \ cloudbuild.googleapis.com \ networkservices.googleapis.com # Grant Cloud Build service account the Cloud Run builder role
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${PROJECT_NUMBER}- compute@developer.gserviceaccount.com" \ --role="roles/run.builder"

第一步:实现就绪探针端点

第一步也是 Service Health 能否工作的最关键步骤——在应用中添�的就绪探针端点。与之前的做法不同,那个 /health 端点是为了负载均衡器的利益,而这个端点由 Cloud Run 直接在每个容器实例上调用,用于判断每个实例的就绪状态。

官方文档中有两条规则很重要:

  • 使用 HTTP/1 端点(Cloud Run 默认值,不是 HTTP/2)
  • 端点路径必须与探针配置中的 path 完全匹配
// Node.js / Express
// Lightweight — no DB calls, no downstream dependencies
// This runs frequently on every instance
app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', region: process.env.REGION, timestamp: new Date().toISOString() });
}); // If you want the probe to reflect actual readiness
// (e.g. connection pool initialised), you can check internal state:
let isReady = false;
app.get('/health', (req, res) => { if (!isReady) { return res.status(503).json({ status: 'not_ready' }); } res.status(200).json({ status: 'healthy', region: process.env.REGION });
}); // Set isReady = true after your startup tasks complete
pool.connect().then(() => { isReady = true; });
# Python / FastAPI
import os
from datetime import datetime
from fastapi import FastAPI, Response app = FastAPI()
is_ready = False @app.get("/health")
async def readiness_probe(response: Response): if not is_ready: response.status_code = 503 return {"status": "not_ready"} return { "status": "healthy", "region": os.environ.get("REGION"), "timestamp": datetime.utcnow().isoformat() } @app.on_event("startup")
async def startup_event(): global is_ready # Initialise connections, warm caches, etc. await init_database_pool() is_ready = True

is_ready 模式是相比基本 /health 端点的关键升级。每个实例上的就绪探针会在启动任务完成前返回 503,防止负载均衡器将流量路由到正在运行但尚未就绪的实例。

第二步:部署到多区域并配置就绪探针

gcloud run deploy 支持在单条命令中部署到多个区域,--readiness-probe 标志在部署时附加探针配置。故障转移需要至少两个不同区域的服务。

# Deploy to both regions simultaneously with readiness probe
gcloud run deploy $SERVICE \ --image=$IMAGE \ --regions=$REGION_A,$REGION_B \ --min=1 \ --max-instances=100 \ --concurrency=80 \ --cpu=1 \ --memory=512Mi \ --timeout=30s \ --readiness-probe="httpGet.path=/health" \ --set-env-vars="ENV=production" \ --allow-unauthenticated

--readiness-probe="httpGet.path=/health" 标志是在部署时配置探针的新方式。你也可以配置额外的探针参数:

# Full readiness probe configuration
gcloud run deploy $SERVICE \ --image=$IMAGE \ --regions=$REGION_A,$REGION_B \ --min=2 \ --readiness-probe="httpGet.path=/health,periodSeconds=10,failureThreshold=3,successThreshold=1,timeoutSeconds=5"

或者通过 YAML 服务定义(Terraform 友好方式):

# service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata: name: my-api
spec: template: metadata: annotations: autoscaling.knative.dev/minScale: "1" autoscaling.knative.dev/maxScale: "100" spec: containers: - image: gcr.io/PROJECT_ID/my-api:latest resources: limits: cpu: "1" memory: 512Mi env: - name: ENV value: production readinessProbe: httpGet: path: /health periodSeconds: 10 failureThreshold: 3 successThreshold: 1 timeoutSeconds: 5 livenessProbe: httpGet: path: /health periodSeconds: 30 failureThreshold: 3

就绪探针和存活探针的区别

两种探针类型都在 Cloud Run 上受支持。理解它们的区别至关重要:

就绪探针失败:Cloud Run 停止向该实例路由请求。实例继续运行。一旦探针再次成功,路由恢复。Service Health 将这些聚合起来判断区域健康状态。

存活探针失败:Cloud Run 重启容器实例。使用存活探针检测死锁或无法恢复的卡死状态。

对于 Service Health 的自动故障转移,就绪探针才是关键。存活探针是补充——它们处理实例级恢复,而就绪探针处理流量路由决策。

第三步:配置全局外部应用负载均衡器

使用新的 Service Health 模型,负载均衡器配置比以前简单——你不再需要在负载均衡器层面配置单独的 HTTPS 健康检查。Service Health 通过 Serverless NEG 本身暴露区域健康状态。

创建后端服务

# Single backend service, both regions are added as NEG backends
gcloud compute backend-services create $SERVICE-bs \ --load-balancing-scheme=EXTERNAL_MANAGED \ --global

注意:与之前每个区域使用独立后端服务的方法不同,Service Health 使用单个后端服务,包含多个区域 NEG 后端。负载均衡器从每个 NEG 读取健康状态并据此路由。

预留全局静态 IP

gcloud compute addresses create $SERVICE-ip \ --network-tier=PREMIUM \ --ip-version=IPV4 \ --global export GLOBAL_IP=$(gcloud compute addresses describe $SERVICE-ip \ --global --format="get(address)") echo "Global IP: ${GLOBAL_IP}"
# → Update your DNS A record to this IP before proceeding

创建 URL 映射、代理和转发规则

# Create a URL map to route incoming requests to the backend service:
gcloud compute url-maps create $SERVICE-lb \ --default-service=$SERVICE-bs # For HTTPS (recommended for production):
# Create Google-managed SSL certificate
gcloud compute ssl-certificates create $SERVICE-ssl \ --domains=$DOMAIN \ --global # Create the target HTTPS proxy to route requests to your URL map:
gcloud compute target-https-proxies create $SERVICE-https-proxy \ --url-map=$SERVICE-lb \ --ssl-certificates=$SERVICE-ssl # Create the HTTPS forwarding rule to route incoming requests to the proxy:
gcloud compute forwarding-rules create $SERVICE-https-fr \ --load-balancing-scheme=EXTERNAL_MANAGED \ --network-tier=PREMIUM \ --address=$SERVICE-ip \ --target-https-proxy=$SERVICE-https-proxy \ --global \ --ports=443 # HTTP forwarding rule (redirect to HTTPS)
gcloud compute target-http-proxies create $SERVICE-http-proxy \ --url-map=$SERVICE-lb gcloud compute forwarding-rules create $SERVICE-http-fr \ --load-balancing-scheme=EXTERNAL_MANAGED \ --network-tier=PREMIUM \ --address=$SERVICE-ip \ --target-http-proxy=$SERVICE-http-proxy \ --global \ --ports=80

第四步:创建 Serverless NEG 并附加它们

# Serverless NEG for africa-south1
gcloud compute network-endpoint-groups create $SERVICE-neg-$REGION_A \ --region=$REGION_A \ --network-endpoint-type=serverless \ --cloud-run-service=$SERVICE # Serverless NEG for us-central1
gcloud compute network-endpoint-groups create $SERVICE-neg-$REGION_B \ --region=$REGION_B \ --network-endpoint-type=serverless \ --cloud-run-service=$SERVICE # Add both NEGs to the single backend service
gcloud compute backend-services add-backend $SERVICE-bs \ --global \ --network-endpoint-group=$SERVICE-neg-$REGION_A \ --network-endpoint-group-region=$REGION_A gcloud compute backend-services add-backend $SERVICE-bs \ --global \ --network-endpoint-group=$SERVICE-neg-$REGION_B \ --network-endpoint-group-region=$REGION_B

此时 Service Health 已激活。Cloud Run 在两个区域的所有实例上运行就绪探针,将结果聚合为区域健康信号,负载均衡器通过 Serverless NEG 读取该信号。

第五步:用 Cloud Monitoring 监控 Service Health

Service Health 通过 Cloud Monitoring 暴露两个指标,你应该从第一天就开始追踪:

run.googleapis.com/container/instance_count_with_readiness——每个区域通过就绪探针的实例数量。观察这个指标可以实时看到每个区域实例池的健康状态。

run.googleapis.com/service_health_count——区域 Cloud Run 服务健康状态,报告给负载均衡器。可能的值:HEALTHYUNHEALTHYUNKNOWN。负载均衡器用这个来做故障转移决策。UNKNOWN 在服务积累足够探针数据来判断健康状态之前报告,通常在部署后最初几分钟内。

# View current service health status via gcloud
gcloud run services describe $SERVICE \ --region=$REGION_A \ --format="value(status.conditions)" # Or check via the Console:
# Cloud Run → your service → Metrics tab → "Instance count with readiness"

设置告警策略,当任何区域的 service_health_count 转为 UNHEALTHY 时触发:

# alert-policy.yaml
displayName: "Cloud Run region unhealthy — failover active"
conditions:
- displayName: "Service health UNHEALTHY" conditionThreshold: filter: | resource.type="cloud_run_revision" metric.type="run.googleapis.com/service_health_count" metric.labels.health_status="UNHEALTHY" comparison: COMPARISON_GT thresholdValue: 0 duration: 60s aggregations: - alignmentPeriod: 60s perSeriesAligner: ALIGN_MAX
notificationChannels:
- projects/${PROJECT_ID}/notificationChannels/YOUR_CHANNEL_ID
documentation: content: | A Cloud Run region has become unhealthy and traffic is being rerouted to the remaining healthy region(s). Investigate the failing region's logs and instance readiness metrics immediately.

第六步:测试故障转移

测试不是可选项——这是在真实故障被用户发现之前,验证故障转移是否真正有效的唯一方法。

方法一:使用示例应用的内置开关

Google Cloud 示例应用(golang-samples/run/service-health)的 UI 中包含一个内置开关,可以将区域标记为不健康。对于生产应用,请使用方法二。

方法二:通过环境变量强制就绪探针失败

# Redeploy africa-south1 with a flag that makes /health return 503
gcloud run deploy $SERVICE \ --image=$IMAGE \ --region=$REGION_A \ --set-env-vars="FORCE_UNHEALTHY=true,ENV=production"

在你的应用中检查这个变量:

app.get('/health', (req, res) => { if (process.env.FORCE_UNHEALTHY === 'true') { return res.status(503).json({ status: 'forced_unhealthy' }); } res.status(200).json({ status: 'healthy', region: process.env.REGION });
});

观察故障转移序列

# Get load balancer IP
export LBIP=$(gcloud compute addresses describe $SERVICE-ip \ --global --format='value(address)') # Continuous requests — watch region shift in responses
while true; do RESPONSE=$(curl -s https://${DOMAIN}/health) echo "$(date '+%H:%M:%S') — $RESPONSE" sleep 2
done

你应该观察到:

  1. 请求显示 "region": "africa-south1"——正常运行
  2. 探针失败在实例间传播时出现响应混合
  3. 所有请求显示 "region": "us-central1"——故障转移完成
  4. africa-south1service_health_count 指标显示 UNHEALTHY

恢复区域:

gcloud run deploy $SERVICE \ --image=$IMAGE \ --region=$REGION_A \ --set-env-vars="ENV=production"

当实例通过就绪探针、Service Health 恢复到 HEALTHY 后,流量逐渐返回 africa-south1

使用就绪探针的安全发布策略

新就绪探针模型最强大的功能之一是能够跨区域做金丝雀部署,通过 Service Health 自动回滚。

官方推荐的发布流程:

# Step 1: Deploy new revision to ONE region with 1% traffic
gcloud run deploy $SERVICE \ --image=$IMAGE_NEW \ --region=$REGION_A \ --readiness-probe="httpGet.path=/health" \ --min=1 \ --no-traffic # Deploy but send no traffic yet # Step 2: Send 1% of traffic to new revision in REGION_A only
gcloud run services update-traffic $SERVICE \ --region=$REGION_A \ --to-revisions=LATEST=1 # Step 3: Monitor readiness metric
# run.googleapis.com/container/instance_count_with_readiness
# If this stays healthy, continue increasing traffic # Step 4: Ramp to 100% in REGION_A
gcloud run services update-traffic $SERVICE \ --region=$REGION_A \ --to-revisions=LATEST=100 # Step 5: Once REGION_A service_health_count is stable HEALTHY,
# deploy to REGION_B
gcloud run deploy $SERVICE \ --image=$IMAGE_NEW \ --region=$REGION_B \ --readiness-probe="httpGet.path=/health" \ --min=1

如果新版本的就绪探针在 REGION_A 失败,Service Health 将该区域标记为不健康,负载均衡器将流量路由到 REGION_B(仍运行旧版本),你获得了自动回滚,无需任何手动步骤。

Service Health(正式版)已知的限制

官方文档列出了几个值得在构建前了解的限值:

  • 需要最小实例数。你必须为每个区域配置至少一个最小实例,Service Health 才能计算健康状态。实例数为零的区域无法报告健康状态,意味着冷启动区域无法参与自动故障转移。
  • 至少两个区域。故障转移需要至少两个不同区域的服务。如果你只部署到一个区域且它发生故障,负载均衡器返回 no healthy upstream
  • 跨区域内部 LB 最多 5 个 NEG 后端。这个限制适用于内部负载均衡器变体,不适用于全局外部 LB。
  • Serverless NEG 不支持 URL 掩码或标签。如果你的路由需要 URL 掩码,则无法使用 Service Health 的 NEG 模型。
  • 后端服务不支持 IAP。如果你需要 Identity-Aware Proxy,直接在 Cloud Run 服务上配置,不要在负载均衡器后端服务上配置。
  • 新实例的首次探针。新启动的实例在开始接收流量前不会进行首次就绪探针。这意味着流量可能在实例确认就绪前短暂路由到该实例。
  • 没有探针的版本被视为未知。负载均衡器将未知健康状态视为健康,因此如果你部署的版本没有配置就绪探针,它将无论如何都接收流量。

最后两点对于零停机部署很重要。官方推荐的安全发布流程(先在一个区域做金丝雀,再部署另一个区域)正是为了解决这两点。

这个架构解决不了什么

数据库可用性。计算层的故障转移毫无意义,如果你的 Cloud Run 服务连接到单区域的云数据库实例。数据库层需要自己的 HA:跨区域读写分离、分布式数据库多活架构、或者原生多区域的文档数据库。

有状态会话。Cloud Run 是无状态的。跨区域路由会使内存会话失效。使用分布式缓存(Redis)或无状态的 JWT 会话。

数据驻留。跨区域路由流量可能与数据保护法规或行业特定规定冲突。在部署多区域之前,了解你的数据驻留义务。

Pub/Sub 推送订阅。默认情况下,Pub/Sub 将消息投送到与消息存储区域相同的推送端点。全局 LB 后的多区域 Cloud Run 设置不会自动接收所有区域的 Pub/Sub 推送流量。在基于此模式构建事件驱动架构之前,请查阅 Pub/Sub 多区域推送文档获取变通方案。