PAYATHON 2026

如何在 GCP 中按用户用量计费并限制 quota?

支付阿杰

结论

GCP 无法原生按”用户”或任意 cost code 对共享项目中的资源统一计费,也无法据此设置硬性 quota。Cloud Billing、Service Quotas、预算和大多数资源上限仍以 project 为主要边界。IAM 控制的是”能否操作”,不是”最多能用多少”。

通常可以采用以下设计:

  1. 优先为每个 cost code 建立独立 project 或一组 projects,通过 folder、billing account、IAM 和组织策略统一治理。
  2. 将 Cloud Billing 数据导出到 BigQuery,再结合 project labels、resource labels 或 tags 分摊成本。
  3. 使用 project-level quotas、IAM、组织策略和自动化策略限制资源使用。
  4. 只有必须共用同一批资源,并且所有资源创建都能经过统一入口时,才考虑建设集中式资源代理 API。一般不需要为每个 cost code 单独开发一套 API。

为什么共享项目难以按用户精确管理

Billing 不等于用户审计

Cloud Billing 记录计费 SKU、项目、资源和标签等信息,但不会为所有服务稳定记录”最终由哪个用户产生了这笔费用”。

Cloud Audit Logs 可以说明”谁调用了某个 API”,却无法可靠地把每条审计日志换算成账单金额。例如:

  • 用户创建 VM 后,VM 可能继续运行数周;
  • 多个用户可能访问同一个 Cloud SQL、GKE 集群或共享存储;
  • 资源可能由 service account 创建,而实际操作来自多个用户;
  • 网络、日志、快照和存储费用会在创建操作完成后继续产生。

因此,不能简单地将 Audit Logs 中的 principal 与 Cloud Billing 费用逐行关联,并据此生成通用且准确的用户账单。

Quota 通常以 quota project 或资源所属 project 为边界

大多数 Google Cloud quota 按 project、region、zone 或特定服务维度管理。即使多个用户位于同一个项目中,通常也无法配置以下规则:

user-a: 最多 20 个 vCPU
user-b: 最多 50 个 vCPU

部分 API 支持 per-user quota 或 consumer quota,但是否可用取决于具体服务,不能作为 Compute Engine、GKE、Cloud SQL 等资源的统一用户配额机制。

IAM Conditions 可以限制用户能访问哪些资源,以及在什么条件下访问,但它不是用量计数器,无法表达”累计使用达到 100 小时后禁止继续使用”。

推荐方案:以 cost code 划分项目

如果不同 cost code 需要独立核算并实施硬限制,project 是最清晰的隔离边界:

Organization
└── Folder: Shared Platform
    ├── Project: cost-code-a-dev
    ├── Project: cost-code-a-prod
    ├── Project: cost-code-b-dev
    └── Project: cost-code-b-prod

每个 project 都可以:

  • 关联同一个 Billing Account;
  • 使用 project label 标记 cost_code、environment 和 owner;
  • 单独配置服务 quota;
  • 单独设置预算和告警;
  • 向对应用户组授予 IAM 权限;
  • 通过 Shared VPC、VPC Network Peering 或服务接口共享平台能力。

例如,为项目添加成本标签:

gcloud projects update PROJECT_ID \
  --update-labels=cost_code=cc1234,environment=prod

权限建议授予 Google Group,不要逐个绑定用户:

gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="group:cc1234-users@example.com" \
  --role="roles/compute.instanceAdmin.v1"

生产环境还应限制用户自行修改项目和资源标签,避免成本归属字段被误改或删除。

在共享项目中进行成本分摊

如果因为 GKE 集群、数据库、网络或许可证等原因必须使用共享项目,可以通过标签和账单导出管理成本。

统一成本归属字段

为支持标记的资源统一约定以下字段:

cost_code=cc1234
owner=team-a
environment=prod

最好在创建资源时写入标签,并通过 Terraform、内部平台或策略检查维持一致。实施时需要注意:

  • 并非所有服务和计费条目都支持相同的资源级标签;
  • 标签通常不会自动传播到所有子资源;
  • 网络、支持费用、承诺使用折扣和共享平台费用可能无法直接归属;
  • GKE 等共享平台还需要结合 namespace、workload 或 pod 维度的成本数据。

对于无法直接归属的成本,应提前定义分摊规则,例如按照 CPU-hours、内存 GiB-hours、请求数或固定比例分配,避免事后猜测。

导出账单到 BigQuery

启用 Cloud Billing export 后,可以按项目或资源标签聚合数据。实际表名以及字段是否存在取决于启用的导出类型,查询时应以当前导出表的 schema 为准。

以下查询按 cost_code 资源标签统计净成本:

SELECT
  COALESCE(
    (
      SELECT ANY_VALUE(label.value)
      FROM UNNEST(labels) AS label
      WHERE label.key = 'cost_code'
    ),
    'unallocated'
  ) AS cost_code,
  SUM(cost) + SUM(
    IFNULL(
      (SELECT SUM(credit.amount) FROM UNNEST(credits) AS credit),
      0
    )
  ) AS net_cost
FROM
  `billing_project.billing_dataset.gcp_billing_export_v1_BILLING_ACCOUNT_ID`
