Google App Engine 使用 RemoteApiServlet 批量加载的计费问题
结论
在旧版 Google App Engine 的计费模型中,这种情况可能是正常的。通过 RemoteApiServlet 发起并以应用管理员身份认证的 Remote API 请求,其 CPU 使用量可能不会计入最终账单,但仍会占用应用的资源配额。
因此,以下两种现象可以同时出现:
- Billing history 中的 CPU 费用为
$0.0。 - CPU quota 达到上限后,Remote API 返回 HTTP 503。
原因在于,资源配额统计和最终计费是两套机制。CPU 不收费,并不代表它不受配额限制。
为什么控制台显示约 70 CPU hours
Bulkloader 不会绕过应用直接写入 Datastore。它通过 RemoteApiServlet 向应用发送请求,再由应用执行 Datastore 操作。因此,控制台仍会记录这些请求消耗的 CPU、Datastore 操作和带宽等资源。
资源使用页面中的 CPU hours 主要用于配额统计和运行监控,不能直接按照“显示的 CPU hours × 单价”计算最终账单。实际费用还取决于请求类型、管理员认证、免费额度,以及当时生效的 App Engine 计费规则。
Billing history 也可能有结算延迟。如果刚完成上传就查看账单,可以等计费周期更新后再确认。如果经过数个结算周期后 CPU 费用仍为零,而且所有请求确实都来自经过管理员认证的 Remote API,通常说明这些请求采用了 Remote API 的特殊计费方式。
为什么达到预算后仍然出现 HTTP 503
在当时的 App Engine 模型中,max. daily budget 不只是信用卡扣款上限,也会影响应用当天可使用的付费资源配额。
系统判断 CPU 配额耗尽后,会在资源准入阶段拒绝请求。这个阶段早于最终账单结算,因此可能出现以下过程:
- Bulkloader 请求消耗 CPU。
- CPU 使用量计入应用的每日配额。
- 配额达到免费额度和 daily budget 共同决定的上限。
- 后续请求被拒绝,并返回 HTTP 503。
- 结算系统随后根据 Remote API 请求的计费属性计算费用。
- CPU 最终可能不收费,但之前触发的配额限制不会随之撤销。
也就是说,HTTP 503 表示当前无法继续分配资源,并不表示已经产生了等额账单。
处理方法
先在管理控制台中确认 503 是否确实由 CPU quota 引起,同时排除 Datastore 配额、请求超时和瞬时服务故障。重点检查:
- CPU quota 是否达到 100%。
- Datastore 写入相关配额是否已经耗尽。
- 503 是否只在当天剩余时间出现,并在配额重置后恢复。
- 应用日志中是否出现
OverQuotaError、deadline exceeded 或 Datastore 错误。 - Billing history 是否已经完成延迟结算。
如果确认是 CPU quota,可以采取以下措施:
- 在能够承担相应费用风险的前提下提高 daily budget。
- 减少每个批次的实体数量,避免资源在短时间内集中消耗。
- 降低 Bulkloader 的并发度。
- 遇到 HTTP 503 时使用指数退避重试。
- 将超大数据集拆分到多个配额周期中导入。
- 避免重复写入同一实体或执行不必要的查询。
简单的指数退避逻辑如下:
import random
import time
def retry_with_backoff(operation, max_attempts=8):
delay = 1.0
for attempt in range(max_attempts):
try:
return operation()
except ServiceUnavailableError:
if attempt == max_attempts - 1:
raise
time.sleep(delay + random.uniform(0, delay * 0.25))
delay = min(delay * 2, 60.0)
如果 503 是因为每日配额已经完全耗尽,持续重试无法立即解决问题。此时只能等待配额重置,或提高允许使用的资源额度。指数退避更适合处理短暂过载和瞬时 503。
RemoteApiServlet 配置注意事项
RemoteApiServlet 应仅允许管理员访问。例如,Java 应用常见的 web.xml 配置如下:
<servlet>
<servlet-name>remoteapi</servlet-name>
<servlet-class>
com.google.apphosting.utils.remoteapi.RemoteApiServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>remoteapi</servlet-name>
<url-pattern>/remote_api</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Remote API</web-resource-name>
<url-pattern>/remote_api</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
不要为了方便导入而公开 /remote_api。Remote API 通常可以直接读写生产环境中的 Datastore,配置不当会带来严重的数据安全风险。
需要注意的版本差异
这个问题对应的是 App Engine 早期以 CPU hours 和 daily budget 为核心的计费方式。此后,Google 多次调整 App Engine 的运行时、配额、实例计费和 Datastore 计费规则。因此,上述行为不能直接套用到当前的 App Engine、Cloud Datastore 或 Firestore。
对于仍在运行的旧应用,应以项目当时使用的运行时、账单明细和配额页面为准。判断时需要分别核对:
- 资源使用页面记录了哪些消耗。
- 配额页面因为什么原因拒绝请求。
- 最终账单实际结算了哪些费用。
这三个页面的用途不同,显示的数字不一定能一一对应。
备注:内容仅供参考。
