PAYATHON 2026

如何为 Google App Engine 构建 Billing Monitor?

支付老李

结论

Google App Engine 没有类似 getPercentBillingCeiling() 的运行时 API,应用也不适合自行读取“当前费用占每日 billing ceiling 的百分比”。

目前可采用以下方案:

  • 在 Google Cloud Budgets 中设置 80%、85%、90%、95%、98% 和 100% 阈值。
  • 使用原生邮件通知,或将预算事件发送到 Pub/Sub,再通过 Cloud Run / Cloud Functions 发送自定义邮件。
  • 如果需要每天按 $50 重新计算,可以启用 Cloud Billing Export to BigQuery,定期汇总当天费用,并记录已经触发的阈值。

Cloud Billing 预算只负责告警,不会自动停止消费。费用数据还可能存在延迟,因此无法保证在超过上限前阻止应用继续运行。旧版 App Engine 的每日 billing ceiling 和当前 Google Cloud 的预算机制也不是同一个概念。

优先使用 Google Cloud Budgets

可以在 Google Cloud Console 的 Billing 页面为指定项目创建预算,并添加多个实际支出阈值:

80%
85%
90%
95%
98%
100%

使用原生邮件通知时,通常不需要另外编写监控程序。如果通知需要发送给自定义收件人、聊天机器人或告警系统,可以配置 Pub/Sub 通知。

下面是一个 Terraform 示例:

resource "google_pubsub_topic" "billing_alerts" {
  name = "billing-alerts"
}

resource "google_billing_budget" "gae_budget" {
  billing_account = var.billing_account_id
  display_name    = "GAE cost alerts"

  amount {
    specified_amount {
      currency_code = "USD"
      units         = "50"
    }
  }

  filter {
    projects = [
      "projects/${var.project_number}"
    ]
  }

  threshold_rules {
    threshold_percent = 0.80
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.85
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.90
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.95
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.98
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 1.00
    spend_basis       = "CURRENT_SPEND"
  }

  all_updates_rule {
    pubsub_topic   = google_pubsub_topic.billing_alerts.id
    schema_version = "1.0"
  }
}

这里的 $50 是预算金额,不一定等同于旧版 App Engine 所说的“每日硬上限”。Google Cloud Budgets 可用的预算周期取决于当前产品能力和具体配置。如果控制台或 Budget API 不支持所需的每日周期,就要自行汇总每日费用。

创建预算、发布 Pub/Sub 消息和订阅消息所需的 IAM 权限需要分别配置。预算资源创建成功,并不表示 Pub/Sub 的服务代理已经具备完整权限。

Pub/Sub 通知的处理方式

预算事件可以发送到 Pub/Sub。消费程序收到消息后,应根据事件中的预算金额、当前费用和已越过的阈值判断是否发送邮件。

示意代码如下:

import base64
import json
from decimal import Decimal

def handle_budget_event(cloud_event):
    message = cloud_event.data["message"]
    payload = json.loads(
        base64.b64decode(message["data"]).decode("utf-8")
    )

    cost = Decimal(str(payload["costAmount"]))
    budget = Decimal(str(payload["budgetAmount"]))
    threshold = Decimal(str(payload.get("alertThresholdExceeded", 0)))

    percent = Decimal("0") if budget == 0 else cost / budget

    subject = (
        f"Billing alert: {percent:.1%} of budget used"
    )
    body = (
        f"Budget: {budget}\n"
        f"Current cost: {cost}\n"
        f"Threshold: {threshold:.0%}\n"
        f"Currency: {payload.get('currencyCode', '')}\n"
    )

    send_email(subject, body)


def send_email(subject: str, body: str) -> None:
    # 调用已选定的邮件服务,例如 SMTP、SendGrid 或其他邮件 API。
    # 密钥应放入 Secret Manager,不要写入源码。
    raise NotImplementedError

预算通知可能重复投递,也可能随着费用增长多次更新,因此处理程序必须具备幂等性。可以使用 Firestore 保存类似下面的唯一键:

