# 为 SaaS 计量对象存储和 S3 账单

> 针对 S3、GCS 和 Azure Blob 的存储上传、容量与带宽向客户收费。构建准确对象存储计量的架构。
- **Author**: Ayush Agarwal
- **Published**: 2026-05-18
- **Category**: Architecture, Storage, Usage-based Billing
- **URL**: https://dodopayments.com/blogs/zh/metering-object-storage-s3-billing

---

对象存储计费意味着要对四个相互独立的维度进行计量，而不是一个：上传的字节数、随时间存储的字节数、流出的字节数，以及针对存储层执行的操作数。难点在第二项，因为存储容量不是可以计数的事件，而是必须通过采样计算的滚动积分；大多数团队都是在第一张发票与 S3 账单对不上之后才发现这一点。

这四个维度都可以独立增长，客户对它们的关注点也各不相同。备份服务会对存储收费。文件托管服务会对存储和带宽收费。媒体平台会对存储、转换和分发收费。每种产品的计费形态不同，但底层的计量管道采用相同的架构。

本文将介绍如何为由对象存储支持的 SaaS 产品构建计量管道。这里的讨论涵盖 AWS S3、Google Cloud Storage、Azure Blob Storage 以及任何兼容的对象存储，并假设产品是 SaaS 或 AI 产品，而不是电商产品。

## 对象存储计量实际需要完成什么

对象存储计费的维度比大多数基于用量的产品更多，因此架构也更有意思。

它会在文件进入系统的瞬间捕获上传事件，包括字节数、客户标识符，以及可选的路径前缀或存储桶标签和时间戳。

它会捕获随时间变化的存储容量。存储字节数不是一个固定数字，而是一个滚动积分。第一天上传并保留三十天的文件，会贡献三十天的存储容量。管道必须计算出正确的总量。

它会在文件离开系统时捕获下载或流出事件。流出通常与存储采用不同的收费方式，并且不同区域的每单位费率也不同。

在适用时，它会捕获操作次数。有些定价模型除了按字节收费，还会对存储层的请求收费。Get、put、list、delete 都要分别计数。

它必须像任何基于用量的管道一样，能够应对重试、部分失败和高流量场景。如果产品允许客户端直接上传，流量可能会非常高。

