PAYATHON 2026

启用 billing 并设为 unlimited 后仍触发 GAE quota limit

支付老李

结论

将 billing 设为 unlimited,只表示应用没有设置每日费用上限,不代表 Datastore 的所有配额都已取消。GAE 仍会受到项目配额、Datastore 硬性上限和平台 safety limit 的约束。

应用在次日自动恢复,可能是每日配额耗尽后重置所致。但先扣除 $0.97 再退款,不能单独证明触发了 safety limit,退款也可能是账务调整。具体原因要结合配额明细、错误日志和账单记录判断。

如何确认触发了哪一项限制

在发生问题的项目中依次核对以下信息:

  1. 打开 Google Cloud Console 的 IAM & Admin → Quotas & System Limits。
  2. 按服务筛选 Datastore、Firestore 或 App Engine,查找 reads、entity reads、Datastore API calls 等指标。
  3. 将时间范围调整到故障发生时段,确认使用量是否达到 limit。
  4. 查看该配额属于哪一类:
    • 可调整的项目配额;
    • 不可调整的系统限制;
    • 按天重置的旧版 App Engine 配额。
  5. 在 Billing → Reports / Cost table 中展开对应 SKU,确认 $0.97 的扣费和退款类型。
  6. 请 billing admin 检查预算、付款状态和结算账户限制。预算告警通常不会主动停服,但付款异常可能影响服务。

还需要检查应用日志,常见错误包括:

OverQuotaError
RESOURCE_EXHAUSTED
Quota exceeded
The API call datastore_v3.RunQuery() required more quota than is available

旧版 App Engine Python 应用还可能抛出:

from google.appengine.api import apiproxy_errors

try:
    result = run_datastore_operation()
except apiproxy_errors.OverQuotaError:
    # 降级处理
    result = None

具体错误名称取决于运行时、Datastore API 版本,以及应用使用的是旧版 Datastore 还是 Firestore in Datastore mode。

是否还要修改 limits

如果 limits 指 App Engine 控制台中的每日预算或费用上限,将其设为 unlimited 后,通常没有其他开关可以解除平台的硬配额。

处理方式取决于配额类型:

  • 可调整配额:在 Quotas 页面申请提高限额。
  • 系统限制或 safety limit:应用无法自行解除,需要联系 Google Cloud 支持。能否提高限额取决于具体指标和项目情况。
  • 旧版按日计算的 App Engine 配额:仅增加预算未必有效,还要确认应用是否仍受旧配额模型约束。
  • 使用量异常增长:提高配额只能延后故障,还需要查明读请求突然增加的原因。

不能只检查 billing 是否启用,还要确认 billing 绑定的是当前项目,并且结算账户有效,没有被暂停或关闭。

降低 DB reads

先检查是否存在全表扫描、无界查询、重复读取,或失败后的高频重试。

查询应设置合理的数量上限,并使用游标分页:

from google.appengine.ext import ndb

class Article(ndb.Model):
    status = ndb.StringProperty()
    updated_at = ndb.DateTimeProperty()

query = Article.query(Article.status == "published")
articles, next_cursor, more = query.order(
    -Article.updated_at
).fetch_page(100)

如果只需要实体键,就不要读取完整实体:

keys = Article.query(
    Article.status == "published"
).fetch(100, keys_only=True)

读取多个已知键时,应使用批量读取:

keys = [
    ndb.Key(Article, article_id)
    for article_id in article_ids
]

articles = ndb.get_multi(keys)

批量读取主要减少 API 往返次数和请求开销。实际计费读取量仍以对应 Datastore 版本的计费规则为准。

对于访问频繁且短时间内变化不大的数据,可以加入缓存:

import json
from google.appengine.api import memcache

def get_article(article_id):
    cache_key = "article:%s" % article_id
    cached = memcache.get(cache_key)

    if cached is not None:
        return json.loads(cached)

    article = ndb.Key(Article, article_id).get()
    if article is None:
        return None

    value = article.to_dict()
    memcache.set(cache_key, json.dumps(value, default=str), time=300)
    return value

还要重点排查以下问题:

  • 页面每次刷新时是否重复执行相同查询;
  • 定时任务是否集中在同一时间扫描大量实体;
  • 爬虫、恶意请求或流量突增是否放大了读取量;
  • 失败重试是否缺少指数退避;
  • 是否存在 N+1 查询;
  • 后台统计任务是否可以改用聚合结果或预计算数据。

防止再次停服

可以为 Datastore reads 设置低于配额上限的监控告警,例如使用量达到 60%、80% 和 90% 时通知值班人员。告警指标名称会随旧版 Datastore、Firestore in Datastore mode 和项目配置而变化,应在项目的 Metrics Explorer 中选择实际存在的指标。

读配额紧张时,非关键功能可以主动降级,例如返回缓存、暂停统计任务或限制高成本查询。重试必须使用指数退避,避免在配额不足时引发请求风暴:

import random
import time

def retry_with_backoff(operation, attempts=5):
    delay = 1.0

    for attempt in range(attempts):
        try:
            return operation()
        except Exception:
            if attempt == attempts - 1:
                raise

            time.sleep(delay + random.uniform(0, delay * 0.2))
            delay = min(delay * 2, 30)

实际代码中不要捕获所有 Exception,而应替换为客户端库提供的临时错误或配额错误类型。对于确定性的 quota exhaustion,也不要无限重试。

应先根据故障时段的 Quotas 图表和日志确定具体指标。确认配额名称,并判断它属于可调整配额还是系统硬限制后,才能决定申请扩容,还是通过缓存、限流和查询优化来降低读取量。

备注:内容仅供参考。