billing-account/project + budget-id + billing-period + threshold

发送邮件前先检查该键,发送成功后再记录状态。若要严格避免并发引起的重复邮件,应使用事务或带前置条件的原子写入。

实现真正的每日费用检查

如果费用需要每天零点重新按 $50 计算,而预算功能又无法直接提供所需周期,可以启用 Cloud Billing Export to BigQuery,再通过计划任务查询当天费用。

示例 SQL:

SELECT
  COALESCE(
    SUM(
      cost
      + IFNULL(
          (SELECT SUM(credit.amount) FROM UNNEST(credits) AS credit),
          0
        )
    ),
    0
  ) AS net_cost
FROM
  `billing_export_project.billing_dataset.gcp_billing_export_v1_*`
WHERE
  DATE(usage_start_time, "Asia/Shanghai")
    = CURRENT_DATE("Asia/Shanghai")
  AND project.id = "your-gae-project-id";

这段查询会将 credits 计入净费用。如果业务中的“费用”指抵扣前金额,可以只汇总 cost。税费、调整项、舍入方式和不同导出表的字段也可能影响结果,具体应以实际账单口径为准。

查询结果可以按以下逻辑判断:

from decimal import Decimal

THRESHOLDS = tuple(
    Decimal(value)
    for value in ("0.80", "0.85", "0.90", "0.95", "0.98", "1.00")
)

def crossed_thresholds(
    previous_cost: Decimal,
    current_cost: Decimal,
    daily_limit: Decimal,
) -> list[Decimal]:
    if daily_limit <= 0:
        return []

    previous_ratio = previous_cost / daily_limit
    current_ratio = current_cost / daily_limit

    return [
        threshold
        for threshold in THRESHOLDS
        if previous_ratio < threshold <= current_ratio
    ]

假设上次检查时费用为 $39,本次检查时已达到 $46,程序会同时识别 80%、85% 和 90% 三个阈值。可以分别发送三封邮件,也可以合并为一封,说明在两次检查之间越过了多个阈值。

常见的执行链路如下:

Cloud Scheduler
    → Cloud Run / Cloud Functions
    → BigQuery 查询当天费用
    → Firestore 检查阈值状态
    → 邮件服务发送通知

Cloud Scheduler 可以每隔几分钟或每小时执行一次,但提高检查频率无法消除计费数据本身的延迟。

为什么不能依靠应用内部的百分比 API

资源用量或配额 API 即使存在,也无法直接推导出准确账单,因为最终费用可能受到以下因素影响:

  • 实例规格与运行时间
  • 请求、带宽和存储用量
  • 免费额度
  • 折扣、合同价格和 credits
  • 跨项目共享资源
  • 计费数据入库延迟
  • 税费及后续账单调整

Cloud Billing Budget API 主要用于创建、查询和管理预算配置,不是实时费用查询 API。Cloud Billing Catalog API 提供的是公开价格信息,也不代表某个账号实际适用的最终价格。用公开单价乘以应用指标,只能得到估算值。

防止应用因费用问题突然失效

费用邮件适合用作预警,不能成为唯一的保护措施。需要可靠 failover 的应用还应配置:

  • Cloud Monitoring 的错误率、延迟和实例数告警
  • 关键服务的 quota 告警
  • 限流、降级和熔断逻辑
  • 非关键任务的暂停开关
  • 跨区域或独立系统的故障转移方案
  • 独立于被监控项目的通知通道

不要只在同一个 App Engine 应用中运行 billing monitor。如果项目因配额、权限、账单或服务状态出现问题,监控程序可能同时停止。将监控部署到独立项目,并使用单独的 Pub/Sub、Cloud Run 和通知凭据,可靠性会更高。

还需要预留足够的缓冲。预算事件和 Billing Export 都不是实时数据,因此 98% 或 100% 阈值更适合确认费用状态和升级告警,不适合承担“阻止停机”的职责。如果需要提前处置,应把可操作阈值设在更低的位置,例如 70% 或 80%。

备注:内容仅供参考。