它必须支持业务模型。有些产品按上传的每 GB 收费，并且免费包含存储额度。有些产品按每月存储的每 GB 收费。有些产品采用分层组合计费。聚合方式必须与业务模型匹配；在确定计量器设计之前，可以参考我们的[按量计费指南](https://dodopayments.com/blogs/metered-pricing-guide)，了解如何在这些计费形态之间做出选择。

这五项工作与任何用量管道相同。不同之处在于，成本依据包含多个维度，时间积分与事件计数同样重要。

## 分层解析架构

整体形态与 LLM 和 API 网关计量类似，但每一层都需要考虑对象存储的特有因素。

### 应用层或上传边界的埋点

最适合埋点的位置取决于你的产品是中转上传，还是让客户端直接访问存储。

如果由应用服务器处理上传，就在应用服务器中埋点。上传完成后，读取文件大小，并发送包含客户标识符、字节数以及其他所需元数据的事件。

如果应用通过预签名 URL 或临时凭证为客户端提供直接上传权限，可以在生成 URL 的步骤中使用上传请求中的大小标头进行埋点；也可以异步监听存储提供商的事件，例如 S3 Object Created 通知。

对于支持直接上传的产品，异步模式更可靠。存储提供商明确知道对象何时创建以及对象有多大。监听这些通知可以避免一类问题：应用认为上传已经发生，但存储层的实际状态并非如此。

对于随时间变化的存储容量，埋点方式不同。容量不是事件，而是需要采样或计算的状态。有两种可行模式。第一种是在应用数据库中为每个客户维护一个实时余额：上传时增加，删除时减少，然后每天向计量器发送快照。第二种是按日直接查询存储提供商，获取每个客户的存储桶总大小。第一种响应更快，第二种准确性更高。

流出事件通常在提供字节内容的 CDN 或源站层捕获。大多数 CDN 提供商会以日志或指标的形式提供流出数据，你可以按客户进行聚合。

### 传输与批处理

传输层与任何高流量计量系统相同。上传事件进入队列，工作进程将其批量处理，并使用幂等键提交到事件接收 API。事件接收 API 接受客户标识符、事件名称和 bytes 属性。

对于每日存储容量快照，由于每天每个客户只有一个事件，传输更加简单。定时任务读取实时余额或查询存储提供商，并为每个客户提交一个事件，其中包含当天的 GB 天数。

流出事件通常在源头进行批处理。CDN 日志按小时或按天聚合，然后与上传事件一起送入同一事件接收管道。

### 平台上的聚合

计费平台会将事件聚合到计量器中。对于对象存储，通常会有多个计量器并行运行。

对上传字节数使用求和计量器，用于按每 GB 上传量计费。

对存储 GB 天数使用求和计量器，用于按每月存储的每 GB 计费。每日快照事件会为该计量器提供数据。

对流出字节数使用求和计量器，用于按每 GB 下载量计费。

在计费模型包含操作费用时，对操作数使用计数计量器，用于按每百万次操作计费。

每个计量器都有自己的订阅额度和超额费率。客户的发票会按计量器分别显示明细项目。

### 跨系统对账

对象存储的对账层需要连接三个系统：你的应用日志、存储提供商自己的用量和账单报告，以及计费平台的计量器总量。

每日对账任务会拉取这三类数据并进行比较。存储提供商的报告是容量和操作数的事实来源。你的应用是客户归属关系的事实来源。计费平台是实际收费金额的事实来源。

差异通常属于几类情况：应用与计费平台之间的传输过程中丢失了事件；存储提供商的计数包含未完成分段上传产生的孤立对象；路径前缀或存储桶结构发生变化，导致客户归属关系发生偏移。每一类问题都有不同的修复方式，对账任务需要能够识别具体属于哪一类。

## 对象存储特有的陷阱

对象存储会带来一些其他用量产品通常不会遇到的问题。

第一是分段上传的计费。客户端开始一次分段上传，上传了几个分片，然后放弃。存储提供商会将这些分片保留在未完成状态，并对其收费。你的应用可能会计算这些分片，也可能不会。如果按上传的每 GB 收费，就必须明确是否对未完成的上传收费，并且政策需要与存储提供商用于清理这些分片的生命周期规则保持一致。

第二是副本和版本的计费。如果启用了版本控制或跨区域复制，一个逻辑对象可能会占用相当于多个物理对象的容量。让客户为每个副本付费通常不是正确做法，但意外地这样计费却很容易发生。存储提供商的存储桶大小默认包含所有版本和所有副本。

第三是冷存储和分层。有些产品会自动将较旧的对象移动到更便宜的存储层。容量相同，但成本依据不同。如果你按存储的每 GB 统一收费，就需要自行承担成本差异；根据访问模式不同，这可能带来正面或负面的利润影响。如果你也对客户采用分层定价，计量系统就需要知道每个对象所处的层级。

第四是流出归属。CDN 日志有时缺少清晰的客户标识符，尤其是公共文件托管场景，因为 URL 可能是唯一信号。最清晰的做法是将客户标识符嵌入 URL 路径或查询参数，然后从 CDN 日志中解析出来。没有这一点，流出计费就不可靠。

第五是跨区域定价。存储和流出价格会因区域以及跨区域传输而变化。客户通常希望价格统一，这意味着你需要承担区域差异。计量管道仍然需要记录每个事件所属的区域，以便你分析底层成本情况。

## Dodo Payments 如何融入其中

Dodo Payments 提供聚合层和事件接收 API。你可以在控制台中定义计量器：对上传量、存储 GB 天数和流出字节数的 bytes 属性进行求和聚合。然后从你的应用、定时快照任务和 CDN 日志聚合器调用事件接收 API。

针对对象存储，平台提供了存储接收蓝图，将事件接收 API 封装为适用于 S3、Google Cloud Storage 和 Azure Blob Storage 上传的常见模式。SDK 简化了事件发送步骤，并确保 bytes 属性按照计量器要求正确构建。

架构上的优势在于，平台会处理聚合、订阅管理、超额计费和开票，流程与我们[实施基于用量的计费](https://dodopayments.com/blogs/implement-usage-based-billing)指南中介绍的设置步骤一致。你的应用只需负责捕获正确的事件，并将其归属到正确的客户。对账层仍由你负责，因为只有你的团队了解应用客户与存储层对象之间的关系。

对象存储蓝图和事件接收 API 的完整参考位于[对象存储接收蓝图](https://docs.dodopayments.com/developer-resources/ingestion-blueprints/object-storage)和[基于用量的计费指南](https://docs.dodopayments.com/developer-resources/usage-based-billing-guide)中。本文介绍架构和常见陷阱，文档则提供具体实现。

## 文件托管 SaaS 的参考架构

将各个部分组合起来，一个典型文件托管产品的管道如下所示。

客户通过你的应用上传文件。应用生成一个用于直接上传到 S3 的预签名 URL，并将包含客户标识符的键作为前缀。客户端直接将文件上传到 S3。

S3 会为每次成功上传发送一个 Object Created 事件。Lambda 函数监听这些事件，从键中提取客户标识符，从事件中读取文件大小，并向事件接收 API 发送计费事件。事件包含客户标识符、表示上传字节数的事件名称，以及 bytes 值。

定时任务每晚运行。它遍历所有客户，查询存储层获取每个客户的存储桶总大小，并为每个客户发送一个包含当天 GB 天数的事件。总 GB 数乘以一天，就等于该快照的 GB 天数。经过一个月的累积后，这些数值会形成客户的存储 GB 天数总量。

另一个任务每小时聚合 CDN 日志。它按客户分组统计流出字节数，从请求路径或查询参数中解析客户标识符，然后向事件接收 API 发送聚合后的事件。

计费平台将这三条事件流聚合到三个计量器中。客户的订阅为每个计量器设置额度。每个计量器独立计算超额用量。发票分别显示这三个明细项目。

每日对账任务会将当天接收的事件与 S3 指标、CDN 日志总量和平台计量器总量进行比较。超过阈值的差异会向值班工程师发出告警。

客户可以在你的产品中看到用量面板，其中显示本计费周期已上传的 GB 数、今天已存储的 GB 数，以及本计费周期已下载的 GB 数。这些数字从平台 API 读取，并与最终开票的数字保持一致。

## 运营维护规范

以下习惯有助于长期保持对象存储计量的准确性。

至少每月将存储提供商的账单与计量器总量进行核对。对象存储成本不容忽视，利润率在不知不觉中被压缩的情况很常见。审计是发现这类问题的唯一可靠方式。

为每个对象添加包含客户标识符的元数据，同时也将其写入键中。这种冗余可以让归属关系更能适应键结构变化。

定期清理孤立对象。未完成的分段上传、被放弃的临时文件以及类似对象会随时间累积。每周清理一次，可以让存储成本与可计费用量保持比例关系。

记录版本控制、副本和分层相关的政策。存储层的物理字节数与客户层的可计费 GB 数之间的关系必须明确。没有文档记录，政策就会逐渐偏移。

使用与驱动发票的相同数据来构建客户面板。如果客户在你的产品中看到一个数字，在账单上却看到另一个数字，你将花费大量支持时间解释差异。我们关于[用量上限和告警体验](https://dodopayments.com/blogs/billing-ux-usage-caps-alerts)的文章中介绍的模式同样适用于存储额度。

## 最后的思考

在真正需要为一个允许客户上传任意文件的产品交付计费管道之前，对象存储计量看起来似乎很简单。多个计量维度、存储事件的异步特性、不同区域之间的成本差异，以及物理字节数与可计费字节数之间的差距，共同构成了一项真正的工程挑战。

这里介绍的架构并非对象存储独有，但每一层都有特定于存储的注意事项，会让那些曾为更简单产品构建过计费管道的团队措手不及。尽早构建对账层。在客户看到账单之前，就明确未完成上传、版本控制和副本相关的政策。为每个对象冗余记录客户标识符。每月将存储成本与计量器总量进行审计。

做好这些工作，由对象存储支持的产品就能扩展到很大的规模，而不会让计费成为长期的痛点。做错这些工作，你就会陷入每月一次的紧急排查中，不断占用工程团队本应投入产品开发的精力。

## 常见问题

### 我应该按上传字节数还是存储字节数计费？

这取决于你与客户之间的关系。备份和归档产品通常按存储字节数计费，因为其价值在于随时间提供持久性。文件传输和短期托管产品通常按上传字节数计费，因为其价值在于完成上传。许多产品会同时按两者计费，在订阅中包含额度，并分别对每个计量器计算超额用量。

### 如何准确追踪存储字节数？

每日从存储提供商获取快照是最可靠的方法。在应用中维护实时余额成本更低，但随着时间推移会产生偏差，因为应用缺陷和存储提供商的生命周期规则可能逐渐不一致。为了获得准确性，快照模式值得承担每天查询的成本。

### 删除事件怎么办？

如果按存储 GB 天数计费，删除操作会在下一次每日快照中自然减少存储总量。如果按上传字节数计费，删除不会影响计量器，因为上传已经发生。无论哪种方式，删除通常都不是单独的计费事件，除非你的产品明确对删除操作收费。

### 可以为所有存储活动使用一个计量器吗？

可以，但很少是正确的做法。不同活动类型的成本依据不同，客户通常也希望分别看到它们。即使每个计量器的设置稍微复杂一些，同时运行三到四个计量器仍然是更清晰的模式。

### 如何处理位于不同区域的客户？

即使底层存储成本因区域而异，面向客户展示的价格也可以保持统一。计量系统会记录每个事件所属的区域，以便你分析成本。无论字节存储在哪个区域，定价层都会将计量用量转换为客户需要支付的价格。请追踪每个区域的底层利润率，以便在区域差异变得难以控制时调整价格。
---
- [More Architecture articles](https://dodopayments.com/blogs/category/architecture)
- [All articles](https://dodopayments.com/blogs)