PAYATHON 2026

Cloud Firestore / Firebase bandwidth 费用过高如何优化?

支付阿杰

结论

你的判断基本正确。如果账单里的高带宽费用来自 Firebase Storage 的视频下载,把视频文件迁移到 Bunny CDN 或其他对象存储/CDN,并让客户端直接通过新服务上传和播放,通常能明显降低 Firebase 的带宽费用。

不过,有两点需要分清:

  • Cloud Firestore 和 Firebase Storage 是两个不同的服务。视频文件应放在 Storage 中,Firestore 只保存标题、作者、播放地址等元数据。
  • 只在 Firebase Storage 前加一层 CDN,不一定能完全消除源站流量。缓存未命中、缓存过期、Range 请求和回源仍会产生 Storage 流量。要彻底迁移,视频文件本身也应转移到新的存储服务。

所以,先不要急着迁移。第一步是确认账单费用到底来自 Firestore、Cloud Storage,还是其他网络出口。

为什么 700 名用户也可能产生很高的带宽

短视频应用的成本主要由播放次数决定,而不是注册用户数。

假设每个视频平均为 2 MB,每名用户每天观看 100 个视频:

2 MB × 100 × 700 × 30
= 4,200,000 MB
≈ 4.2 TB/月

这个估算还没有包括:

  • 视频重复播放
  • 用户上传流量
  • 预加载下一个或多个视频
  • 用户滑走但没有看完的视频
  • 播放失败后的重试
  • 同一视频的不同清晰度版本
  • 缩略图、头像和其他媒体文件

即使只有几百名活跃用户,每月产生数 TB 流量也很常见。短视频应用本身就是带宽密集型业务。

先确认费用来自哪里

可以在 Google Cloud Billing 中按以下维度拆分费用:

  • Service
  • SKU
  • Project
  • Region

重点查看并区分这些项目:

  • Cloud Firestore document reads、writes、deletes
  • Cloud Firestore network egress
  • Cloud Storage data storage
  • Cloud Storage operations
  • Cloud Storage network egress
  • Cloud Functions 或 Cloud Run 网络费用

如果视频下载地址类似:

https://firebasestorage.googleapis.com/...

而且播放器直接请求这个地址,那么播放产生的主要流量通常会计入 Cloud Storage/Firebase Storage,而不是 Firestore。

如果 Firestore 文档只保存下面这类数据,文档本身产生的带宽一般远小于视频流量:

{
  "authorId": "user_123",
  "caption": "Example video",
  "videoUrl": "https://cdn.example.com/videos/abc.mp4",
  "thumbnailUrl": "https://cdn.example.com/thumbnails/abc.jpg",
  "createdAt": "..."
}

具体 SKU 名称和价格可能随区域、计费方案和时间变化,请以当前项目的 Billing report 和官方价格页面为准。

推荐的迁移架构

Firebase Authentication 和 Cloud Firestore 可以继续使用,只迁移视频的存储与分发链路:

客户端
  ├─ Firebase Authentication:用户登录
  ├─ Cloud Firestore:视频元数据、评论、点赞
  └─ CDN/Object Storage:视频上传与播放

可以按下面的流程实现:

  1. 客户端向自己的后端请求上传凭证。
  2. 后端验证 Firebase ID token。
  3. 后端生成短期上传 URL 或一次性上传凭证。
  4. 客户端把视频直接上传到对象存储。
  5. 上传完成后,由后端或可信客户端向 Firestore 写入视频元数据。
  6. 播放器直接请求 CDN 地址。

这样,视频文件不会经过 Firestore,也不需要先上传到 Firebase Storage,再复制到其他存储。

不要把 CDN 密钥放进客户端

对象存储的永久 API key、Storage password 或管理密钥不能写入 Android、iOS 或 Web 客户端。客户端安装包可以被反编译,Web 请求也能被检查。密钥一旦泄漏,攻击者可能上传、覆盖或删除文件。

正确的做法是由后端验证 Firebase 登录状态,再签发短期凭证。下面是一个不绑定具体 CDN 厂商的示例:

import express from "express";
import admin from "firebase-admin";

admin.initializeApp();

const app = express();
app.use(express.json());

app.post("/upload-session", async (req, res) => {
  const authorization = req.headers.authorization ?? "";
  const idToken = authorization.startsWith("Bearer ")
    ? authorization.slice(7)
    : null;

  if (!idToken) {
    return res.status(401).json({ error: "Missing Firebase ID token" });
  }

  try {
    const user = await admin.auth().verifyIdToken(idToken);

    const objectKey = `videos/${user.uid}/${crypto.randomUUID()}.mp4`;

    // 调用所选存储服务的服务端 API,
    // 创建短期 signed upload URL 或一次性上传凭证。
    const uploadUrl = await createSignedUploadUrl(objectKey);

    return res.json({
      objectKey,
      uploadUrl,
      expiresIn: 300
    });
  } catch {
    return res.status(401).json({ error: "Invalid Firebase ID token" });
  }
});

客户端获得短期 URL 后,直接上传文件:

async function uploadVideo(file, firebaseUser) {
  const idToken = await firebaseUser.getIdToken();

  const sessionResponse = await fetch("/upload-session", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${idToken}`,
      "Content-Type": "application/json"
    }
  });

  if (!sessionResponse.ok) {
    throw new Error("Unable to create upload session");
  }

  const { uploadUrl, objectKey } = await sessionResponse.json();

  const uploadResponse = await fetch(uploadUrl, {
    method: "PUT",
    headers: {
      "Content-Type": file.type
    },
    body: file
  });

  if (!uploadResponse.ok) {
    throw new Error("Video upload failed");
  }

  return objectKey;
}

createSignedUploadUrl() 的实现取决于所选服务商。需要先确认 Bunny CDN 或其他供应商当前支持的认证方式,再填写对应的 API 参数,不能直接套用其他服务商的签名规则。

Firestore 中只保存受控的对象标识

不要完全信任客户端提交的任意 URL,例如:

{
  "videoUrl": "https://unknown.example/something.mp4"
}

更稳妥的做法是保存受控的 objectKey,再由后端或应用使用固定的 CDN 域名构造播放地址:

function getVideoUrl(objectKey) {
  const allowedPattern = /^videos\/[A-Za-z0-9_-]+\/[A-Za-z0-9-]+\.mp4$/;

  if (!allowedPattern.test(objectKey)) {
    throw new Error("Invalid object key");
  }

  return `https://cdn.example.com/${objectKey}`;
}

这样可以减少恶意链接、域名替换和脏数据进入 Firestore 的风险。

仅添加 CDN 是否足够

如果继续把 Firebase Storage 作为源站,只在前面增加 CDN,能节省多少流量取决于缓存命中率。

适合缓存的公开视频通常需要满足这些条件:

  • URL 稳定且不可变
  • 设置合理的 Cache-Control
  • 文件更新时更换文件名或版本号
  • CDN 支持 HTTP Range 请求
  • 播放器始终使用 CDN 域名

例如,可以使用带版本标识的路径:

https://cdn.example.com/videos/user_123/550e8400-e29b-41d4-a716-446655440000.mp4

不可变文件可以使用类似的缓存策略:

Cache-Control: public, max-age=31536000, immutable

如果视频会在原路径上被覆盖,就不要设置长期不可变缓存,否则用户可能长时间看到旧文件。

即使缓存配置正确,首次访问和缓存未命中时仍会回源。因此:

  • “Firebase Storage + 外部 CDN”可以减少源站下载流量;
  • “外部对象存储 + CDN”才能真正把视频存储和分发流量迁出 Firebase。

还可以做哪些优化

避免过度预加载

短视频列表通常会提前加载后续视频。如果一次预加载多个完整 MP4,用户快速滑动时会产生大量无效流量。

可以只预加载下一个视频,并在卡片离开可视区域后取消请求。同时检查播放器是否会因组件重建而重复下载同一资源。

使用自适应码率

单个 MP4 使用简单,但不便于根据网络状况控制流量。可以考虑 HLS 或 MPEG-DASH,生成多种分辨率和码率,让客户端只下载当前网络环境和屏幕实际需要的片段。

转码、分片和多版本存储同样会产生费用,需要结合完播率和观看时长评估,不能只比较单个文件的大小。

检查 Range 请求和缓存行为

播放器通常会发送:

Range: bytes=...

迁移前需要确认 CDN:

  • 能正确支持 Range 请求
  • 不会因 Range 缓存策略频繁回源
  • 能正确返回 206 Partial Content
  • 不会在每次拖动进度条时重新下载整个文件

限制上传

服务端需要限制:

  • 文件大小
  • MIME type
  • 每名用户的上传频率
  • 每日上传数量
  • 对象路径
  • 上传凭证有效期

只检查客户端文件扩展名并不够。上传完成后,还可以通过后台任务检查文件的真实类型和媒体信息。

设置预算告警

预算告警通常不会自动停止服务,但可以帮助你尽早发现费用异常增长。还应监控:

  • 每日 CDN 流量
  • 单用户播放流量
  • 每个视频的请求次数
  • 缓存命中率
  • 上传和播放失败后的重试次数
  • 热门视频的流量分布

迁移时不要忽略隐藏成本

比较服务商时,不能只看每 GB 的下载价格,还要计算:

总成本 =
存储费用
+ CDN 下行流量
+ 存储到 CDN 的回源流量
+ 请求次数费用
+ 上传与跨区域流量
+ 转码费用
+ 图片处理费用
+ 日志和监控费用

还需要确认服务区域、最低消费、缓存清除费用、访问控制、签名 URL、内容审核、删除响应时间和服务条款。

最稳妥的实施顺序

可以先选择少量视频做灰度迁移:

  1. 从 Billing report 确认高费用对应的具体服务和 SKU。
  2. 记录当前日均播放量、视频下载量和单次播放流量。
  3. 让新上传的视频使用新的对象存储和 CDN。
  4. 在 Firestore 中增加 storageProvider 或 objectKey,兼容新旧视频。
  5. 让少量用户通过 CDN 播放,检查 Range、缓存命中率和失败率。
  6. 对比迁移前后的 Firebase Storage 流量和整体成本。
  7. 确认运行稳定后,再迁移历史视频。
  8. 最后删除不再使用的 Firebase Storage 副本,避免重复支付存储费。

如果视频确实是主要流量来源,把视频存储和播放链路迁移到价格更合适的对象存储/CDN,通常能明显降低 Firebase 的带宽费用。Firebase 仍然可以负责认证、数据库和业务逻辑,不必因为视频带宽问题迁移整个后端。

备注:内容仅供参考。