WHERE
  usage_start_time >= TIMESTAMP('2026-08-01')
  AND usage_start_time < TIMESTAMP('2026-09-01')
GROUP BY
  cost_code
ORDER BY
  net_cost DESC;

如果资源没有 cost_code 标签,还可以使用项目标签作为后备。不过,资源标签和项目标签在导出 schema 中使用不同字段,查询前需要检查实际表结构。

未分配费用也应单独统计:

SELECT
  service.description AS service,
  sku.description AS sku,
  SUM(cost) AS gross_cost
FROM
  `billing_project.billing_dataset.gcp_billing_export_v1_BILLING_ACCOUNT_ID`
WHERE
  NOT EXISTS (
    SELECT 1
    FROM UNNEST(labels) AS label
    WHERE label.key = 'cost_code'
  )
GROUP BY
  service,
  sku
ORDER BY
  gross_cost DESC;

不要忽略 unallocated。它通常意味着标签缺失、服务不支持标签,或者共享费用还没有明确的分摊规则。

如何实施资源限制

使用项目级 quota

将不同 cost code 放在独立项目后,可以分别申请或调整各项目的服务 quota。具体 quota 名称、是否允许降低以及调整方式取决于服务,并非所有 quota 都能设置任意下限。

这些机制的用途不同:

  • Quota:限制 API 请求速率或某类资源的数量;
  • Budget:监控预计费用或实际费用,并发送通知;
  • IAM:控制谁能创建、修改或删除资源;
  • Organization Policy:限制可使用的资源类型、区域或配置;
  • Billing Account:负责结算,不提供通用的用户消费硬上限。

不要把 Budget 当作硬限额

Cloud Billing Budget 默认只发送告警,不会自动停止 VM、禁用 API 或冻结项目。如需自动处理,可以让预算通知触发 Pub/Sub、Cloud Run 或 Cloud Functions,再执行预先定义的动作。

自动停服有几项明显风险:

  • 账单数据可能存在延迟;
  • 禁用 billing 或 API 可能影响共享资源;
  • 关闭资源后,不一定会立即停止所有费用;
  • 强制停机可能造成数据损坏或业务中断。

自动化动作更适合采用分级策略:

达到 70%:通知负责人
达到 90%:禁止创建高成本资源
达到 100%:停止非关键 workload
超过阈值:人工审批后执行进一步限制

要实现”禁止继续创建”,通常需要撤销创建权限、修改内部平台策略,或者让统一代理拒绝新请求。预算本身不会执行这些操作。

是否需要建设 API

如果所有用户都能直接调用 GCP API,内部 API 就无法实施可靠限制,因为用户可以绕过它。使用代理 API 时,必须同时满足以下条件:

  • 最终用户没有直接创建底层资源的 IAM 权限;
  • 只有代理使用的 service account 拥有创建权限;
  • 代理验证用户身份及其 cost code;
  • 代理维护用量台账,并执行并发量、资源数量或预算策略;
  • 所有创建、更新和删除操作都经过同一个控制面;
  • 防止用户通过 service account impersonation 等方式绕过入口。

通常只需要建设一个集中式控制面,再根据调用者所属 group、请求中的 cost code 和相关策略选择目标 project,不必为每个 cost code 复制一套 API。例如:

def create_instance(request, principal):
    cost_code = resolve_cost_code(principal)

    if not is_member(principal, cost_code):
        raise PermissionError("principal is not authorized for this cost code")

    policy = load_policy(cost_code)
    usage = read_current_usage(cost_code)

    if usage.vcpu + request.vcpu > policy.max_vcpu:
        raise QuotaExceededError("cost code vCPU limit exceeded")

    project_id = select_project(cost_code, request.environment)

    return compute.instances().insert(
        project=project_id,
        zone=request.zone,
        body=build_instance(request, cost_code),
    ).execute()

这段逻辑只是架构示意。实际实现还必须处理并发预留、创建失败后的额度释放、异步资源状态、幂等请求和账单数据延迟。如果只在创建前读取一次当前用量,并发请求可能同时通过检查,最终突破上限。

必须保留共享项目时的折中方案

如果无法拆分项目,可以组合使用以下措施:

  1. 用户只能查看和使用已有资源,不能直接创建高成本资源。
  2. 资源由集中式平台或 service account 创建。
  3. 创建请求必须携带 cost_code,平台还要验证调用者是否属于对应成本中心。
  4. 将资源标签、Audit Logs 和平台台账写入统一数据仓库。
  5. 使用 Billing export 计算实际费用,使用平台台账计算实时或近实时用量。
  6. 为共享成本制定明确、可审计的分摊公式。
  7. 通过自动化任务定期查找无标签、越权和闲置资源。

这套方案可以支持内部 chargeback/showback 和业务级限制,但它属于自建治理系统,并不是 GCP 原生的按用户 billing 或统一 user quota。

如果能够按 cost code 拆分 project,应优先拆分。共享项目加标签适合成本统计,但不适合严格隔离。代理 API 则适用于无法拆分项目,同时又确实需要自定义实时配额的场景。

备注:内容仅供参考。